File Coverage

blib/lib/Gtk2/Ex/Datasheet/DBI.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             # (C) Daniel Kasak: dan@entropy.homelinux.org
2             # See COPYRIGHT file for full license
3              
4             # See 'man Gtk2::Ex::Datasheet::DBI' for full documentation ... or of course continue reading
5              
6             package Gtk2::Ex::Datasheet::DBI;
7              
8 1     1   35859 use strict;
  1         2  
  1         39  
9              
10             #use warnings;
11 1     1   4 no warnings;
  1         2  
  1         27  
12              
13 1     1   1039 use Data::Dumper;
  1         12088  
  1         103  
14              
15 1     1   414 use Glib qw/TRUE FALSE/;
  0            
  0            
16             use Gtk2::Pango;
17              
18             use Gtk2::Ex::Dialogs (
19             destroy_with_parent => TRUE,
20             modal => TRUE,
21             no_separator => FALSE
22             );
23              
24             # Record Status Indicators
25             use constant {
26             UNCHANGED => 0,
27             CHANGED => 1,
28             INSERTED => 2,
29             DELETED => 3,
30             LOCKED => 4
31             };
32              
33             # Record Status column
34             use constant {
35             STATUS_COLUMN => 0
36             };
37              
38             BEGIN {
39             $Gtk2::Ex::DBI::Datasheet::VERSION = '2.1';
40             }
41              
42             sub new {
43            
44             my ( $class, $req ) = @_;
45            
46             # Assemble object from request
47             my $self = {
48             dbh => $$req{dbh}, # A database handle
49             primary_key => $$req{primary_key}, # The primary key ( needed for inserts / updates )
50             schema => $$req{schema}, # Database schema ( not required for MySQL )
51             search_path => $$req{search_path}, # Schema search paths ( not required for MySQL )
52             sql => $$req{sql}, # A hash of SQL related stuff
53             treeview => $$req{treeview}, # A Gtk2::Treeview to connect to
54             footer_treeview => $$req{footer_treeview}, # A Gtk2::Treeview to connect to ( for the footer )
55             vbox => $$req{vbox}, # A vbox to create treeview(s) in
56             footer => $$req{footer}, # A boolean to activate the footer treeview
57             fields => $$req{fields}, # Field definitions
58             column_info => $$req{column_info} || undef, # 'Faked' column_info
59             multi_select => $$req{multi_select}, # Boolean to enable multi selection mode
60             column_sorting => $$req{column_sorting} || 0, # Boolean to activate ( incomplete ) column sorting
61             read_only => $$req{read_only}, # Boolean to indicate read-only mode
62             before_apply => $$req{before_apply}, # Code that runs *before* each *record is applied
63             on_apply => $$req{on_apply}, # Code that runs *after* each *record* is applied
64             on_row_select => $$req{on_row_select}, # Code that runs when a row is selected
65             on_changed => $$req{on_changed}, # Code that runs when a record is changed ( any column )
66             after_size_allocate => $$req{after_size_allocate} || undef, # Code that runs after the columns have responded to a size_allocate
67             dump_on_error => $$req{dump_on_error}, # Boolean to dump SQL command on DBI error
68             friendly_table_name => $$req{friendly_table_name}, # Table name to use when issuing GUI errors
69             custom_changed_text => $$req{custom_changed_text} || undef, # Text ( including markup ) to use in GUI questions when changes need to be applied
70             data_lock_field => $$req{data_lock_field} || undef, # A field ( sql fieldname ) to use as a data-driven lock ( positive values will lock the record )
71             quiet => $$req{quiet} || 0 # Boolean to supress non-fatal warnings
72             };
73            
74             # Sanity checks ...
75             if ( ! $self->{dbh} ) {
76             die "Gtk2::Ex::Datasheet::DBI constructor missing a dbh!";
77             }
78            
79             if ( ! $self->{treeview} && ! $self->{vbox} ) {
80             die "Gtk2::Ex::Datasheet::DBI constructor requires either a treeview or a vbox!";
81             }
82            
83             if ( $self->{treeview} && $self->{vbox} ) {
84             die "You passed BOTH a treeview AND a vbox. Use one or the other!";
85             }
86            
87             if ( $self->{sql} ) {
88             if ( exists $self->{sql}->{pass_through} ) {
89             $self->{read_only} = TRUE;
90             } elsif ( ! ( exists $self->{sql}->{select} && exists $self->{sql}->{from} ) ) {
91             die "Gtk2::Ex::DBI constructor missing a complete sql definition!\n"
92             . "You either need to specify a pass_through key ( 'pass_through' )\n"
93             . "or BOTH a 'select' AND and a 'from' key\n";
94             }
95             }
96            
97             bless $self, $class;
98            
99             my $legacy_warnings;
100            
101             # Reconstruct sql object if needed
102             if ( $$req{sql_select} || $$req{table} || $$req{sql_where} || $$req{sql_order_by} ) {
103            
104             # Strip out SQL directives
105             if ( $$req{sql_select} ) {
106             $$req{sql_select} =~ s/^select //i;
107             }
108             if ( $$req{table} ) {
109             $$req{table} =~ s/^from //i;
110             }
111             if ( $$req{sql_where} ) {
112             $$req{sql_where} =~ s/^where //i;
113             }
114             if ( $$req{sql_order_by} ) {
115             $$req{sql_order_by} =~ s/^order by //i;
116             }
117            
118             # Assemble things
119             my $sql = {
120             select => $$req{sql_select},
121             from => $$req{table},
122             where => $$req{sql_where},
123             order_by => $$req{sql_order_by}
124             };
125            
126             $self->{sql} = $sql;
127            
128             $legacy_warnings = " - use the new sql object for the SQL string\n";
129            
130             }
131            
132             # Set the table name to use for GUI errors
133             if ( ! $self->{friendly_table_name} ) {
134             $self->{friendly_table_name} = $self->{sql}->{from};
135             }
136            
137             if ( $legacy_warnings || $self->{legacy_mode} ) {
138             warn "\n\n **** Gtk2::Ex::Datasheet::DBI starting in legacy mode ***\n";
139             warn "While quite some effort has gone into supporting this, it would be wise to take action now.\n";
140             warn "Warnings triggered by your request:\n$legacy_warnings\n";
141             }
142            
143             $self->{server} = $self->{dbh}->get_info( 17 );
144            
145             # Some PostGreSQL stuff - DLB
146             if ( $self->{server} =~ /postgres/i ) {
147            
148             if ( ! $self->{search_path} ) {
149             $self->{search_path} = $self->{schema} . ",public";
150             }
151            
152             my $sth = $self->{dbh}->prepare ( "SET search_path to " . $self->{search_path} );
153             $sth->execute or die $self->{dbh}->errstr;
154            
155             }
156            
157             $self->setup_fields;
158            
159             $self->setup_treeview( "treeview" );
160            
161             if ( $self->{footer} ) {
162            
163             $self->setup_treeview( "footer_treeview" );
164            
165             # Unlike the main treeview's model, which gets constructed each time
166             # we query, the footer model stays the same, and the values get updated
167            
168             $self->{footer_model} = Gtk2::ListStore->new( @{ $self->{footer_treeview_treestore_def} } );
169            
170             # Insert a row
171             $self->{footer_model}->set(
172             $self->{footer_model}->append,
173             0, 0
174             );
175            
176             $self->{footer_treeview}->set_model( $self->{footer_model} );
177            
178             }
179            
180             # Check recordset status when window is destroyed
181             my $parent_widget = $self->{treeview}->get_parent;
182             my $toplevel_widget;
183            
184             # Climb up through the widget heirarchy to find the toplevel widget ( the window )
185             while ( $parent_widget ) {
186             $toplevel_widget = $parent_widget;
187             $parent_widget = $toplevel_widget->get_parent;
188             }
189            
190             push @{$self->{objects_and_signals}},
191             [
192             $toplevel_widget,
193             $toplevel_widget->signal_connect( delete_event => sub {
194             if ( ! $self->{read_only} && $self->any_changes ) {
195             my $answer = Gtk2::Ex::Dialogs::Question->new_and_run(
196             title => "Apply changes to " . $self->{friendly_table_name} . " before closing?",
197             icon => "question",
198             text => $self->{custom_changed_text} ||
199             "There are changes to the current datasheet ( " . $self->{friendly_table_name} . " )\n"
200             . "that haven't yet been applied. Would you like to apply them before closing the form?",
201             default_yes => TRUE
202             );
203             # We return FALSE to allow the default signal handler to
204             # continue with destroying the window - all we wanted to do was check
205             # whether to apply records or not
206             if ( $answer ) {
207             if ( $self->apply ) {
208             return FALSE;
209             } else {
210             # ie don't allow the form to close if there was an error applying
211             return TRUE;
212             }
213             } else {
214             return FALSE;
215             }
216             }
217             } )
218             ];
219            
220             $self->query;
221            
222             push @{$self->{objects_and_signals}},
223             [
224             $self->{treeview},
225             $self->{treeview}->signal_connect( cursor_changed => sub {
226             my ( $path, $focus_column ) = $self->{treeview}->get_cursor;
227             if ( $path && $focus_column ) {
228             $self->{treeview}->scroll_to_cell ( undef, $focus_column, FALSE, 0.0, 0.0);
229             }
230             }
231             )
232             ];
233            
234             return $self;
235            
236             }
237              
238             sub destroy_self {
239            
240             undef $_[0];
241            
242             }
243              
244             sub destroy {
245            
246             my $self = shift;
247            
248             # Destroy signal handlers
249            
250             foreach my $set ( @{$self->{objects_and_signals}} ) {
251             $$set[0]->signal_handler_disconnect( $$set[1] );
252             }
253            
254             if ( $self->{changed_signal} ) {
255             $self->{treeview}->get_model->signal_handler_disconnect( $self->{changed_signal} );
256             }
257            
258             if ( $self->{row_select_signal} ) {
259             $self->{treeview}->get_selection->signal_handler_disconnect( $self->{row_select_signal} );
260             }
261            
262             # Destroy renderers and treeview columns
263             foreach my $field ( @{$self->{fields}} ) {
264             $field->{treeview_column}->{renderer}->destroy;
265             $field->{treeview_column}->destroy;
266             if ( $self->{footer} ) {
267             $field->{footer_treeview_column}->{renderer}->destroy;
268             $field->{footer_treeview_column}->destroy;
269             }
270             $field = undef;
271             }
272            
273             $self->destroy_self;
274            
275             }
276              
277             sub setup_fields {
278            
279             my $self = shift;
280            
281             # Cache the fieldlist array so we don't have to continually query the Database Server for it
282             my $sth;
283            
284             eval {
285             if ( exists $self->{sql}->{pass_through} ) {
286             $sth = $self->{dbh}->prepare( $self->{sql}->{pass_through} )
287             || die $self->{dbh}->errstr;
288             } else {
289             $sth = $self->{dbh}->prepare(
290             "select " . $self->{sql}->{select} . " from " . $self->{sql}->{from} . " where 0=1")
291             || die $self->{dbh}->errstr;
292             }
293             };
294            
295             if ( $@ ) {
296             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
297             title => "Error in Query!",
298             icon => "error",
299             text => "Database server says:\n\n$@"
300             );
301             if ( $self->{dump_on_error} ) {
302             if ( exists $self->{sql}->{pass_through} ) {
303             print "SQL was:\n\n" . $self->{sql}->{pass_through} . "\n\n";
304             } else {
305             print "SQL was:\n\n" . $self->{sql}->{select} . "\n\n";
306             }
307             }
308             return FALSE;
309             }
310            
311             eval {
312             $sth->execute || die $self->{dbh}->errstr;
313             };
314            
315             if ( $@ ) {
316             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
317             title => "Error in Query!",
318             icon => "error",
319             text => "Database server says:\n\n$@"
320             );
321             if ( $self->{dump_on_error} ) {
322             if ( exists $self->{sql}->{pass_through} ) {
323             print "SQL was:\n\n" . $self->{sql}->{pass_through} . "\n\n";
324             } else {
325             print "SQL was:\n\n$self->{sql}->{select}\n\n";
326             }
327             }
328             return FALSE;
329             }
330            
331             $self->{fieldlist} = $sth->{'NAME'};
332            
333             $sth->finish;
334            
335             # If there are no field definitions, then create some from our fieldlist from the database
336             if ( ! $self->{fields} ) {
337             for my $field ( @{$self->{fieldlist}} ) {
338             push @{$self->{fields}}, { name => $field };
339             }
340             }
341            
342             # Shove a _status_column_ at the front of $self->{fieldlist} and also $self->{fields}
343             # so we don't have off-by-one BS everywhere
344             unshift @{$self->{fieldlist}}, "_status_column_";
345            
346             unshift @{$self->{fields}}, {
347             name => "_status_column_",
348             renderer => "status_column",
349             header_markup => ""
350             };
351            
352             # Fetch column_info for current table ( for those that support it )
353            
354             eval {
355             if ( $self->{sql}->{pass_through} ) {
356             $sth = $self->{dbh}->column_info( undef, $self->{schema}, $self->{sql}->{pass_through}, '%' )
357             || die $self->{dbh}->errstr;
358             } else {
359             $sth = $self->{dbh}->column_info ( undef, $self->{schema}, $self->{sql}->{from}, '%' )
360             || die $self->{dbh}->errstr;
361             }
362             };
363            
364             if ( $@ ) {
365            
366             # SQLite doesn't support column_info, but it does support primary_key_info ...
367             if ( lc($self->{server}) eq "sqlite" ) {
368            
369             eval {
370             $sth = $self->{dbh}->primary_key_info( undef, undef, $self->{sql}->{from} )
371             || die $self->{dbh}->errstr;
372             };
373            
374             if ( ! $@ ) {
375             my $primary_key_info = $sth->fetchrow_hashref;
376             $self->{primary_key} = $primary_key_info->{COLUMN_NAME};
377             } else {
378             warn "\nFailed to get primary key info from SQLite!\n";
379             }
380            
381             } elsif ( ! $self->{quiet} ) {
382            
383             # We don't really want a dialog error message in this case. Dump a warning to the console
384             # that we can't get column info, and continue ( renderers will default to text )
385             warn "\nCouldn't get column info ( based on " . $self->{friendly_table_name} . " ) from database ...\n"
386             . " ... This will happen in a multi-table query ...\n"
387             . " ... Defaulting to text renderers for undefined fields\n\n";
388            
389             }
390            
391             if ( ! $self->{primary_key} ) {
392            
393             if ( ! $self->{quiet} ) {
394             warn "\nGtk2::Ex::DBI::Datasheet ( based on " . $self->{friendly_table_name} . " ) MISSING primary_key definition!\n"
395             . " ... If column_info fails ( eg multi-table queries ), then you MUST ...\n"
396             . " ... provide a primary_key in the constructor ...\n"
397             . " ... if you want to be able to update the recordset ...\n"
398             . " ... Defaulting to READ-ONLY mode ...\n\n";
399             }
400            
401             $self->{read_only} = TRUE;
402            
403             } else {
404            
405             # Check if the primary key is in the field list. If not, add it.
406             if ( ! $self->column_from_sql_name( $self->{primary_key} ) ) {
407            
408             # Append the primary key to the select string
409             push @{$self->{fieldlist}}, $self->{primary_key};
410            
411             # Create a hidden column to store the PK in
412             push @{$self->{fields}},
413             {
414             name => $self->{primary_key},
415             renderer => "hidden"
416             };
417            
418             }
419            
420             }
421            
422             } else {
423            
424             my $primary_key_in_list = FALSE;
425             my $primary_key_column_info;
426             my $primary_key_position;
427            
428             while ( my $column_info_row = $sth->fetchrow_hashref ) {
429             # Set the primary key if we find one or if one is specified
430             # Current detection works for MySQL, Postgres & SQL Server only at present
431             # TODO Add support for more database servers here!
432             if (
433             ( $self->{primary_key} && $self->{primary_key} eq $column_info_row->{COLUMN_NAME} )
434             || ( exists $column_info_row->{mysql_is_pri_key} && $column_info_row->{mysql_is_pri_key} ) # MySQL
435             || $column_info_row->{TYPE_NAME} =~ m/ identity/ # SQL Server, maybe others ( Sybase ? )
436             || $column_info_row->{COLUMN_DEF} =~ m/nextval/ # Postgres
437             )
438             {
439             $self->{primary_key} = $column_info_row->{COLUMN_NAME};
440             $primary_key_column_info = $column_info_row; # We might need this later
441             }
442             # Loop through the list of columns from the database, and
443             # add only columns that we're actually dealing with
444             for my $field ( @{$self->{fieldlist}} ) {
445             # Allow column_info injection - skip if column_info already exists for this field
446             if ( $column_info_row->{COLUMN_NAME} eq $field && ! exists $self->{column_info}->{$field} ) {
447             $self->{column_info}->{$field} = $column_info_row;
448             # Also test if this is the primary key
449             # ... if we don't find one anywhere, we need to append one
450             # to the end of the select string
451             if ( ( $self->{primary_key} ) && ( $column_info_row->{COLUMN_NAME} eq $self->{primary_key} ) ) {
452             $primary_key_in_list = TRUE;
453             }
454             last;
455             }
456             }
457             }
458            
459             $sth->finish;
460            
461             if ( ! $primary_key_in_list && $self->{primary_key} ) {
462            
463             # Append the primary key to the select string
464             push @{$self->{fieldlist}}, $self->{primary_key};
465            
466             # Create a hidden column to store the PK in
467             push @{$self->{fields}},
468             {
469             name => $self->{primary_key},
470             renderer => "hidden"
471             };
472            
473             # Also add the primary key column_info stuff ( which would have been skipped
474             # if the primary key wasn't originally included in the select string
475             $self->{column_info}->{$self->{primary_key}} = $primary_key_column_info;
476            
477             }
478            
479             }
480            
481             # Remember the primary key column for later
482             $self->{primary_key_column} = $self->column_from_sql_name( $self->{primary_key} );
483            
484             # Fill in renderer types
485             my $column_no = 0;
486            
487             for my $field ( @{$self->{fields}} ) {
488            
489             # Set up column name <==> column number mapping
490             $self->{column_name_to_number_mapping}->{ $field->{name} } = $column_no;
491            
492             # Grab a default renderer type if one hasn't been defined
493             if ( ! $field->{renderer} ) {
494             my $sql_name = $self->column_name_to_sql_name( $field->{name} );
495             my $fieldtype = $self->{column_info}->{$sql_name}->{TYPE_NAME};
496             if ( $fieldtype =~ m/INT|DOUBLE/ ) {
497             $field->{renderer} = "number";
498             } elsif ( $fieldtype =~ m/CHAR/ ) {
499             $field->{renderer} = "text";
500             } elsif ( $fieldtype eq "TIMESTAMP" || $fieldtype =~ m/DATE/ ) {
501             $field->{renderer} = "date";
502             } elsif ( $fieldtype eq "TIME" ) {
503             $field->{renderer} = "time";
504             } else {
505             $field->{renderer} = "text";
506             }
507             }
508            
509             # Rename 'none' renderer to 'hidden' ... support legacy software using the old term
510             if ( $field->{renderer} eq "none" ) {
511             $field->{renderer} = "hidden";
512             }
513            
514             $field->{column} = $column_no;
515            
516             $column_no ++;
517            
518             }
519            
520             }
521              
522             sub setup_treeview {
523            
524             my ( $self, $treeview_type ) = @_;
525            
526             # Sets up the TreeView, *and* a definition for the TreeStore
527             # ( which is used to create a new TreeStore whenever we requery )
528            
529             # $type is either 'treeview' or 'footer_treeview'
530            
531             # If we're setting up the main treeview, and we've been given a vbox, construct a treeview and put it in the vbox
532            
533             if ( $treeview_type eq "treeview" && $self->{vbox} ) {
534            
535             my $sw = Gtk2::ScrolledWindow->new;
536             $sw->set_policy( "automatic", "always" );
537             $self->{vbox}->pack_start( $sw, TRUE, TRUE, 0 );
538             $self->{treeview} = Gtk2::TreeView->new;
539            
540             eval { # This might fail, but if so, we don't care
541             $self->{treeview}->set_grid_lines( "both" );
542             };
543            
544             $self->{treeview}->set_rules_hint( TRUE );
545             $sw->add( $self->{treeview} );
546             $sw->show_all;
547            
548             } elsif ( $treeview_type eq "footer_treeview" && $self->{vbox} ) {
549            
550             my $hseparator = Gtk2::HSeparator->new;
551             $self->{vbox}->pack_start( $hseparator, FALSE, TRUE, 0 );
552             $hseparator->show;
553            
554             my $sw = Gtk2::ScrolledWindow->new;
555             $sw->set_policy( "automatic", "always" );
556             $self->{vbox}->pack_start( $sw, FALSE, TRUE, 0 );
557             $self->{footer_treeview} = Gtk2::TreeView->new;
558             $self->{footer_treeview}->set_headers_visible( FALSE );
559            
560             eval { # This might fail, but if so, we don't care
561             $self->{footer_treeview}->set_grid_lines( "both" );
562             };
563            
564             $self->{footer_treeview}->set_rules_hint( TRUE );
565             $sw->set_size_request( undef, 25 ); # TODO How do we determine the row height?
566             $sw->add( $self->{footer_treeview} );
567             $sw->show_all;
568            
569             }
570            
571             # Set up icons for use in the record status column
572             if ( $treeview_type eq "treeview" ) {
573             $self->{icons}[UNCHANGED] = $self->{treeview}->render_icon( "gtk-yes", "menu" );
574             $self->{icons}[CHANGED] = $self->{treeview}->render_icon( "gtk-refresh", "menu" );
575             $self->{icons}[INSERTED] = $self->{treeview}->render_icon( "gtk-add", "menu" );
576             $self->{icons}[DELETED] = $self->{treeview}->render_icon( "gtk-delete", "menu" );
577             $self->{icons}[LOCKED] = $self->{treeview}->render_icon( "gtk-dialog-authentication", "menu" );
578            
579             foreach my $icon ( @{$self->{icons}} ) {
580             my $icon_width = $icon->get_width;
581             if ( $icon_width > $self->{status_icon_width} ) {
582             $self->{status_icon_width} = $icon_width;
583             }
584             }
585            
586             # Icons don't seem to take up the entire cell, so we need some more room. This will do ...
587             $self->{status_icon_width} += 10;
588             }
589            
590             # Now set up the model and columns
591             for my $field ( @{$self->{fields}} ) {
592            
593             my $renderer;
594            
595             # We try to default to a stock text renderer ( as it's the fastest ) where possible
596             # We can't do that for combo cells, but otherwise if cells are read-only or hidden,
597             # use the stock text renderer
598             if (
599             $field->{renderer} eq "text"
600             || $field->{renderer} eq "hidden"
601             || $field->{renderer} eq "number"
602             || (
603             ( $field->{read_only} || $self->{read_only} )
604             && $field->{renderer} !~ m/combo/
605             && $field->{renderer} ne "toggle"
606             && $field->{renderer} ne "progress"
607             && $field->{renderer} ne "status_column"
608             )
609             ) {
610            
611             if ( $treeview_type eq "footer_treeview" || $field->{renderer} eq "hidden" || $field->{read_only} || $self->{read_only} ) {
612             $renderer = Gtk2::CellRendererText->new;
613             } else {
614             $renderer = Gtk2::Ex::Datasheet::DBI::CellRendererText->new;
615             }
616            
617             $renderer->{column} = $field->{column};
618            
619             if ( $treeview_type ne "footer_treeview" && ! $self->{read_only} && ! $field->{read_only} ) {
620             $renderer->set( editable => TRUE );
621             } else {
622             $renderer->set( editable => FALSE );
623             }
624            
625             # TODO Make text wrapping work, and then document it
626             if ( $field->{wrap_text} ) {
627             $renderer->set( 'wrap-mode', 'word' );
628             }
629            
630             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes(
631             $field->{name},
632             $renderer,
633             'text' => $field->{column}
634             );
635            
636             # 'date_only' render functions need to be converted to 'date_only_text' for text renderers
637             # ( and we're in the text renderer section here )
638             my $counter = 0;
639             foreach my $render_function ( @{$field->{builtin_render_functions}} ) {
640             if ( $render_function eq "date_only" ) {
641             $render_function = "date_only_text";
642             }
643             $counter ++;
644             }
645            
646             if ( $field->{renderer} eq "hidden" ) {
647             $field->{ $treeview_type . "_column" }->set_visible( FALSE );
648             }
649            
650             push @{$self->{objects_and_signals}},
651             [
652             $renderer,
653             $renderer->signal_connect( edited => sub { $self->process_text_editing( @_ ); } )
654             ];
655            
656             $self->{ $treeview_type }->append_column( $field->{ $treeview_type . "_column" } );
657            
658             # Add a string column to the TreeStore definition ( recreated when we query() )
659             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::String";
660            
661             } elsif ( $field->{renderer} eq "combo" ) {
662            
663             $renderer = Gtk2::CellRendererCombo->new;
664             $renderer->{column} = $field->{column};
665            
666             # Get the data type and attach it to the renderer, so we know what kind of comparison
667             # ( string vs numeric ) to use later
668             my $sql_name = $self->column_name_to_sql_name( $field->{name} );
669             my $fieldtype = $self->{column_info}->{$sql_name}->{TYPE_NAME};
670            
671             if ( $fieldtype =~ m/INT/ ) {
672             $renderer->{data_type} = "numeric";
673             } else {
674             $renderer->{data_type} = "string";
675             }
676            
677             if ( ! $self->{read_only} && ! $field->{read_only} ) {
678            
679             $renderer->set(
680             editable => TRUE,
681             text_column => 1,
682             has_entry => FALSE # TODO Periodically investigate: Gtk2::CellRendererCombos's 'has_entry' MUST be disabled to avoid http://bugzilla.gnome.org/show_bug.cgi?id=317387
683             );
684            
685             # It's possible that we won't have a model at this point
686             if ( $field->{model} ) {
687             $renderer->set( model => $field->{model} );
688             }
689            
690             } else {
691            
692             $renderer->set( editable => FALSE );
693            
694             }
695            
696             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes(
697             $field->{name},
698             $renderer,
699             text => $field->{column}
700             );
701            
702             push @{$self->{objects_and_signals}},
703             [
704             $renderer,
705             $renderer->signal_connect( edited => sub { $self->process_text_editing( @_ ) } )
706             ];
707            
708             $self->{ $treeview_type }->append_column( $field->{ $treeview_type . "_column" } );
709            
710             # We have to do this *after* the column is added ( directly above )
711             if ( $field->{model_setup} ) {
712             $self->setup_combo( $field->{name} ) ;
713             }
714            
715             push @{$field->{ $treeview_type . "_column" }->{builtin_render_functions}}, sub { $self->render_combo_cell( @_ ) };
716            
717             # Add a string column to the TreeStore definition ( recreated when we query() )
718             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::String";
719            
720             } elsif ( $field->{renderer} eq "dynamic_combo" ) {
721            
722             $renderer = Gtk2::CellRendererCombo->new;
723             $renderer->{column} = $field->{column};
724            
725             # For a dynamic combo, we have to tell the TreeViewColumn where the model is.
726             # Therefore we need to keep track of how many models we've got.
727             # We can't use $self->column_from_name() because this only works for columns that have a matching
728             # field in our SQL command ( ie are in $self->{fieldlist} ). We also have to be careful not to
729             # upset the order of columns in $self->column_from_name and $self->{fieldlist} ... ie we should
730             # append these models at the end of the the main model, just before the primary key
731            
732             $self->{dynamic_models} ++;
733             $renderer->{dynamic_model_no} = $self->{dynamic_models};
734             $renderer->{dynamic_model_position} = scalar @{$self->{fieldlist}} + $self->{dynamic_model_no};
735            
736             # Keep this position number in the field has as well
737             $field->{dynamic_model_position} = $renderer->{dynamic_model_position};
738            
739             if ( ! $self->{read_only} && ! $field->{read_only} ) {
740             $renderer->set(
741             editable => TRUE,
742             text_column => 1,
743             has_entry => FALSE # TODO Periodically investigate: Gtk2::CellRendererCombos's 'has_entry' MUST be disabled to avoid http://bugzilla.gnome.org/show_bug.cgi?id=317387
744             );
745             } else {
746             $renderer->set(
747             editable => FALSE
748             );
749             }
750            
751             # Get the data type and attach it to the renderer, so we know what kind of comparison
752             # ( string vs numeric ) to use later
753             my $sql_name = $self->column_name_to_sql_name( $field->{name} );
754             my $fieldtype = $self->{column_info}->{$sql_name}->{TYPE_NAME};
755            
756             if ( $fieldtype =~ m/INT/ ) {
757             $renderer->{data_type} = "numeric";
758             } else {
759             $renderer->{data_type} = "string";
760             }
761            
762             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes(
763             $field->{name},
764             $renderer,
765             text => $field->{column},
766             model => $renderer->{dynamic_model_position}
767             );
768            
769             push @{$self->{objects_and_signals}},
770             [
771             $renderer,
772             $renderer->signal_connect( edited => sub { $self->process_text_editing( @_ ); } )
773             ];
774            
775             $self->{ $treeview_type }->append_column( $field->{ $treeview_type . "_column" } );
776            
777             push @{$field->{ $treeview_type . "_column" }->{builtin_render_functions}}, sub { $self->render_combo_cell( @_ ) };
778            
779             # Add a string column to the TreeStore definition ( recreated when we query() )
780             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::String";
781            
782             # Add a Gtk2::ListStore column to the TreeStore definition for the model of this combo,
783             # ***BUT*** we can't add it here - queue it until the end of the 'normal' columns ( in the SQL select )
784             push @{$self->{ts_models}}, "Gtk2::ListStore";
785            
786             } elsif ( $field->{renderer} eq "toggle" ) {
787            
788             $renderer = Gtk2::CellRendererToggle->new;
789            
790             if ( ! $self->{read_only} && ! $field->{read_only} ) {
791             $renderer->set( activatable => TRUE );
792             } else {
793             $renderer->set( activatable => FALSE );
794             }
795            
796             $renderer->{column} = $field->{column};
797            
798             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes(
799             $field->{name},
800             $renderer,
801             active => $field->{column}
802             );
803            
804             push @{$self->{objects_and_signals}},
805             [
806             $renderer,
807             $renderer->signal_connect( toggled => sub { $self->process_toggle( @_ ); } )
808             ];
809            
810             $self->{ $treeview_type }->append_column( $field->{ $treeview_type . "_column" } );
811            
812             # Add an integer column to the TreeStore definition ( recreated when we query() )
813             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::Boolean";
814            
815             } elsif ( $field->{renderer} eq "progress" ) {
816            
817             $renderer = Gtk2::CellRendererProgress->new;
818            
819             $renderer->{column} = $field->{column};
820            
821             #$renderer->set( text => "" );
822            
823             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes(
824             $field->{name},
825             $renderer,
826             value => $field->{column}
827             );
828            
829             $self->{ $treeview_type }->append_column( $field->{ $treeview_type . "_column" } );
830            
831             # Add an integer column to the TreeStore definition ( recreated when we query() )
832             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::Int";
833            
834             } elsif ( $field->{renderer} eq "date" ) {
835            
836             $renderer = Gtk2::Ex::Datasheet::DBI::CellRendererDate->new;
837             $renderer->{column} = $field->{column};
838             $renderer->set( mode => "editable" );
839            
840             if ( $field->{read_only} || $self->{read_only} ) {
841             push @{$field->{builtin_render_functions}}, "date_only_text";
842             } else {
843             push @{$field->{buildin_render_functions}}, "date_only";
844             }
845            
846             # Check for a dd-mm-yyyy or dd-mm-yy builtin_render_function.
847             # Read-only cells get a text renderer ( ie not this one ), and the corresponding
848             # buildin_render_function_ddmmyyyy
849             # If the cell is *not* read-only, then CellRendererDate is used, so we
850             # *remove* dd-mm-yyyy from builtin_render_functions, and mark the column so
851             # CellRendererDate knows what to do ( ie our CellRendererDate knows about
852             # dd-mm-yyyy format internally )
853            
854             my $counter = 0;
855            
856             foreach my $render_function ( @{$field->{builtin_render_functions}} ) {
857             if ( $render_function eq "dd-mm-yyyy" ) {
858             delete $field->{builtin_render_functions}[$counter];
859             $renderer->set( format => "dd-mm-yyyy" );
860             } elsif ( $render_function eq "dd-mm-yy" ) {
861             delete $field->{builtin_render_functions}[$counter];
862             $renderer->set( format => "dd-mm-yy" );
863             }
864             $counter ++;
865             }
866            
867             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes(
868             $field->{name},
869             $renderer,
870             'date' => $field->{column}
871             );
872            
873             push @{$self->{objects_and_signals}},
874             [
875             $renderer,
876             $renderer->signal_connect( edited => sub { $self->process_text_editing( @_ ); } )
877             ];
878            
879             $self->{ $treeview_type }->append_column( $field->{ $treeview_type . "_column" } );
880            
881             # Add a string column to the TreeStore definition ( recreated when we query() )
882             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::String";
883            
884             } elsif ( $field->{renderer} eq "time" || $field->{renderer} eq "access_time" ) {
885            
886             $renderer = Gtk2::Ex::Datasheet::DBI::CellRendererTime->new;
887             $renderer->{column} = $field->{column};
888            
889             if ( $field->{renderer} eq "access_time" ) {
890             $renderer->{access_time} = 1;
891             }
892            
893             if ( ! $self->{read_only} && ! $field->{read_only} ) {
894             $renderer->set( mode => "editable" );
895             }
896            
897             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes(
898             $field->{name},
899             $renderer,
900             'time' => $field->{column}
901             );
902            
903             push @{$self->{objects_and_renderers}},
904             [
905             $renderer,
906             $renderer->signal_connect( edited => sub { $self->process_text_editing( @_ ); } )
907             ];
908            
909             if ( $field->{renderer} eq "access_time" ) {
910             push @{$field->{ $treeview_type . "_column" }->{builtin_render_functions}}, "access_time";
911             }
912            
913             $self->{ $treeview_type }->append_column($field->{ $treeview_type . "_column" });
914            
915             # Add a string column to the TreeStore definition ( recreated when we query() )
916             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::String";
917            
918             } elsif ( $field->{renderer} eq "status_column" ) {
919            
920             # The 1st column ( column 0 ) is the record status indicator: a CellRendererPixbuf
921             $renderer = Gtk2::CellRendererPixbuf->new;
922             $field->{ $treeview_type . "_column" } = Gtk2::TreeViewColumn->new_with_attributes( "", $renderer );
923            
924             $self->{ $treeview_type }->append_column( $field->{ $treeview_type . "_column" } );
925            
926             if ( $self->{read_only} ) {
927             # Hide status indicator if read-only ...
928             $field->{ $treeview_type . "_column" }->set_visible( FALSE );
929             # ... and set our status_icon_width to 0
930             $self->{status_icon_width} = 0;
931             } else {
932             # Otherwise set fixed width
933             $field->{x_absolute} = $self->{status_icon_width};
934             $field->{ $treeview_type . "_column" }->set_cell_data_func( $renderer, sub { $self->render_pixbuf_cell( @_ ); } );
935             }
936            
937             # ... and the TreeStore column that goes with it
938             push @{ $self->{ $treeview_type . "_treestore_def" } }, "Glib::Int";
939            
940             } else {
941            
942             warn "Unknown render: " . $field->{renderer} . "\n";
943            
944             }
945            
946             # Set up sorting
947             if ( $self->{column_sorting} ) {
948             $field->{ $treeview_type . "_column" }->set_sort_column_id( $field->{column} );
949             }
950            
951             # Set up on_changed stuff for this field
952             # TODO Document $field->{on_changed} support
953            
954             $renderer->{on_changed} = $field->{on_changed};
955            
956             # Pack some definition stuff into the treeviewcolumn so we can easily access it from other places ...
957             my $definition = {
958             name => $field->{name},
959             number => $field->{number},
960             date => $field->{date}
961             };
962            
963             $field->{ $treeview_type . "_column" }->{definition} = $definition;
964            
965             # ... and also shove the renderer into the treeviewcolumn hash so we can destroy it later
966             $field->{ $treeview_type . "_column" }->{renderer} = $renderer;
967            
968             # Replace the default ( whatever it is ) column header with a GtkLabel so
969             # we can format the text somewhat
970             my $label = Gtk2::Label->new;
971            
972             if ( exists $field->{header_markup} ) {
973             $label->set_markup( $field->{header_markup} );
974             } else {
975             $label->set_text( "$field->{name}" );
976             }
977            
978             $label->visible( 1 );
979            
980             $field->{ $treeview_type . "_column" }->set_widget( $label );
981            
982             # Set up column sizing stuff
983             if ( $field->{x_absolute} || $field->{x_percent} ) {
984             $field->{ $treeview_type . "_column" }->set_sizing("fixed");
985             }
986            
987             # Add any absolute x values to our total and set their column size ( once only for these )
988             if ( $field->{x_absolute} ) {
989             $field->{ $treeview_type . "_column" }->set_fixed_width( $field->{x_absolute} );
990             if ( $treeview_type eq "treeview" ) { # only add these once, ie in the main treeview cycle
991             $self->{sum_absolute_x} += $field->{x_absolute};
992             $field->{current_width} = $field->{x_absolute};
993             }
994             }
995            
996             # Set up static colouring ...
997             foreach my $property ( "foreground", "background" ) {
998             if ( $field->{ $property . "_colour" } ) {
999             $renderer->set( $property . "_set" => TRUE );
1000             $renderer->set( $property => $field->{ $property . "_colour" } );
1001             }
1002             }
1003            
1004             # ... and formatting ...
1005             if ( $field->{bold} ) {
1006             $renderer->set( weight => PANGO_WEIGHT_BOLD );
1007             }
1008            
1009             # ... and font size ...
1010             if ( $field->{font_size} ) {
1011             $renderer->set( font => $field->{font_size} );
1012             }
1013            
1014             # ... and alignment ...
1015             if ( $field->{align} ) {
1016             # Test for decimal ( from PerlFaq4 )
1017             if ( $field->{align} =~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/ ) {
1018             $renderer->set( xalign => $field->{align} );
1019             } elsif ( lc( $field->{align} ) eq "left" ) {
1020             $renderer->set( xalign => 0 );
1021             } elsif ( lc( $field->{align} ) eq "right" ) {
1022             $renderer->set( xalign => 1 );
1023             } elsif ( lc( $field->{align} ) eq "centre" || lc( $field->{align} ) eq "center" ) {
1024             $renderer->set( xalign => 0.5 );
1025             } else {
1026             warn "$field->{name} has unknown alignment: $field->{align}\n";
1027             }
1028             } elsif ( $field->{renderer} eq "number" || $field->{renderer} eq "currency" ) {
1029             $renderer->set( xalign => 1 );
1030             }
1031            
1032             # Activate 'number' builtin render function if we've got a number definition
1033             if ( exists $field->{number} ) {
1034             unshift @{$field->{builtin_render_functions}}, "number";
1035             }
1036            
1037             # TODO Remove legacy custom_cell_data_func support when I've ported all legacy stuff
1038             # We haven't released a public version with this legacy support yet, so
1039             # there's no need to support this indefinitely
1040            
1041             if ( $field->{custom_cell_data_func} ) {
1042             warn "\nMoving legacy custom_cell_data_func to new\n"
1043             . "custom_render_functions array ...\n"
1044             . "Please update your code accordingly ...\n";
1045             push
1046             @{$field->{ $treeview_type . "_column" }->{custom_render_functions}},
1047             $field->{custom_cell_data_func};
1048             delete $field->{custom_cell_data_func};
1049             }
1050            
1051             # Copy custom render functions from field definition into column
1052             # We have to put it into the column thing, otherwise it's very hard
1053             # to get to inside $self->process_render_functions
1054            
1055             if ( exists $field->{custom_render_functions} ) {
1056             # We have to suppress ticking over the Gtk2 main loop inside the query if there are any
1057             # custom render function ( some can crash things in a nasty way, particularly with the
1058             # footer functionality enabled
1059             # TODO Investigate Gtk2 main loop with footers and custom render functions further
1060             $self->{suppress_gtk2_main_iteration_in_query} = TRUE;
1061             $field->{ $treeview_type . "_column" }->{custom_render_functions} = $field->{custom_render_functions};
1062             }
1063            
1064             if ( exists $field->{builtin_render_functions} ) {
1065             $field->{ $treeview_type . "_column" }->{builtin_render_functions} = $field->{builtin_render_functions};
1066             }
1067            
1068             if (
1069             exists $field->{ $treeview_type . "_column" }->{builtin_render_functions} ||
1070             exists $field->{ $treeview_type . "_column" }->{custom_render_functions}
1071             ) {
1072             $field->{ $treeview_type . "_column" }->set_cell_data_func(
1073             $renderer,
1074             sub { $self->process_render_functions( @_ ) }
1075             );
1076             }
1077            
1078             if ( exists $field->{on_clicked} ) {
1079             print $field->{ $treeview_type . "_column" }->get_clickable . "\n";
1080             $field->{ $treeview_type . "_column" }->set_clickable( TRUE );
1081             # TODO TRACK AND DISCONNECT THIS SIGNAL!
1082             # This isn't documented yet, and I also don't use it myself, so there's no PARTICULAR hurry ...
1083             $field->{ $treeview_type . "_column" }->signal_connect( clicked => sub { $field->{on_clicked}( @_ ) } );
1084             }
1085            
1086             }
1087            
1088             # Now we've finished the 'normal' columns, we can add any queued dynamic model definitions
1089             for my $model_def ( @{$self->{ts_models}} ) {
1090             push @{ $self->{ $treeview_type . "_treestore_def" } }, $model_def;
1091             }
1092            
1093             # Now that all the columns are set up, loop over them again looking for dynamic models, so we can
1094             # set up automatic requerying of models when a column they depend on changes. We *could* have done this
1095             # in the above loop, but there's a ( remote ) chance that someone will want to set up a dynamic combo
1096             # that depends on a column *after* it ... while I can't see why people would do this, it's easy relatively
1097             # easy to accomodate anyway.
1098            
1099             for my $field ( @{$self->{fields}} ) {
1100             if ( $field->{renderer} && $field->{renderer} eq "dynamic_combo" ) {
1101             for my $criteria ( @{$field->{model_setup}->{criteria}} ) {
1102             push @{($self->{fields}[ $self->column_from_name( $criteria->{column_name} ) ]->{ $treeview_type . "_column" }->get_cell_renderers)[0]->{dependant_columns}},
1103             $field->{column};
1104             }
1105             }
1106             }
1107            
1108             $self->{ $treeview_type . "_resize_signal" } = $self->{ $treeview_type }->signal_connect( size_allocate => sub { $self->on_size_allocate( @_, $treeview_type ); } );
1109            
1110             push @{$self->{objects_and_signals}},
1111             [
1112             $self->{ $treeview_type },
1113             $self->{ $treeview_type . "_resize_signal" }
1114             ];
1115            
1116             # The expose signal gets destroyed after the 1st expose event ...
1117             # ... we only use it to align the column headers, and this only happens once
1118             $self->{ $treeview_type . "_expose_signal" } = $self->{ $treeview_type }->signal_connect( expose_event => sub { $self->on_expose_event( @_, $treeview_type ); } );
1119            
1120             # Turn on multi-select mode if requested
1121             if ($self->{multi_select}) {
1122             $self->{ $treeview_type }->get_selection->set_mode("multiple");
1123             }
1124            
1125             $self->{current_width} = 0; # Prevent warnings
1126            
1127             }
1128              
1129             sub process_render_functions {
1130            
1131             my ( $self, $tree_column, $renderer, $model, $iter, @all_other_stuff ) = @_;
1132            
1133             # This sub handles multiple rendering functions
1134            
1135             # To allow these functions to be chained together,
1136             # we copy the value from the model into the $tree_column hash, and then
1137             # ALL FUNCTIONS SHOULD USE THIS VALUE AND UPDATE IT ACCORDINGLY
1138            
1139             # ie In your custom render functions, you should pull the value from
1140             # $tree_column->{render_value}, which gets set right here:
1141            
1142             $tree_column->{render_value} = $model->get( $iter, $renderer->{column} );
1143            
1144             # First we do custom render functions ...
1145             foreach my $render_function ( @{$tree_column->{custom_render_functions}} ) {
1146             &$render_function( $tree_column, $renderer, $model, $iter, @all_other_stuff );
1147             }
1148            
1149             # ... and then the built-in ones
1150             foreach my $render_function ( @{$tree_column->{builtin_render_functions}} ) {
1151             if ( ref $render_function eq "CODE" ) {
1152             &$render_function( $tree_column, $renderer, $model, $iter );
1153             } elsif ( $render_function eq "access_time" ) {
1154             $self->builtin_render_function_access_time( $tree_column, $renderer, $model, $iter );
1155             } elsif ( $render_function eq "number" ) {
1156             $self->builtin_render_function_number( $tree_column, $renderer, $model, $iter );
1157             } elsif ( $render_function eq "dd-mm-yyyy" ) {
1158             $self->builtin_render_function_ddmmyyyy( $tree_column, $renderer, $model, $iter );
1159             } elsif ( $render_function eq "dd-mm-yy" ) {
1160             $self->builtin_render_function_ddmmyy( $tree_column, $renderer, $model, $iter );
1161             } elsif ( $render_function eq "date_only" ) {
1162             $self->builtin_render_function_date_only( $tree_column, $renderer, $model, $iter, { renderer => "date" } );
1163             } elsif ( $render_function eq "date_only_text" ) {
1164             $self->builtin_render_function_date_only( $tree_column, $renderer, $model, $iter, { renderer => "text" } );
1165             } else {
1166             warn "Unknown builtin_renderer: $render_function\n";
1167             }
1168             }
1169            
1170             }
1171              
1172             sub builtin_render_function_number {
1173            
1174             my ( $self, $tree_column, $renderer, $model, $iter ) = @_;
1175            
1176             # The $tree_column has a 'definition' hash, which is our entire field definition
1177             # In this hash, we pay attention to the 'number' hash, which may have the following keys:
1178             # - currency
1179             # - decimals
1180             # - decimal_fill
1181             # - null_if_zero
1182             # - red_if_negative
1183             # - separate_thousands
1184            
1185             my $number = $tree_column->{definition}->{number};
1186             my $value = $tree_column->{render_value};
1187            
1188             # Strip out currency / numeric formatting
1189             $value =~ s/\$|,//g;
1190            
1191             # Skip numeric stuff if possible
1192             if ( ( $number->{null_if_zero} ) && ! ( $value - 0 ) ) { # Need this to strip decimals from values such as 0.00
1193            
1194             $tree_column->{render_value} = "";
1195            
1196             } else {
1197            
1198             my $final;
1199            
1200             # Allow for our number of decimal places
1201             if ( $number->{decimals} ) {
1202             $value *= 10 ** $number->{decimals};
1203             }
1204            
1205             # Round
1206             $value = int( $value + .5 * ( $value <=> 0 ) );
1207            
1208             # Get decimals back
1209             if ( $number->{decimals} ) {
1210             $value /= 10 ** $number->{decimals};
1211             }
1212            
1213             # Split whole and decimal parts
1214             my ( $whole, $decimal ) = split /\./, $value;
1215            
1216             # Pad decimals
1217             if ( $number->{decimals} && ( ( $number->{decimal_fill} ) || ( $number->{currency} && ! exists $number->{decimal_fill} ) ) ) {
1218             if ( defined $decimal ) {
1219             $decimal = $decimal . "0" x ( $number->{decimals} - length( $decimal ) );
1220             } else {
1221             $decimal = "0" x $number->{decimals};
1222             }
1223             }
1224            
1225             # Separate thousands if specified, OR make it the default to separate them if we're dealing with currency
1226             if ( $number->{separate_thousands} || ( $number->{currency} && ! exists $number->{separate_thousands} ) ) {
1227             # This BS comes from 'perldoc -q numbers'
1228             $whole =~ s/(^[-+]?\d+?(?=(?>(?:\d{3})+)(?!\d))|\G\d{3}(?=\d))/$1,/g;
1229             }
1230            
1231             if ( $number->{decimals} ) {
1232             $value = $whole . "." . $decimal;
1233             } else {
1234             $value = $whole;
1235             }
1236            
1237             # TODO Why are we still getting commas here? Very rare anyway ...
1238             if ( $number->{red_if_negative} ) {
1239             if ( $value < 0 ) {
1240             $renderer->set( foreground => "red" );
1241             } else {
1242             $renderer->set( foreground => "black" );
1243             }
1244             }
1245            
1246             # Prepend a dollar sign for currency
1247             if ( $number->{currency} ) {
1248             $value = "\$" . $value;
1249             # If this is a negative value, we want to force the negative sign to the left of the dollar sign ...
1250             $value =~ s/\$-/-\$/;
1251             }
1252            
1253             $tree_column->{render_value} = $value;
1254            
1255             }
1256            
1257             $renderer->set( text => $tree_column->{render_value} );
1258            
1259             return FALSE;
1260            
1261             }
1262              
1263             sub builtin_render_function_ddmmyyyy {
1264            
1265             my ( $self, $tree_column, $renderer, $model, $iter ) = @_;
1266            
1267             # Only do something if we've got a value
1268            
1269             if ( $tree_column->{render_value} ) {
1270            
1271             my ( $yyyy, $mm, $dd ) = split /-/,$tree_column->{render_value};
1272            
1273             $tree_column->{render_value} = $dd . "-" . $mm . "-" . $yyyy;
1274            
1275             $renderer->set( text => $tree_column->{render_value} );
1276            
1277             }
1278            
1279             }
1280              
1281             sub builtin_render_function_ddmmyy {
1282            
1283             # TODO Document builtin_render_function_ddmmyy
1284            
1285             my ( $self, $tree_column, $renderer, $model, $iter ) = @_;
1286            
1287             # Only do something if we've got a value
1288            
1289             if ( $tree_column->{render_value} ) {
1290            
1291             my ( $yyyy, $mm, $dd ) = split /-/,$tree_column->{render_value};
1292            
1293             $tree_column->{render_value} = $dd . "-" . $mm . "-" . substr( $yyyy, 2, 2 );
1294            
1295             $renderer->set( text => $tree_column->{render_value} );
1296            
1297             }
1298            
1299             }
1300              
1301             sub builtin_render_function_access_time {
1302            
1303             my ( $self, $tree_column, $renderer, $model, $iter ) = @_;
1304            
1305             my $access_time = $model->get( $iter, $renderer->{column} );
1306             my $real_time;
1307            
1308             # If the time has been edited already, it will be a sane value,
1309             # Otherwise it will have the date '1899-12-30' shoved at the front
1310             if ( length($access_time) == 19 ) {
1311             $real_time = substr( $access_time, 11, 8 );
1312             } else {
1313             $real_time = $access_time;
1314             }
1315            
1316             $tree_column->{render_value} = $real_time;
1317            
1318             $renderer->set( time => $real_time );
1319            
1320             return FALSE;
1321            
1322             }
1323              
1324             sub builtin_render_function_date_only {
1325            
1326             my ( $self, $tree_column, $renderer, $model, $iter, $options ) = @_;
1327            
1328             my $date_string = $model->get( $iter, $renderer->{column} );
1329             my $real_date;
1330            
1331             if ( length( $date_string ) > 10 ) {
1332             $real_date = substr( $date_string, 0, 10 );
1333             } else {
1334             $real_date = $date_string;
1335             }
1336            
1337             $tree_column->{render_value} = $real_date;
1338            
1339             if ( $options->{renderer} eq "date" ) {
1340             $renderer->date( time => $real_date );
1341             } else {
1342             $renderer->set( text => $real_date );
1343             }
1344            
1345             return FALSE;
1346            
1347             }
1348              
1349             sub render_pixbuf_cell {
1350            
1351             my ( $self, $tree_column, $renderer, $model, $iter ) = @_;
1352            
1353             my $status = $model->get( $iter, STATUS_COLUMN );
1354             $renderer->set( pixbuf => $self->{icons}[$status] );
1355            
1356             return FALSE;
1357            
1358             }
1359              
1360             sub render_combo_cell {
1361            
1362             my ( $self, $tree_column, $renderer, $model, $iter ) = @_;
1363            
1364             # Get the ID that represents the text value to display
1365             my $key_value = $model->get( $iter, $renderer->{column} );
1366            
1367             my $combo_model = $renderer->get("model");
1368            
1369             if ( $combo_model ) {
1370            
1371             # Loop through our combo's model and find a match for the above ID to get our text value
1372             my $combo_iter = $combo_model->get_iter_first;
1373             my $found_match = FALSE;
1374            
1375             while ( $combo_iter ) {
1376            
1377             if ( $renderer->{data_type} eq "numeric" ) {
1378             if (
1379             $combo_model->get( $combo_iter, 0 )
1380             && $key_value
1381             && $combo_model->get( $combo_iter, 0 ) == $key_value
1382             )
1383             {
1384             $found_match = TRUE;
1385             $renderer->set( text => $combo_model->get( $combo_iter, 1 ) );
1386             last;
1387             }
1388             } else {
1389             if (
1390             $combo_model->get( $combo_iter, 0 )
1391             && $key_value
1392             && $combo_model->get( $combo_iter, 0 ) eq $key_value
1393             )
1394             {
1395             $found_match = TRUE;
1396             $renderer->set( text => $combo_model->get( $combo_iter, 1 ) );
1397             last;
1398             }
1399             }
1400            
1401             $combo_iter = $combo_model->iter_next($combo_iter);
1402            
1403             }
1404            
1405             # If we haven't found a match, default to displaying an empty value
1406             if ( ! $found_match ) {
1407             $renderer->set( text => "" );
1408             }
1409            
1410             } else {
1411            
1412             print "Gtk2::Ex::Datasheet::DBI::render_combo_cell called without a model being attached!\n";
1413            
1414             }
1415            
1416             return FALSE;
1417            
1418             }
1419              
1420             sub refresh_dynamic_combos {
1421            
1422             # If this column has dependant cells ...
1423             # ( ie dynamic combos - in this case *this* renderer will have an array of
1424             # dependant_columns pointing to the *dependant* columns )
1425             # ... refresh them
1426            
1427             my ( $self, $renderer, $path ) = @_;
1428            
1429             my $model = $self->{treeview}->get_model;
1430             my $iter = $model->get_iter( $path ); # I've been told not to pass iters around, so we'd better get a fresh one
1431            
1432             if ( $renderer->{dependant_columns} ) {
1433            
1434             # Get the current row in an array
1435             my @data = $model->get( $model->get_iter( $path ) );
1436            
1437             for my $dependant ( @{$renderer->{dependant_columns}} ) {
1438            
1439             # Create a new model
1440             my $new_model = $self->create_dynamic_model(
1441             $self->{fields}[$dependant]->{model_setup},
1442             \@data
1443             );
1444            
1445             # Dump the combo model in the main TreeView model
1446             $model->set(
1447             $iter,
1448             $self->{fields}[$dependant]->{dynamic_model_position},
1449             $new_model
1450             );
1451            
1452             }
1453            
1454             }
1455            
1456             return TRUE;
1457            
1458             }
1459              
1460             sub process_text_editing {
1461            
1462             my ( $self, $renderer, $text_path, $new_text ) = @_;
1463            
1464             my $column_no = $renderer->{column};
1465             my $path = Gtk2::TreePath->new_from_string( $text_path );
1466             my $model = $self->{treeview}->get_model;
1467             my $iter = $model->get_iter ( $path );
1468            
1469             if ( $self->{data_lock_field} ) {
1470             if ( $self->get_column_value( $self->{data_lock_field} ) ) {
1471             warn "Record locked!\n";
1472             return FALSE;
1473             }
1474             }
1475            
1476             # If this is a CellRendererCombo, then we have to look up the ID to match $new_text
1477             if ( ref($renderer) eq "Gtk2::CellRendererCombo" ) {
1478            
1479             my $combo_model;
1480            
1481             # If this is a dynamic combo, we can't get the model simply by $render->get("model") because
1482             # this is unreliable if the user has clicked outside the current row to end editing.
1483             if ( $renderer->{dynamic_model_position} ) {
1484             $combo_model = $model->get( $iter, $renderer->{dynamic_model_position} );
1485             } else {
1486             $combo_model = $renderer->get("model");
1487             }
1488            
1489             my $combo_iter = $combo_model->get_iter_first;
1490             my $found_match = FALSE;
1491            
1492             while ( $combo_iter ) {
1493            
1494             if ( $combo_model->get( $combo_iter, 1 ) eq $new_text ) {
1495             $found_match = TRUE;
1496             $new_text = $combo_model->get( $combo_iter, 0 ); # It's possible that this is a bad idea
1497             last;
1498             }
1499            
1500             $combo_iter = $combo_model->iter_next( $combo_iter );
1501            
1502             }
1503            
1504             # If we haven't found a match, default to a zero
1505             if ( ! $found_match ) {
1506             $new_text = 0; # This may also be a bad idea
1507             }
1508            
1509             }
1510            
1511             # If this is an access_time renderer, we have to shove the date '1899-12-30' at the
1512             # front of the time ( if the length of $new_text indicates it doesn't already have this )
1513             if ( $renderer->{access_time} && length( $new_text ) == 8) {
1514             $new_text = "1899-12-30 " . $new_text;
1515             }
1516            
1517             # Test to see if there is *really* a change or whether we've just received a double-click
1518             # or something else that hasn't actually changed the data
1519             my $old_text = $model->get( $iter, $column_no );
1520            
1521             if ( $old_text ne $new_text ) {
1522            
1523             if ( $self->{fields}->[$column_no]->{validation} && ! $self->{suppress_validation} ) { # Array of field defs starts at zero
1524             $self->{suppress_validation} = TRUE;
1525             if ( ! $self->{fields}->[$column_no]->{validation}(
1526             {
1527             renderer => $renderer,
1528             text_path => $text_path,
1529             new_text => $new_text
1530             }
1531             )
1532             ) {
1533             return FALSE; # Error dialog should have already been produced by validation code
1534             }
1535             }
1536            
1537             # Supress setting the record status if the changed column is an sql_ignore column
1538             if ( exists $self->{columns}[$column_no]->{sql_ignore} && $self->{columns}[$column_no]->{sql_ignore} ) {
1539             $model->signal_handler_block( $self->{changed_signal} );
1540             $model->set( $iter, $column_no, $new_text );
1541             $model->signal_handler_unblock( $self->{changed_signal} );
1542             } else {
1543             $model->set( $iter, $column_no, $new_text );
1544             }
1545            
1546             $self->{suppress_validation} = FALSE;
1547            
1548             # Refresh dependant columns if any
1549             if ( $renderer->{dependant_columns} ) {
1550             $self->refresh_dynamic_combos( $renderer, $path );
1551             }
1552            
1553             }
1554            
1555             # Execute user-defined functions
1556             if ( $renderer->{on_changed} ) {
1557             $renderer->{on_changed}();
1558             }
1559            
1560             return FALSE;
1561            
1562             }
1563              
1564             sub process_toggle {
1565            
1566             my ( $self, $renderer, $text_path, $something ) = @_;
1567            
1568             my $column_no = $renderer->{column};
1569             my $path = Gtk2::TreePath->new ( $text_path );
1570             my $model = $self->{treeview}->get_model;
1571             my $iter = $model->get_iter ( $path );
1572             my $old_value = $model->get( $iter, $renderer->{column} );
1573             my $new_text = ! $old_value;
1574            
1575             if ( $self->{data_lock_column} ) {
1576             if ( $self->get_column_value( $self->{data_lock_column} ) ) {
1577             warn "Record locked!\n";
1578             return FALSE;
1579             }
1580             }
1581            
1582             if ( exists $self->{fields}->[$column_no]->{validation} && ! $self->{fields}->[$column_no]->{validation}(
1583             {
1584             renderer => $renderer,
1585             text_path => $text_path,
1586             new_text => $new_text
1587             }
1588             )
1589             ) {
1590             return FALSE; # Error dialog should have already been produced by validation code
1591             } else {
1592            
1593             $model->set ( $iter, $renderer->{column}, $new_text );
1594            
1595             # Refresh dependant columns if any
1596             if ( $renderer->{dependant_columns} ) {
1597             $self->refresh_dynamic_combos( $renderer, $path );
1598             }
1599            
1600             }
1601            
1602             return FALSE;
1603            
1604             }
1605              
1606             sub query {
1607            
1608             my ( $self, $where_object, $dont_apply ) = @_;
1609            
1610             my $model = $self->{treeview}->get_model;
1611            
1612             if ( ! $dont_apply && $model ) {
1613            
1614             # First test to see if we have any outstanding changes to the current datasheet
1615             my $iter = $model->get_iter_first;
1616            
1617             while ( $iter ) {
1618            
1619             my $status = $model->get( $iter, STATUS_COLUMN );
1620            
1621             # Decide what to do based on status
1622             if ( $status != UNCHANGED && $status != LOCKED ) {
1623            
1624             my $answer = Gtk2::Ex::Dialogs::Question->ask(
1625             title => "Apply changes to " . $self->{friendly_table_name} . " before querying?",
1626             icon => "question",
1627             text => $self->{custom_changed_text} ||
1628             "There are outstanding changes to the current datasheet ( " . $self->{friendly_table_name} . " )."
1629             . " Do you want to apply them before running a new query?",
1630             default_yes => TRUE
1631             );
1632            
1633             if ( $answer ) {
1634             if ( ! $self->apply ) {
1635             return FALSE; # Apply method will already give a dialog explaining error
1636             }
1637             }
1638            
1639             }
1640            
1641             $iter = $model->iter_next( $iter );
1642            
1643             }
1644            
1645             }
1646            
1647             my $sql;
1648            
1649             if ( exists $self->{sql}->{pass_through} ) {
1650            
1651             $sql = $self->{sql}->{pass_through};
1652            
1653             } else {
1654            
1655             # Deal with legacy mode - the query method used to accept an optional where clause
1656             if ( $where_object ) {
1657            
1658             if ( ref( $where_object ) ne "HASH" ) {
1659            
1660             # Legacy mode
1661             # Strip 'where ' out of clause
1662             $where_object =~ s/^where //i;
1663            
1664             # Transfer new where clause if defined
1665             $self->{sql}->{where} = $where_object;
1666            
1667             # Also remove any bound values if called in legacy mode
1668             $self->{sql}->{bind_values} = undef;
1669            
1670             } else {
1671            
1672             # NOT legacy mode
1673             if ( $where_object->{where} ) {
1674             $self->{sql}->{where} = $where_object->{where};
1675             }
1676             if ( $where_object->{bind_values} ) {
1677             $self->{sql}->{bind_values} = $where_object->{bind_values};
1678             }
1679            
1680             }
1681            
1682             }
1683            
1684             $sql = "select " . $self->{sql}->{select};
1685            
1686             if ( $self->{primary_key} ) {
1687             $sql .= ", " . $self->{primary_key};
1688             }
1689            
1690             $sql .= " from " . $self->{sql}->{from};
1691            
1692             if ( $self->{sql}->{where} ) {
1693             $sql .= " where " . $self->{sql}->{where};
1694             }
1695            
1696             if ( $self->{sql}->{order_by} ) {
1697             $sql .= " order by " . $self->{sql}->{order_by};
1698             }
1699            
1700             }
1701            
1702             my $sth;
1703            
1704             eval {
1705             $sth = $self->{dbh}->prepare( $sql ) || die $self->{dbh}->errstr;
1706             };
1707            
1708             if ( $@ ) {
1709             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
1710             title => "Error preparing select statement!",
1711             icon => "error",
1712             text => "Database server says:\n\n" . $self->{dbh}->errstr
1713             );
1714             if ( $self->{dump_on_error} ) {
1715             print "SQL was:\n\n$sql\n\n";
1716             }
1717             return FALSE;
1718             }
1719            
1720             # Create a new ListStore
1721             my $liststore = Gtk2::ListStore->new( @{ $self->{treeview_treestore_def} } );
1722            
1723             eval {
1724             if ( $self->{sql}->{bind_values} ) {
1725             $sth->execute( @{$self->{sql}->{bind_values}} ) || die $self->{dbh}->errstr;
1726             } else {
1727             $sth->execute || die $self->{dbh}->errstr;
1728             }
1729             };
1730            
1731             if ( $@ ) {
1732             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
1733             title => "Error executing statement!",
1734             icon => "error",
1735             text => "Database server says:\n\n" . $self->{dbh}->errstr
1736             );
1737             if ( $self->{dump_on_error} ) {
1738             print "SQL was:\n\n$sql\n\n";
1739             }
1740             return FALSE;
1741             }
1742            
1743             # Remember the data_lock_field's position in the field array ...
1744             my $lock_position;
1745             if ( $self->{data_lock_field} ) {
1746             # Minus one because the status column ( taken into account by column_from_sql_name ) isn't in the SQL select string
1747             $lock_position = $self->column_from_sql_name( $self->{data_lock_field} ) - 1;
1748             }
1749            
1750             while ( my @row = $sth->fetchrow_array ) {
1751            
1752             my @model_row;
1753             my @dynamic_models;
1754             my $column = 0;
1755            
1756             for my $field ( @{$self->{fields}} ) {
1757            
1758             if ( $column == 0 ) {
1759            
1760             my $record_status = UNCHANGED;
1761            
1762             # Check whether this record should be locked
1763             if ( $self->{data_lock_field} ) {
1764             if ( $row[$lock_position] ) {
1765             $record_status = LOCKED;
1766             }
1767             }
1768            
1769             # Append a new treeiter, and the status indicator
1770             push @model_row, $liststore->append, STATUS_COLUMN, $record_status;
1771            
1772             } else {
1773            
1774             push @model_row,
1775             $column,
1776             $row[$column - 1]; # 1 back for the status column, which isn't in the SQL select string
1777            
1778             # If this is a dynamic combo, append it to the end of the 'normal' columns
1779             # Luckily we have already figured out it's position ...
1780             if ( $field->{renderer} && $field->{renderer} eq "dynamic_combo" ) {
1781             push @model_row,
1782             $field->{dynamic_model_position},
1783             $self->create_dynamic_model( $field->{model_setup}, \@row );
1784             }
1785            
1786             }
1787            
1788             $column++;
1789             }
1790            
1791             $liststore->set( @model_row );
1792            
1793             if ( $Gtk2::Ex::Datasheet::DBI::gtk2_main_iteration_in_query && ! $self->{suppress_gtk2_main_iteration_in_query} ) {
1794             Gtk2->main_iteration while ( Gtk2->events_pending );
1795             }
1796            
1797             }
1798            
1799             # Destroy changed_signal attached to old model ...
1800             if ( $self->{changed_signal} ) {
1801             $self->{treeview}->get_model->signal_handler_disconnect( $self->{changed_signal} );
1802             }
1803            
1804             # ... and the row_select_signal
1805             if ( $self->{row_select_signal} ) {
1806             $self->{treeview}->get_selection->signal_handler_disconnect( $self->{row_select_signal} );
1807             }
1808            
1809             $self->{treeview}->set_model( $liststore );
1810            
1811             # Refresh all dynamic combos
1812             my $iter = $liststore->get_iter_first;
1813            
1814             while ( $iter ) {
1815             my $treepath = $liststore->get_path( $iter );
1816             foreach my $field ( @{$self->{fields}} ) {
1817             my $renderer = ($field->{treeview_column}->get_cell_renderers)[0];
1818             foreach my $dependant_column ( @{$renderer->{dependant_columns}} ) {
1819             $self->refresh_dynamic_combos( $renderer, $treepath );
1820             }
1821             }
1822             $iter = $liststore->iter_next( $iter );
1823             }
1824            
1825             $self->{changed_signal} = $liststore->signal_connect( "row-changed" => sub { $self->changed(@_) } );
1826            
1827             if ( $self->{on_row_select} ) {
1828             $self->{row_select_signal} = $self->{treeview}->get_selection->signal_connect( changed => sub { $self->{on_row_select}(@_); } );
1829             }
1830            
1831             if ( $self->{footer} ) {
1832             $self->update_footer;
1833             }
1834            
1835             return TRUE;
1836            
1837             }
1838              
1839             sub undo {
1840            
1841             # undo and revert are synonyms of each other
1842            
1843             my $self = shift;
1844            
1845             $self->query( undef, TRUE );
1846            
1847             return TRUE;
1848            
1849             }
1850              
1851             sub revert {
1852            
1853             # undo and revert are synonyms of each other
1854            
1855             my $self = shift;
1856            
1857             $self->query( undef, TRUE );
1858            
1859             return TRUE;
1860            
1861             }
1862              
1863             sub changed {
1864            
1865             my ( $self, $liststore, $treepath, $iter ) = @_;
1866            
1867             my $model = $self->{treeview}->get_model;
1868            
1869             # Only change the record status if it's currently unchanged
1870             if ( ! $model->get( $iter, STATUS_COLUMN ) ) {
1871             $model->signal_handler_block( $self->{changed_signal} );
1872             $model->set( $iter, STATUS_COLUMN, CHANGED );
1873             $model->signal_handler_unblock( $self->{changed_signal} );
1874             }
1875            
1876             # Execute user-defined functions
1877             if ( $self->{on_changed} ) {
1878            
1879             $self->{on_changed}(
1880             {
1881             treepath => $treepath,
1882             iter => $iter
1883             }
1884             );
1885            
1886             }
1887            
1888             if ( $self->{footer} ) {
1889             $self->update_footer;
1890             }
1891            
1892             return FALSE;
1893            
1894             }
1895              
1896             sub update_footer {
1897            
1898             my $self = shift;
1899            
1900             my @model_row;
1901            
1902             foreach my $field ( @{$self->{fields}} ) {
1903             if ( $field->{footer_function} eq "sum" ) {
1904             push @model_row, $field->{column}, $self->sum_column( $field->{column} );
1905             } elsif ( $field->{footer_function} eq "max" ) {
1906             push @model_row, $field->{column}, $self->max_column( $field->{column} );
1907             } elsif ( $field->{footer_function} eq "average" ) {
1908             push @model_row, $field->{column}, $self->average_column( $field->{column} );
1909             } elsif ( $field->{footer_text} ) {
1910             push @model_row, $field->{column}, $field->{footer_text};
1911             } else {
1912             push @model_row, $field->{column}, undef;
1913             }
1914             }
1915            
1916             $self->{footer_model}->set(
1917             $self->{footer_model}->get_iter_first,
1918             @model_row
1919             );
1920            
1921             }
1922              
1923             sub apply {
1924            
1925             my $self = shift;
1926            
1927             my ( @iters_to_remove );
1928            
1929             if ( $self->{read_only} ) {
1930             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
1931             title => "Read Only!",
1932             icon => "warning",
1933             text => "Datasheet is open in read-only mode!"
1934             );
1935             return FALSE;
1936             }
1937            
1938             my $model = $self->{treeview}->get_model;
1939             my $iter = $model->get_iter_first;
1940            
1941             while ( $iter ) {
1942            
1943             my $status = $model->get( $iter, STATUS_COLUMN );
1944            
1945             # Decide what to do based on status
1946             if ( $status == UNCHANGED || $status == LOCKED ) {
1947             $iter = $model->iter_next( $iter );
1948             next;
1949             }
1950            
1951             my $primary_key = $model->get( $iter, $self->{primary_key_column} );
1952            
1953             if ( $self->{before_apply} ) {
1954            
1955             # Better change the status indicator back into text, rather than make
1956             # people use our constants. I think, anyway ...
1957             my $status_txt;
1958            
1959             if ( $status == INSERTED ) {
1960             $status_txt = "inserted";
1961             } elsif ( $status == CHANGED ) {
1962             $status_txt = "changed";
1963             } elsif ( $status == DELETED ) {
1964             $status_txt = "deleted";
1965             }
1966            
1967             # Do people want the whole row? I don't. Maybe others would? Wait for requests...
1968             my $result = $self->{before_apply}(
1969             {
1970             status => $status_txt,
1971             primary_key => $primary_key,
1972             model => $model,
1973             iter => $iter
1974             }
1975             );
1976            
1977             # If the user-defined before_apply() function returns 0, we abort this
1978             # update and continue with the next
1979             if ( $result == 0 ) {
1980             $iter = $model->iter_next( $iter );
1981             next;
1982             }
1983            
1984             }
1985            
1986             if ( $status == DELETED ) {
1987            
1988             my $sql = "delete from " . $self->{sql}->{from} . " where " . $self->{primary_key} . "=?";
1989            
1990             my $sth = $self->{dbh}->prepare( $sql );
1991            
1992             eval {
1993             $sth->execute( $primary_key ) || die;
1994             };
1995            
1996             if ( $@ ) {
1997             new_and_run Gtk2::Ex::Dialogs::ErrorMsg(
1998             title => "Error deleting record!",
1999             text => "Database server says:\n" . $self->{dbh}->errstr
2000             );
2001             if ( $self->{dump_on_error} ) {
2002             print "SQL was:\n\n$sql\n\n";
2003             }
2004             return FALSE;
2005             };
2006            
2007             # Remember iter for deletion later
2008             push @iters_to_remove, $iter;
2009            
2010             } else {
2011            
2012             # We process the insert / update operations in a similar fashion
2013            
2014             my $sql; # Final SQL to send to Database Server
2015             my $sql_fields; # A comma-separated list of fields
2016             my @values; # An array of values taken from the current record
2017             my $placeholders; # A string of placeholders, eg ( ?, ?, ? )
2018             my $primary_key = undef; # We pass this to the before_apply() and on_apply() functions
2019            
2020             foreach my $fieldname ( @{$self->{fieldlist}} ) {
2021            
2022             # Don't include the field if it's the primary key.
2023             # We ONLY support auto_increment type fields for primary keys, and
2024             # we shouldn't be updating these fields. This FAILS for SQL Server anyway ...
2025            
2026             # Also skip the _status_column_ which we've shoved at the front of $self->{fieldlist}
2027             # ... and also skip blank field names ( which '' / sql_ignore combos produce )
2028             if ( $fieldname eq $self->{primary_key} || $fieldname eq "_status_column_" || ! $fieldname ) {
2029             next;
2030             };
2031            
2032             my $column_no = $self->column_from_sql_name( $fieldname );
2033            
2034             # Skip if this column is set as sql_ignore
2035             # TODO Document sql_ignore ... currently incomplete and not in use
2036             if ( exists $self->{fields}[$column_no]->{sql_ignore} && $self->{fields}[$column_no]->{sql_ignore} ) {
2037             next;
2038             }
2039            
2040             if ( $status == INSERTED ) {
2041             $sql_fields .= " $fieldname,";
2042             $placeholders .= " ?,";
2043             } else {
2044             $sql_fields .= " $fieldname=?,";
2045             }
2046            
2047             my $value = $model->get( $iter, $column_no );
2048            
2049             if ( exists $self->{fields}[$column_no]->{number}
2050             && $self->{fields}[$column_no]->{number} ) {
2051             $value =~ s/[\$\,]//g;
2052             }
2053             push @values, $value;
2054            
2055             }
2056            
2057             # Remove trailing comma
2058             chop( $sql_fields );
2059            
2060             if ( $status == INSERTED ) {
2061             chop( $placeholders );
2062             $sql = "insert into " . $self->{sql}->{from} . " ( $sql_fields ) values ( $placeholders )";
2063             } elsif ( $status == CHANGED ) {
2064             $sql = "update " . $self->{sql}->{from} . " set $sql_fields where " . $self->{primary_key} . "=?";
2065             $primary_key = $model->get( $iter, $self->{primary_key_column} );
2066             push @values, $primary_key;
2067             } else {
2068             warn "WTF? Unknown status: $status in status column! Skipping ...\n";
2069             }
2070            
2071             my $sth;
2072            
2073             eval {
2074             $sth = $self->{dbh}->prepare( $sql ) || die;
2075             };
2076            
2077             if ( $@ ) {
2078             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2079             title => "Error preparing statement!",
2080             icon => "error",
2081             text => "Database server says:\n\n" . $self->{dbh}->errstr
2082             );
2083             if ( $self->{dump_on_error} ) {
2084             print "SQL was:\n\n$sql\n\n";
2085             }
2086             return FALSE;
2087             }
2088            
2089             eval {
2090             $sth->execute( @values ) || die;
2091             };
2092            
2093             if ( $@ ) {
2094             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2095             title => "Error processing recordset!",
2096             icon => "error",
2097             text => "Database server says:\n\n" . $self->{dbh}->errstr
2098             );
2099             if ( $self->{dump_on_error} ) {
2100             print "SQL was:\n\n$sql\n\n";
2101             }
2102             return FALSE;
2103             }
2104            
2105             # If we just inserted a record, we have to fetch the primary key and replace the current '!' with it
2106             if ( $status == INSERTED ) {
2107             $primary_key = $self->last_insert_id;
2108             $model->set( $iter, $self->{primary_key_column}, $primary_key );
2109             }
2110            
2111             # If we've gotten this far, the update was OK, so we'll reset the 'changed' flag
2112             # and move onto the next record
2113             $model->signal_handler_block( $self->{changed_signal} );
2114            
2115             if ( $self->{data_lock_field} ) {
2116             if ( $model->get( $iter, $self->column_from_sql_name( $self->{data_lock_field} ) ) ) {
2117             $model->set( $iter, STATUS_COLUMN, LOCKED );
2118             } else {
2119             $model->set( $iter, STATUS_COLUMN, UNCHANGED );
2120             }
2121             } else {
2122             $model->set( $iter, STATUS_COLUMN, UNCHANGED );
2123             }
2124            
2125             $model->signal_handler_unblock( $self->{changed_signal} );
2126            
2127             # Execute user-defined functions
2128             if ( $self->{on_apply} ) {
2129            
2130             # Better change the status indicator back into text, rather than make
2131             # people use our constants. I think, anyway ...
2132             my $status_txt;
2133            
2134             if ( $status == INSERTED ) {
2135             $status_txt = "inserted";
2136             } elsif ( $status == CHANGED ) {
2137             $status_txt = "changed";
2138             } elsif ( $status == DELETED ) {
2139             $status_txt = "deleted";
2140             }
2141            
2142             # Do people want the whole row? I don't. Maybe others would? Wait for requests...
2143             $self->{on_apply}(
2144             {
2145             status => $status_txt,
2146             primary_key => $primary_key,
2147             model => $model,
2148             iter => $iter
2149             }
2150             );
2151            
2152             }
2153            
2154             }
2155            
2156             $iter = $model->iter_next( $iter );
2157            
2158             }
2159            
2160             # Delete queued iters ( that were marked as DELETED )
2161             foreach $iter ( @iters_to_remove ) {
2162             $model->remove( $iter );
2163             }
2164            
2165             return TRUE;
2166            
2167             }
2168              
2169             sub insert {
2170            
2171             my ( $self, @columns_and_values ) = @_;
2172            
2173             if ( $self->{read_only} ) {
2174             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2175             title => "Read Only!",
2176             icon => "warning",
2177             text => "Datasheet is open in read-only mode!"
2178             );
2179             return FALSE;
2180             }
2181            
2182             my $model = $self->{treeview}->get_model;
2183             my $iter = $model->append;
2184            
2185             # Append any remaining fields ( ie that haven't been explicitely defined in @columns_and_values )
2186             # with default values from the database to the @columns_and_values array
2187            
2188             for my $column_no ( 1 .. @{$self->{fieldlist}} - 1) {
2189             my $found = FALSE;
2190             for ( my $x = 0; $x < ( scalar(@columns_and_values) / 2 ); $x ++ ) {
2191             #if ( $columns_and_values[ ( $x * 2 ) ] - 1 == $column_no ) { # The array is 2 wide, plus 1 for status
2192             if ( $columns_and_values[ ( $x * 2 ) ] == $column_no ) { # The array is 2 wide
2193             $found = TRUE;
2194             last;
2195             }
2196             }
2197             if ( ! $found ) {
2198             my $default = $self->{column_info}->{$self->{fieldlist}[$column_no]}->{COLUMN_DEF};
2199             if ( $default && $self->{server} =~ /microsoft/i ) {
2200             $default = $self->parse_sql_server_default( $default );
2201             }
2202             push @columns_and_values,
2203             #$column_no + 1, # Add 1 for status
2204             $column_no,
2205             $default
2206             }
2207             }
2208            
2209             my @new_record;
2210            
2211             push @new_record,
2212             $iter,
2213             STATUS_COLUMN,
2214             INSERTED;
2215            
2216             if ( scalar(@columns_and_values) ) {
2217             push @new_record,
2218             @columns_and_values;
2219             }
2220            
2221             $model->set( @new_record );
2222            
2223             # As of gtk+-2.8.19 ( or so ), this DOES NOT WORK if you have a CellRendererDate as the 1st column
2224             # ( after the status column, of course ). I don't know why. I've posted to the gtk-devel list,
2225             # but it seems like a bit of a corner-case. Perhaps someone else knows what's up.
2226             #$self->{treeview}->set_cursor( $model->get_path($iter), $self->{columns}[1], 1 );
2227            
2228             # This, however, works :)
2229             $self->{treeview}->set_cursor( $model->get_path($iter), $self->{fields}[0]->{treeview_column}, 0 );
2230            
2231             return TRUE;
2232            
2233             }
2234              
2235             sub delete {
2236            
2237             my $self = shift;
2238            
2239             if ( $self->{read_only} ) {
2240             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2241             title => "Read Only!",
2242             icon => "warning",
2243             text => "Datasheet is open in read-only mode!"
2244             );
2245             return FALSE;
2246             }
2247            
2248             # We only mark the selected record for deletion at this point
2249             my @selected_paths = $self->{treeview}->get_selection->get_selected_rows;
2250             my $model = $self->{treeview}->get_model;
2251            
2252             for my $path ( @selected_paths ) {
2253             my $iter = $model->get_iter( $path );
2254             # Prevent people from deleting locked records
2255             if ( $self->{data_lock_field} && $model->get( $iter, $self->column_from_sql_name( $self->{data_lock_field} ) ) ) {
2256             next;
2257             }
2258             $model->set( $iter, STATUS_COLUMN, DELETED );
2259             }
2260            
2261             return TRUE;
2262            
2263             }
2264              
2265             sub lock {
2266            
2267             # Locks the current record from further edits
2268            
2269             my $self = shift;
2270            
2271             if ( ! $self->{data_lock_field} ) {
2272             warn "\nGtk2::Ex::DBI::lock called without having a data_lock_field defined!\n";
2273             return FALSE;
2274             }
2275            
2276             $self->set_column_value( $self->{data_lock_field}, 1 );
2277            
2278             # Apply it ( which will implement the lock )
2279             if ( ! $self->apply ) {
2280             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2281             title => "Failed to lock record!",
2282             icon => "error",
2283             text => "There was an error applying the current record.\n"
2284             . "The lock operation has been aborted."
2285             );
2286             $self->set_column_value( $self->{data_lock_field}, 0 ); # Reset the lock column
2287             return FALSE;
2288             }
2289            
2290             return TRUE;
2291            
2292             }
2293              
2294             sub unlock {
2295            
2296             # Unlocks the current record
2297            
2298             my $self = shift;
2299            
2300             if ( ! $self->{data_lock_field} ) {
2301             warn "\nGtk2::Ex::DBI::unlock called without having a data_lock_field defined!\n";
2302             return FALSE;
2303             }
2304            
2305             # Unset the lock field
2306             $self->set_column_value( $self->{data_lock_field}, 0 );
2307            
2308             # Set the STATUS indicator ( which actually implements the lock )
2309             my ( $path, $column ) = $self->{treeview}->get_cursor;
2310             my $model = $self->{treeview}->get_model;
2311             my $iter = $model->get_iter( $path );
2312             $model->set( $iter, STATUS_COLUMN, CHANGED );
2313            
2314             if ( ! $self->apply ) {
2315             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2316             title => "Failed to unlock record!",
2317             icon => "error",
2318             text => "There was an error applying the current record.\n"
2319             . "The unlock operation has been aborted."
2320             );
2321             $self->set_column_value( $self->{data_lock_field}, 1 ); # Removes our changes to the lock column
2322             $model->set( $iter, STATUS_COLUMN, LOCKED );
2323             return FALSE;
2324             }
2325            
2326             return TRUE;
2327            
2328             }
2329              
2330             sub on_size_allocate {
2331            
2332             my ( $self, $widget, $rectangle, $treeview_type ) = @_;
2333            
2334             my ( $x, $y, $width, $height ) = $rectangle->values;
2335            
2336             if ( $self->{ $treeview_type . "_current_width" } != $width ) { # TODO Remove on_size_allocate blocking workaround when blocking actually works
2337            
2338             # Absolute values are calculated in setup_treeview as they only have to be calculated once
2339             # We take the sum of the absolute values away from the width we've just been passed, and *THEN*
2340             # allocate the remainder to fields according to their x_percent values
2341            
2342             my $available_x = $width - $self->{sum_absolute_x};
2343            
2344             $self->{ $treeview_type . "_current_width" } = $width;
2345            
2346             # TODO Resize signal blocking doesn't currently work ( completely )
2347             $self->{ $treeview_type }->signal_handler_block( $self->{ $treeview_type . "_resize_signal" } );
2348            
2349             for my $field ( @{$self->{fields}} ) {
2350            
2351             if ( $field->{x_percent} ) { # Only need to set ones that have a percentage
2352             $field->{current_width} = $available_x * ( $field->{x_percent} / 100 );
2353             # TODO Figure out why we're getting very small values when constructing our own treeview
2354             # and avoid this some other way ... this works, but ... hmmmmm
2355             if ( $field->{current_width} < 1 ) {
2356             $field->{current_width} = 1;
2357             }
2358             $field->{ $treeview_type . "_column" }->set_fixed_width( $field->{current_width} );
2359             }
2360            
2361             }
2362            
2363             # TODO Blocking resize signals doesn't currently work ( completely )
2364             $self->{ $treeview_type }->signal_handler_unblock( $self->{ $treeview_type . "_resize_signal" } );
2365            
2366             }
2367            
2368             if ( $self->{after_size_allocate} ) {
2369             # TODO Document after_size_allocate()? Or Remove?
2370             # Still could be handy, especially for setting up headers ( ie multi-row headers )
2371             $self->{after_size_allocate}();
2372             }
2373            
2374             return FALSE;
2375            
2376             }
2377              
2378             sub on_expose_event {
2379            
2380             my ( $self, $widget, $expose, $treeview_type ) = @_;
2381            
2382             # We set up the label alignment when an expose_event is triggered
2383             # ( ie when the treeview is rendered )
2384             # because the label doesn't ( apparently ) exist before this
2385             # ( ie if the treeview isn't visible ... on a notebook page that isn't selected, etc )
2386            
2387             for my $field ( @{$self->{fields}} ) {
2388            
2389             my $label = $field->{ $treeview_type . "_column" }->get_widget;
2390            
2391             if ( $label ) {
2392            
2393             # TODO Support user-defined alignment of header text
2394             # Alignment
2395             $label->get_parent->set( 0.5, 0.5, 1, 1 );
2396            
2397             # Markup
2398             if ( exists $field->{header_markup} ) {
2399             $label->set_justify( 'center' );
2400             $label->set_markup( $field->{header_markup} );
2401             }
2402            
2403             }
2404            
2405             }
2406            
2407             $self->{ $treeview_type }->signal_handler_disconnect( $self->{ $treeview_type . "_expose_signal" } );
2408            
2409             return FALSE;
2410            
2411             }
2412              
2413             sub column_from_name {
2414            
2415             my ( $self, $sql_fieldname ) = @_;
2416            
2417             # Legacy support of stoopid function name
2418             return $self->column_from_sql_name( $sql_fieldname );
2419            
2420             }
2421              
2422             sub column_from_sql_name {
2423            
2424             # Take an *SQL* field name and return the column that the field is in
2425            
2426             my ( $self, $sql_fieldname ) = @_;
2427            
2428             my $counter = 0;
2429            
2430             for my $field ( @{$self->{fieldlist}} ) {
2431             if ( $field eq $sql_fieldname ) {
2432             return $counter;
2433             }
2434             $counter ++;
2435             }
2436            
2437             }
2438              
2439             sub column_from_column_name {
2440            
2441             # Take a *COLUMN* name and returns the column that the field is in
2442            
2443             my ( $self, $column_name ) = @_;
2444            
2445             if ( exists $self->{column_name_to_number_mapping}->{ $column_name } ) {
2446             return $self->{column_name_to_number_mapping}->{ $column_name };
2447             } else {
2448             warn "Gtk2::Ex::Datasheet::DBI::column_from_column_name called with an unknown column name! ( $column_name )\n";
2449             return -1;
2450             }
2451            
2452             }
2453              
2454             sub column_name_to_sql_name {
2455            
2456             # This function converts a column name to an SQL field name
2457            
2458             my ( $self, $column_name ) = @_;
2459            
2460             my $column_no = $self->column_from_column_name ( $column_name );
2461             return $self->{fieldlist}[$column_no];
2462            
2463             }
2464              
2465             sub column_value {
2466            
2467             # This sub has been renamed to get_column_value, and is here for legacy support
2468            
2469             my ( $self, $sql_fieldname ) = @_;
2470            
2471             return $self->get_column_value( $sql_fieldname );
2472            
2473             }
2474              
2475             sub get_column_value {
2476            
2477             # This function returns the value in the requested column in the currently selected row
2478             # If multi_select is turned on and more than 1 row is selected, it looks in the 1st row
2479            
2480             my ( $self, $sql_fieldname ) = @_;
2481            
2482             my @selected_paths = $self->{treeview}->get_selection->get_selected_rows;
2483            
2484             if ( ! scalar(@selected_paths) ) {
2485             return 0;
2486             }
2487            
2488             my $model = $self->{treeview}->get_model;
2489             my @selected_values;
2490            
2491             foreach my $selected_path ( @selected_paths ) {
2492            
2493             my $column_no = $self->column_from_name( $sql_fieldname );
2494             my $value = $model->get( $model->get_iter( $selected_path ), $column_no );
2495            
2496             # Strip out dollars and commas for numeric columns
2497             # We *don't* look for a number column with currency turned on,
2498             # because sometimes you don't want to display currency formatting,
2499             # and in this case, we still want to strip out currency formatting
2500             # if people have entered it into a cell
2501            
2502             if ( exists $self->{fields}[$column_no]->{number}
2503             && $self->{fields}[$column_no]->{number} ) {
2504             $value =~ s/[\$\,]//g;
2505             }
2506            
2507             push @selected_values, $value;
2508            
2509             }
2510            
2511             # Previous behaviour was to only return the 1st selected value
2512             # To preserve backwards compatibility, we return a scalar if multi_select is off,
2513             # and we return an array if multi_select is turned on
2514             if ( $self->{multi_select} ) {
2515             return @selected_values;
2516             } else {
2517             return $selected_values[0];
2518             }
2519            
2520             }
2521              
2522             sub set_column_value {
2523            
2524             # This function sets the value in the requested column in the currently selected row
2525            
2526             my ( $self, $sql_fieldname, $value ) = @_;
2527            
2528             if ( $self->{mult_select} ) {
2529             print "Gtk2::Ex::Datasheet::DBI - set_column_value) called with multi_select enabled!\n"
2530             . " ... setting value in 1st selected row\n";
2531             }
2532            
2533             my @selected_paths = $self->{treeview}->get_selection->get_selected_rows;
2534            
2535             if ( ! scalar( @selected_paths ) ) {
2536             return 0;
2537             }
2538            
2539             my $model = $self->{treeview}->get_model;
2540             my $iter = $model->get_iter( $selected_paths[0] );
2541            
2542             $model->set(
2543             $iter,
2544             $self->column_from_name( $sql_fieldname ),
2545             $value
2546             );
2547            
2548             return TRUE;
2549            
2550             }
2551              
2552             sub last_insert_id {
2553            
2554             my $self = shift;
2555            
2556             my $primary_key;
2557            
2558             if ( $self->{server} =~ /postgres/i ) {
2559            
2560             # Postgres drivers support DBI's last_insert_id()
2561            
2562             $primary_key = $self->{dbh}->last_insert_id (
2563             undef,
2564             $self->{schema},
2565             $self->{sql}->{from},
2566             undef
2567             );
2568            
2569             } elsif ( lc($self->{server}) eq "sqlite" ) {
2570            
2571             $primary_key = $self->{dbh}->last_insert_id(
2572             undef,
2573             undef,
2574             $self->{sql}->{from},
2575             undef
2576             );
2577            
2578             } else {
2579            
2580             # MySQL drivers ( recent ones ) claim to support last_insert_id(), but I'll be
2581             # damned if I can get it to work. Older drivers don't support it anyway, so for
2582             # maximum compatibility, we do something they can all deal with.
2583             # The below works for MySQL and SQL Server, and possibly others
2584            
2585             my $sth = $self->{dbh}->prepare('select @@IDENTITY');
2586             $sth->execute;
2587            
2588             if ( my $row = $sth->fetchrow_array ) {
2589             $primary_key = $row;
2590             } else {
2591             $primary_key = undef;
2592             }
2593            
2594             }
2595            
2596             return $primary_key;
2597            
2598             }
2599              
2600             sub replace_combo_model {
2601            
2602             # This function replaces a *normal* combo ( NOT a dynamic one ) with a new one
2603            
2604             my ( $self, $column_no, $model ) = @_;
2605            
2606             my $column = $self->{treeview}->get_column($column_no);
2607             my $renderer = ($column->get_cell_renderers)[0];
2608             $renderer->set( model => $model );
2609            
2610             return TRUE;
2611            
2612             }
2613              
2614             sub create_dynamic_model {
2615            
2616             # This function accepts a combo definition and a row of data ( *MINUS* the record status column ),
2617             # and creates a combo model to insert back into the main TreeView's model
2618             # We currently only support a model with 2 columns: an ID column and a Display column
2619            
2620             # TODO create_dynamic_model: Support adding more columns to the model
2621            
2622             my ( $self, $model_setup, $data ) = @_;
2623            
2624             # Firstly we clone the database handle, as the DBD::ODBC / FreeTDS combo won't allow
2625             # multiple active statements on the same connection
2626            
2627             # TODO Test for the DBD::ODBC driver type so we don't clone the dbh unless we need to
2628            
2629             my $dbh = $self->{dbh}->clone;
2630            
2631             my $liststore = Gtk2::ListStore->new(
2632             "Glib::String",
2633             "Glib::String"
2634             );
2635            
2636             # Deal with legacy mode
2637             my $legacy_warnings;
2638            
2639             if ( $model_setup->{table} ) {
2640             $model_setup->{from} = $model_setup->{table};
2641             $legacy_warnings .= " - \$model_setup->{table} renamed to \$model_setup->{from} for consistency\n";
2642             }
2643            
2644             if ( $model_setup->{order_by} && $model_setup->{order_by} =~ m/^order by /i ) {
2645             $model_setup->{order_by} =~ s/^order by //i;
2646             $legacy_warnings .= " - ommit the words \'order by\' from \$model_setup->{order_by}\n";
2647             }
2648            
2649             if ( $model_setup->{group_by} && $model_setup->{group_by} =~ m/^group by /i ) {
2650             $model_setup->{group_by} =~ s/^group by //i;
2651             $legacy_warnings .= " - ommit the words \'order by\' from \$model_setup->{group_by}\n";
2652             }
2653            
2654             if ( $legacy_warnings ) {
2655             print "Gtk2::Ex::Datasheet::DBI::create_dynamic_model raised the following warnings:\n$legacy_warnings\n";
2656             }
2657            
2658             my $sql = "select " . $model_setup->{id} . ", " . $model_setup->{display} . " from " . $model_setup->{from};
2659             my @bind_variables;
2660            
2661             if ( $model_setup->{criteria} ) {
2662             $sql .= " where";
2663             for my $criteria ( @{$model_setup->{criteria}} ) {
2664             $sql .= " " . $criteria->{field} . "=? and";
2665             #push @bind_variables, $$data[$self->column_from_name( $criteria->{column_name} ) - 1];
2666             push @bind_variables, $$data[ $self->column_from_name( $criteria->{column_name} ) ];
2667             }
2668             }
2669            
2670             $sql = substr( $sql, 0, length($sql) - 3 ); # Remove trailing 'and'
2671            
2672             if ( $model_setup->{group_by} ) {
2673             $sql .= " " . $model_setup->{group_by};
2674             }
2675            
2676             if ( $model_setup->{order_by} ) {
2677             $sql .= " order by " . $model_setup->{order_by};
2678             }
2679            
2680             my $sth;
2681            
2682             eval {
2683             $sth = $dbh->prepare( $sql ) || die $dbh->errstr;
2684             };
2685            
2686             if ( $@ ) {
2687             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2688             title => "Error creating combo model!",
2689             icon => "error",
2690             text => "Database Server Says:\n\n$@"
2691             );
2692             if ( $self->{dump_on_error} ) {
2693             print "SQL was:\n\n$sql\n\n";
2694             }
2695             return FALSE;
2696             }
2697            
2698             $sth->execute( @bind_variables );
2699            
2700             my $iter;
2701            
2702             while ( my @record = $sth->fetchrow_array ) {
2703             $iter = $liststore->append;
2704             $liststore->set(
2705             $iter,
2706             0, $record[0],
2707             1, $record[1]
2708             );
2709             }
2710            
2711             $sth->finish;
2712             $dbh->disconnect;
2713            
2714             return $liststore;
2715            
2716             }
2717              
2718             sub setup_combo {
2719            
2720             # Convenience function that creates / refreshes a combo's model
2721            
2722             my ( $self, $combo_name ) = @_;
2723            
2724             my $column_no = $self->column_from_column_name($combo_name);
2725            
2726             my $combo = $self->{fields}[$column_no]->{model_setup};
2727            
2728             # First we clone a database connection - in case we're dealing with SQL Server here ...
2729             # ... SQL Server doesn't like it if you do too many things ( > 1 ) with one connection :)
2730             my $local_dbh;
2731            
2732             if ( exists $combo->{alternate_dbh} ) {
2733             $local_dbh = $combo->{alternate_dbh}->clone;
2734             } else {
2735             $local_dbh = $self->{dbh}->clone;
2736             }
2737            
2738             if ( ! $combo->{sql} ) {
2739             warn "\nMissing an SQL object in the combo definition for $combo_name!\n\n";
2740             return FALSE;
2741             } elsif ( ! $combo->{sql}->{from} ) {
2742             warn "\nMissing the 'from' key in the sql object in the combo definition for $combo_name!\n\n";
2743             return FALSE;
2744             }
2745            
2746             # Assemble items for liststore and SQL to get the data
2747             my ( @liststore_def, $sql );
2748            
2749             $sql = "select";
2750            
2751             foreach my $field ( @{$combo->{fields}} ) {
2752             push @liststore_def, $field->{type};
2753             $sql .= " $field->{name},";
2754             }
2755            
2756             chop( $sql );
2757            
2758             $sql .= " from $combo->{sql}->{from}";
2759            
2760             if ( $combo->{sql}->{where_object} ) {
2761             if ( ! $combo->{sql}->{where_object}->{bind_variables} && ! $self->{quiet} ) {
2762             warn "\n* * * Gtk2::Ex::Datasheet::DBI::setup_combo called with a where clause but *WITHOUT* an array of variables to bind!\n";
2763             warn "* * * While this method is supported, it is a security hazard. *PLEASE* take advantage of our support of bind variables\n\n";
2764             }
2765             $sql .= " where $combo->{sql}->{where_object}->{where}";
2766             }
2767            
2768             if ( $combo->{sql}->{group_by} ) {
2769             $sql .= " group by $combo->{sql}->{group_by}";
2770             }
2771            
2772             if ( $combo->{sql}->{order_by} ) {
2773             $sql .= " order by $combo->{sql}->{order_by}";
2774             }
2775            
2776             my $sth;
2777            
2778             eval {
2779             $sth = $local_dbh->prepare( $sql )
2780             || die $local_dbh->errstr;
2781             };
2782            
2783             if ( $@ ) {
2784             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2785             title => "Error setting up combo box: $combo_name",
2786             icon => "error",
2787             text => "Database Server Says:\n\n$@"
2788             );
2789             return FALSE;
2790             }
2791            
2792             # We have to use 'exists' here, otherwise we inadvertently create the where_object hash,
2793             # just by testing for it ... ( or by testing for bind_variables anyway )
2794             if ( exists $combo->{sql}->{where_object} && exists $combo->{sql}->{where_object}->{bind_variables} ) {
2795             eval {
2796             $sth->execute( @{$combo->{sql}->{where_object}->{bind_variables}} )
2797             || die $local_dbh->errstr;
2798             };
2799             } else {
2800             eval {
2801             $sth->execute || die $local_dbh->errstr;
2802             };
2803             }
2804            
2805             if ( $@ ) {
2806             Gtk2::Ex::Dialogs::ErrorMsg->new_and_run(
2807             title => "Error setting up combo box: $combo_name",
2808             icon => "error",
2809             text => "Database Server Says:\n\n$@\n\n"
2810             . "Check the definintion of the table:"
2811             . " $combo->{sql}->{from}"
2812             );
2813             return FALSE;
2814             }
2815            
2816             # Create the model
2817             my $model = Gtk2::ListStore->new( @liststore_def );
2818            
2819             while ( my @row = $sth->fetchrow_array ) {
2820            
2821             # We use fetchrow_array instead of fetchrow_hashref so
2822             # we can support the use of aliases in the fields
2823            
2824             my @model_row;
2825             my $column = 0;
2826             push @model_row, $model->append;
2827            
2828             foreach my $field ( @{$combo->{fields}} ) {
2829             push @model_row, $column, $row[$column];
2830             $column ++;
2831             }
2832            
2833             $model->set( @model_row );
2834            
2835             }
2836            
2837             $sth->finish;
2838            
2839             if ( lc($self->{server}) eq "sqlite" ) {
2840             warn "You're using SQLite ... the next command will throw an error. Someone please fix it.";
2841             }
2842            
2843             $local_dbh->disconnect;
2844            
2845             # Connect the model to the widget
2846             $self->replace_combo_model( $column_no, $model );
2847            
2848             return TRUE;
2849            
2850             }
2851              
2852             sub any_changes {
2853            
2854             # This function loops through all records and returns TRUE if any record status is not UNCHANGED
2855            
2856             my $self = shift;
2857            
2858             my $model = $self->{treeview}->get_model;
2859             my $iter = $model->get_iter_first;
2860            
2861             while ( $iter ) {
2862             my $status = $model->get( $iter, STATUS_COLUMN );
2863             if ( $status == UNCHANGED || $status == LOCKED ) {
2864             $iter = $model->iter_next( $iter );
2865             next;
2866             } else {
2867             return TRUE;
2868             }
2869             }
2870            
2871             return FALSE;
2872            
2873             }
2874              
2875             sub sum_column {
2876            
2877             # This function returns the sum of all values in the given column
2878             my ( $self, $column_no, $conditions ) = @_;
2879            
2880             my $model = $self->{treeview}->get_model;
2881             my $iter = $model->get_iter_first;
2882             my $total = 0;
2883            
2884             if ( $conditions ) {
2885             if ( ! ( exists $conditions->{column} && exists $conditions->{operator} && exists $conditions->{value} ) ) {
2886             warn "Gtk2::Ex::Datasheet::DBI->sum_column() called with an incomplete conditions hash ..."
2887             . " ... must have 'column', 'operator' and 'value' keys to conditions hash!\n\n";
2888             return 0;
2889             }
2890             }
2891            
2892             while ( $iter ) {
2893            
2894             # Get the column value, strip out dollar signs and commas, and then figure out what to do ...
2895             my $value = $model->get( $iter, $column_no );
2896            
2897             if ( exists $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency}
2898             && $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency} ) {
2899             $value =~ s/[\$\,]//g;
2900             }
2901            
2902             if ( $conditions ) {
2903            
2904             my $test_value = $model->get( $iter, $conditions->{column} );
2905            
2906             if ( exists $self->{fields}[$conditions->{column}]->{treeview_column}->{definition}->{number}->{currency}
2907             && $self->{fields}[$conditions->{column}]->{treeview_column}->{definition}->{number}->{currency} ) {
2908             $test_value =~ s/[\$\,]//g;
2909             }
2910            
2911             if ( $conditions->{operator} eq "==" ) {
2912             if ( $test_value == $conditions->{value} ) {
2913             $total += $value;
2914             }
2915             } elsif ( $conditions->{operator} eq "<" ) {
2916             if ( $test_value < $conditions->{value} ) {
2917             $total += $value;
2918             }
2919             } elsif ( $conditions->{operator} eq ">" ) {
2920             if ( $test_value > $conditions->{value} ) {
2921             $total += $value;
2922             }
2923             } elsif ( $conditions->{operator} eq "eq" ) {
2924             if ( $test_value eq $conditions->{value} ) {
2925             $total += $value;
2926             }
2927             } else {
2928             warn "Gtk2::Ex::Datasheet::DBI->sum_column() called with an invalid operator in the condition ...\n"
2929             . " ... operator: $conditions->{operator}\n\n";
2930             }
2931             } else {
2932             $total += $value;
2933             }
2934             $iter = $model->iter_next( $iter );
2935             }
2936            
2937             return $total;
2938            
2939             }
2940              
2941             sub max_column {
2942            
2943             my ( $self, $column_no ) = @_;
2944            
2945             # This function returns the MAXIMUM value in a given column
2946            
2947             my $model = $self->{treeview}->get_model;
2948             my $iter = $model->get_iter_first;
2949             my $max = 0;
2950            
2951             while ( $iter ) {
2952            
2953             # Get the column value, strip out dollar signs and commas
2954             my $value = $model->get( $iter, $column_no );
2955            
2956             if ( exists $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency}
2957             && $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency} ) {
2958             $value =~ s/[\$\,]//g;
2959             }
2960            
2961             $max = $value > $max ? $value : $max;
2962            
2963             $iter = $model->iter_next( $iter );
2964            
2965             }
2966            
2967             return $max;
2968            
2969             }
2970              
2971             sub average_column {
2972            
2973             my ( $self, $column_no ) = @_;
2974            
2975             # This function returns the AVERAGE value in a given column
2976            
2977             my $model = $self->{treeview}->get_model;
2978             my $iter = $model->get_iter_first;
2979             my $total = 0;
2980             my $counter = 0;
2981            
2982             while ( $iter ) {
2983            
2984             # Get the column value, strip out dollar signs and commas
2985             my $value = $model->get( $iter, $column_no );
2986            
2987             if ( exists $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency}
2988             && $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency} ) {
2989             $value =~ s/[\$\,]//g;
2990             }
2991            
2992             $total += $value;
2993             $counter ++;
2994            
2995             $iter = $model->iter_next( $iter );
2996            
2997             }
2998            
2999             return $counter ? $total / $counter : undef;
3000            
3001             }
3002              
3003             sub count {
3004            
3005             # This function returns the number of all records ( optionally where $column_no matches $conditions )
3006            
3007             my ( $self, $column_no, $conditions ) = @_;
3008            
3009             my $model = $self->{treeview}->get_model;
3010             my $iter = $model->get_iter_first;
3011             my $count = 0;
3012            
3013             if ( $conditions ) {
3014             if ( ! ( exists $conditions->{column} && exists $conditions->{operator} && exists $conditions->{value} ) ) {
3015             warn "Gtk2::Ex::Datasheet::DBI->count() called with an incomplete conditions hash ..."
3016             . " ... must have 'column', 'operator' and 'value' keys to conditions hash!\n\n";
3017             return 0;
3018             }
3019             }
3020            
3021             while ( $iter ) {
3022            
3023             if ( $conditions ) {
3024            
3025             my $this_value = $model->get( $iter, $conditions->{column} );
3026            
3027             if ( $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency}
3028             && $self->{fields}[$column_no]->{treeview_column}->{definition}->{number}->{currency} ) {
3029             $this_value =~ s/[\$\,]//g;
3030             }
3031            
3032             if ( $conditions->{operator} eq "==" ) {
3033             if ( $this_value == $conditions->{value} ) {
3034             $count ++;
3035             }
3036             } elsif ( $conditions->{operator} eq "<" ) {
3037             if ( $this_value < $conditions->{value} ) {
3038             $count ++;
3039             }
3040             } elsif ( $conditions->{operator} eq ">" ) {
3041             if ( $this_value > $conditions->{value} ) {
3042             $count ++;
3043             }
3044             } elsif ( $conditions->{operator} eq "eq" ) {
3045             if ( $this_value eq $conditions->{value} ) {
3046             $count ++;
3047             }
3048             } else {
3049             warn "Gtk2::Ex::Datasheet::DBI->count() called with an invalid operator in the condition ...\n"
3050             . " ... operator: $conditions->{operator}\n\n";
3051             }
3052             } else {
3053             $count ++;
3054             }
3055             $iter = $model->iter_next( $iter );
3056             }
3057            
3058             return $count;
3059            
3060             }
3061              
3062             sub parse_sql_server_default {
3063            
3064             # This sub parses the string returned by SQL Server as the DEFAULT value for a given field
3065            
3066             my ( $self, $sqlserver_default ) = @_;
3067            
3068             # Find the last space in the string
3069             my $final_space_position = rindex( $sqlserver_default, " " );
3070            
3071             if ( ! $final_space_position || $final_space_position == -1 ) {
3072             # Bail out, returning undef.
3073             # We can't use the current default value ( as it's a string definition ), so we might as well just drop it completely
3074             warn "Gtk2::Ex::DBI::parse_sql_server_default failed to find the last space character in the DEFAULT definition:\n$sqlserver_default\n";
3075             return undef;
3076             } else {
3077             # We've got the final space character. Now get everything to the right of it ...
3078             my $default_value = substr( $sqlserver_default, $final_space_position + 1, length( $sqlserver_default ) - $final_space_position - 1 );
3079             # ... and strip off any quotes
3080             $default_value =~ s/'//g;
3081             return $default_value;
3082             }
3083            
3084             }
3085              
3086             sub calculator {
3087            
3088             # This pops up a simple addition-only calculator, and returns the calculated value to the calling widget
3089            
3090             my ( $self, $column_name ) = @_;
3091            
3092             my $dialog = Gtk2::Dialog->new(
3093             "Gtk2::Ex::DBI calculator",
3094             undef,
3095             "modal",
3096             "gtk-ok" => "ok",
3097             "gtk-cancel" => "reject"
3098             );
3099            
3100             $dialog->set_default_size( 300, 480 );
3101            
3102             # The model
3103             my $model = Gtk2::ListStore->new( "Glib::Double" );
3104            
3105             # Add an initial row data to the model
3106             my $iter = $model->append;
3107             $model->set( $iter, 0, 0 );
3108            
3109             # A renderer
3110             my $renderer = Gtk2::CellRendererText->new;
3111             $renderer->set(
3112             editable => TRUE,
3113             xalign => 1
3114             );
3115            
3116             # A column
3117             my $column = Gtk2::TreeViewColumn->new_with_attributes(
3118             "Values",
3119             $renderer,
3120             'text' => 0
3121             );
3122            
3123             # The TreeView
3124             my $treeview = Gtk2::TreeView->new( $model );
3125             $treeview->set_rules_hint( TRUE );
3126             $treeview->append_column($column);
3127            
3128             # A scrolled window to put the TreeView in
3129             my $sw = Gtk2::ScrolledWindow->new( undef, undef );
3130             $sw->set_shadow_type( "etched-in" );
3131             $sw->set_policy( "never", "always" );
3132            
3133             # Add treeview to scrolled window
3134             $sw->add( $treeview );
3135            
3136             # Add scrolled window to the dialog
3137             $dialog->vbox->pack_start( $sw, TRUE, TRUE, 0 );
3138            
3139             # Add a Gtk2::Entry to show the current total ...
3140             my $total_widget = Gtk2::Entry->new;
3141             $total_widget->set_alignment( 1 );
3142            
3143             # ... and a toggle button to strip GST
3144             my $gst_toggle = Gtk2::ToggleButton->new_with_label( "Strip GST" );
3145             $gst_toggle->signal_connect( toggled => sub {
3146            
3147             my ( $widget, $signal, $something ) = @_;
3148            
3149             # Add up all the items in the model
3150             my $iter = $model->get_iter_first;
3151             my $current_total;
3152            
3153             while ( $iter ) {
3154             $current_total += $model->get( $iter, 0 );
3155             $iter = $model->iter_next( $iter );
3156             }
3157            
3158             if ( $widget->get_active ) {
3159             $current_total = $current_total / 11 * 10;
3160             }
3161            
3162             # Allow for our number of decimal places
3163             $current_total *= 10 ** 2;
3164            
3165             # Round
3166             $current_total = int( $current_total + .5 * ( $current_total <=> 0 ) );
3167            
3168             # Get decimals back
3169             $current_total /= 10 ** 2;
3170            
3171             $total_widget->set_text( $current_total );
3172            
3173             } );
3174            
3175             my $total_hbox = Gtk2::HBox->new( 1, 5 );
3176             $total_hbox->pack_start( $gst_toggle, TRUE, TRUE, 0 );
3177             $total_hbox->pack_start( $total_widget, TRUE, TRUE, 0 );
3178            
3179             $dialog->vbox->pack_start( $total_hbox, FALSE, FALSE, 0 );
3180            
3181             # Handle editing in the renderer
3182             $renderer->signal_connect_after( edited => sub {
3183            
3184             #$self->calculator_process_editing( @_, $treeview, $model, $column, $total_widget );
3185            
3186             my ( $renderer, $text_path, $new_text ) = @_;
3187            
3188             my $path = Gtk2::TreePath->new_from_string ($text_path);
3189             my $iter = $model->get_iter ($path);
3190            
3191             # Only do something if we get a numeric value that isn't zero
3192             if ( $new_text !~ /\d/ || $new_text == 0 ) {
3193             return FALSE;
3194             }
3195            
3196             $model->set( $iter, 0, $new_text);
3197             my $new_iter = $model->append;
3198            
3199             $treeview->set_cursor(
3200             $model->get_path( $new_iter ),
3201             $column,
3202             TRUE
3203             );
3204            
3205             # Calculate total and display
3206             $iter = $model->get_iter_first;
3207             my $current_total;
3208            
3209             while ( $iter ) {
3210             $current_total += $model->get( $iter, 0 );
3211             $iter = $model->iter_next( $iter );
3212             }
3213            
3214             if ( $gst_toggle->get_active ) {
3215             $current_total = $current_total / 11 * 10;
3216             }
3217            
3218             # Allow for our number of decimal places
3219             $current_total *= 10 ** 2;
3220            
3221             # Round
3222             $current_total = int( $current_total + .5 * ( $current_total <=> 0 ) );
3223            
3224             # Get decimals back
3225             $current_total /= 10 ** 2;
3226            
3227             $total_widget->set_text( $current_total );
3228            
3229             } );
3230            
3231             # Show everything
3232             $dialog->show_all;
3233            
3234             # Start editing in the 1st row
3235             $treeview->set_cursor( $model->get_path( $iter ), $column, TRUE );
3236            
3237             my $response = $dialog->run;
3238            
3239             if ( $response eq "ok" ) {
3240             # Transfer value back to calling widget and exit
3241             $self->set_column_value( $self->column_name_to_sql_name( $column_name ), $total_widget->get_text );
3242             $dialog->destroy;
3243             } else {
3244             $dialog->destroy;
3245             }
3246            
3247             }
3248              
3249             1;
3250              
3251             #######################################################################################
3252             # That's the end of Gtk2::Ex::Datasheet::DBI
3253             # What follows is stuff I've plucked from around the place
3254             #######################################################################################
3255              
3256              
3257              
3258              
3259              
3260              
3261             #######################################################################################
3262             # Custom CellRendererText
3263             #######################################################################################
3264              
3265             package Gtk2::Ex::Datasheet::DBI::CellEditableText;
3266              
3267             # Copied and pasted from Odot
3268              
3269             use strict;
3270             use warnings;
3271              
3272             use Glib qw(TRUE FALSE);
3273             use Glib::Object::Subclass
3274             Gtk2::TextView::,
3275             interfaces => [ Gtk2::CellEditable:: ];
3276              
3277             sub set_text {
3278            
3279             my ( $editable, $text ) = @_;
3280            
3281             $text = "" unless ( defined( $text ) );
3282            
3283             $editable->get_buffer()->set_text( $text );
3284            
3285             }
3286              
3287             sub get_text {
3288            
3289             my ( $editable ) = @_;
3290             my $buffer = $editable->get_buffer();
3291            
3292             return $buffer->get_text( $buffer->get_bounds(), TRUE );
3293            
3294             }
3295              
3296             sub select_all {
3297            
3298             my ( $editable ) = @_;
3299             my $buffer = $editable->get_buffer();
3300            
3301             my ( $start, $end ) = $buffer->get_bounds();
3302             $buffer->move_mark_by_name( insert => $start );
3303             $buffer->move_mark_by_name( selection_bound => $end );
3304            
3305             }
3306              
3307             1;
3308              
3309             package Gtk2::Ex::Datasheet::DBI::CellRendererText;
3310              
3311             # Originally from Odot, with bits and pieces from the CellRendererSpinButton example,
3312             # and even some of my own stuff worked in :)
3313              
3314             use constant x_padding => 2;
3315             use constant y_padding => 3;
3316              
3317             use strict;
3318             use warnings;
3319              
3320             use Gtk2::Gdk::Keysyms;
3321             use Glib qw(TRUE FALSE);
3322              
3323             use Glib::Object::Subclass
3324             Gtk2::CellRendererText::,
3325             properties => [
3326             Glib::ParamSpec->object(
3327             "editable-widget",
3328             "Editable widget",
3329             "The editable that's used for cell editing.",
3330             Gtk2::Ex::Datasheet::DBI::CellEditableText::,
3331             [ qw( readable writable ) ]
3332             ),
3333             Glib::ParamSpec->boolean(
3334             "number",
3335             "Number",
3336             "Should I apply number formatting to the data?",
3337             0,
3338             [ qw( readable writable ) ]
3339             ),
3340             Glib::ParamSpec->string(
3341             "decimals",
3342             "Decimals",
3343             "How many decimal places should be displayed?",
3344             -1,
3345             [ qw( readable writable ) ]
3346             ),
3347             Glib::ParamSpec->boolean(
3348             "currency",
3349             "Currency",
3350             "Should I prepend a dollar sign to the data?",
3351             0,
3352             [ qw( readable writable ) ]
3353             )
3354             ];
3355              
3356             sub INIT_INSTANCE {
3357            
3358             my ( $cell ) = @_;
3359            
3360             my $editable = Gtk2::Ex::Datasheet::DBI::CellEditableText->new();
3361            
3362             $editable->set( border_width => $cell->get("ypad") );
3363            
3364             $editable->signal_connect( key_press_event => sub {
3365            
3366             my ( $editable, $event ) = @_;
3367            
3368             if (
3369             $event -> keyval == $Gtk2::Gdk::Keysyms{ Return } ||
3370             $event -> keyval == $Gtk2::Gdk::Keysyms{ KP_Enter }
3371             and not $event -> state & qw(control-mask)
3372             )
3373             {
3374            
3375             # Grab parent
3376             my $parent = $editable->get_parent;
3377            
3378             $editable->{ _editing_canceled } = FALSE;
3379             $editable->editing_done();
3380             $editable->remove_widget();
3381            
3382             my ( $path, $focus_column ) = $parent->get_cursor;
3383             my @cols = $parent->get_columns;
3384             my $next_col = undef;
3385            
3386             foreach my $i (0..$#cols) {
3387             if ( $cols[$i] == $focus_column ) {
3388             if ( $event->state >= 'shift-mask' ) {
3389             # go backwards
3390             $next_col = $cols[$i-1] if $i > 0;
3391             } else {
3392             # Go forwards
3393             # First check whether the next column is read_only
3394             while ( $i-1 < $#cols ) {
3395             $i++;
3396             if ( ! $cols[$i]->{definition}->{read_only} ) {
3397             last;
3398             }
3399             }
3400             $next_col = $cols[$i];
3401             }
3402             last;
3403             }
3404             }
3405            
3406             # For some reason, the last item returned by the above call to
3407             # $parent->get_columns isn't a Gtk2::TreeViewColumn, and therefore
3408             # the $parent_set_cursor line fails. Avoid this.
3409             if ( ref $next_col eq 'Gtk2::TreeViewColumn' ) {
3410             $parent->set_cursor ( $path, $next_col, 1 )
3411             if $next_col;
3412             }
3413            
3414             return TRUE;
3415            
3416             }
3417            
3418             return FALSE;
3419            
3420             });
3421            
3422             $editable->signal_connect( editing_done => sub {
3423            
3424             my ( $editable ) = @_;
3425            
3426             # gtk+ changed semantics in 2.6. you now need to call stop_editing().
3427             if ( Gtk2->CHECK_VERSION( 2, 6, 0 ) ) {
3428             $cell->stop_editing( $editable->{ _editing_canceled } );
3429             }
3430            
3431             # if gtk+ < 2.4.0, emit the signal regardless of whether editing was
3432             # canceled to make undo/redo work.
3433            
3434             my $new = Gtk2->CHECK_VERSION( 2, 4, 0 );
3435            
3436             if ( ! $new || ( $new && ! $editable->{ _editing_canceled } ) ) {
3437             $cell->signal_emit( edited => $editable->{ _path }, $editable -> get_text() );
3438             } else {
3439             $cell->editing_canceled();
3440             }
3441             });
3442            
3443             $cell->set( editable_widget => $editable );
3444            
3445             }
3446              
3447             sub START_EDITING {
3448            
3449             my ( $cell, $event, $view, $path, $background_area, $cell_area, $flags ) = @_;
3450            
3451             if ( $event ) {
3452             return unless ( $event->button == 1 );
3453             }
3454            
3455             my $editable = $cell->get( "editable-widget" );
3456            
3457             $editable->modify_font( Gtk2::Pango::FontDescription->from_string( "Arial " . $cell->get( "font" ) ) );
3458            
3459             $editable->{ _editing_canceled } = FALSE;
3460             $editable->{ _path } = $path;
3461             $editable->set( height_request => $cell_area->height );
3462            
3463             $editable->set_text( $cell->get( "text" ) );
3464             $editable->select_all();
3465             $editable->show();
3466            
3467             $editable -> signal_connect_after(
3468             'focus-out-event' => sub {
3469             my ( $event_box, $event ) = @_;
3470             $cell->signal_emit( edited => $editable->{ _path }, $editable->get_text );
3471             return $event;
3472             }
3473             );
3474            
3475             return $editable;
3476            
3477             }
3478              
3479             sub get_layout {
3480            
3481             my ( $cell, $widget ) = @_;
3482            
3483             return $widget -> create_pango_layout("");
3484            
3485             }
3486              
3487             1;
3488              
3489              
3490             #######################################################################################
3491             # CellRendererDate
3492             #######################################################################################
3493              
3494             # Copyright (C) 2003 by Torsten Schoenfeld
3495             #
3496             # This library is free software; you can redistribute it and/or modify it under
3497             # the terms of the GNU Library General Public License as published by the Free
3498             # Software Foundation; either version 2.1 of the License, or (at your option)
3499             # any later version.
3500             #
3501             # This library is distributed in the hope that it will be useful, but WITHOUT
3502             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3503             # FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
3504             # more details.
3505             #
3506             # You should have received a copy of the GNU Library General Public License
3507             # along with this library; if not, write to the Free Software Foundation, Inc.,
3508             # 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA.
3509             #
3510             # $Header: /cvsroot/gtk2-perl/gtk2-perl-xs/Gtk2/examples/cellrenderer_date.pl,v 1.5 2005/01/07 21:31:59 kaffeetisch Exp $
3511             #
3512              
3513              
3514             use strict;
3515             use Gtk2 -init;
3516              
3517             package Gtk2::Ex::Datasheet::DBI::CellRendererDate;
3518              
3519             use Glib::Object::Subclass
3520             "Gtk2::CellRenderer",
3521             signals => {
3522             edited => {
3523             flags => [qw(run-last)],
3524             param_types => [qw(Glib::String Glib::Scalar)],
3525             },
3526             },
3527             properties => [
3528             Glib::ParamSpec->boolean(
3529             "editable",
3530             "Editable",
3531             "Can I change that?",
3532             0,
3533             [qw(readable writable)]
3534             ),
3535             Glib::ParamSpec->string(
3536             "date",
3537             "Date",
3538             "What's the date again?",
3539             "",
3540             [qw(readable writable)]
3541             ),
3542             Glib::ParamSpec->string(
3543             "format",
3544             "Format",
3545             "What day-month-year formatting?",
3546             "yyyy-mm-dd",
3547             [qw(readable writable)]
3548             ),
3549             Glib::ParamSpec->string(
3550             "font",
3551             "Font",
3552             "What size fonts should be used?",
3553             12,
3554             [qw(readable writable)]
3555             )
3556             ];
3557              
3558             use constant x_padding => 2;
3559             use constant y_padding => 3;
3560              
3561             use constant arrow_width => 15;
3562             use constant arrow_height => 15;
3563              
3564             sub hide_popup {
3565            
3566             my ( $cell ) = @_;
3567            
3568             Gtk2->grab_remove( $cell->{ _popup } );
3569             $cell->{ _popup }->hide();
3570            
3571             }
3572              
3573             sub get_today {
3574            
3575             my ( $cell ) = @_;
3576            
3577             my ( $day, $month, $year ) = (localtime())[3, 4, 5];
3578            
3579             $year += 1900;
3580             $month += 1;
3581            
3582             return ( $year, $month, $day );
3583            
3584             }
3585              
3586             sub get_date {
3587            
3588             my ( $cell ) = @_;
3589            
3590             my $text = $cell->get("date");
3591            
3592             my ( $year, $month, $day ) = $text
3593             ? split(/[\/-]/, $text)
3594             : $cell->get_today();
3595            
3596             return ( $year, $month, $day );
3597            
3598             }
3599              
3600             sub add_padding {
3601            
3602             my ( $cell, $year, $month, $day ) = @_;
3603             return ( $year, sprintf("%02d", $month), sprintf("%02d", $day) );
3604            
3605             }
3606              
3607             sub INIT_INSTANCE {
3608            
3609             my ( $cell ) = @_;
3610            
3611             my $popup = Gtk2::Window->new ('popup');
3612             my $vbox = Gtk2::VBox->new( 0, 0 );
3613            
3614             my $calendar = Gtk2::Calendar->new();
3615            
3616             $calendar->modify_font( Gtk2::Pango::FontDescription->from_string( "Arial " . $cell->get( "font" ) ) );
3617            
3618             my $hbox = Gtk2::HBox->new( 0, 0 );
3619            
3620             my $today = Gtk2::Button->new('Today');
3621             my $none = Gtk2::Button->new('None');
3622            
3623             $cell -> {_arrow} = Gtk2::Arrow->new( "down", "none" );
3624            
3625             # We can't just provide the callbacks now because they might need access to
3626             # cell-specific variables. And we can't just connect the signals in
3627             # START_EDITING because we'd be connecting many signal handlers to the same
3628             # widgets.
3629             $today->signal_connect( clicked => sub {
3630             $cell->{ _today_clicked_callback }->( @_ )
3631             if ( exists( $cell->{ _today_clicked_callback } ) );
3632             } );
3633            
3634             $none->signal_connect( clicked => sub {
3635             $cell->{ _none_clicked_callback }->( @_ )
3636             if ( exists( $cell->{ _none_clicked_callback } ) );
3637             } );
3638            
3639             $calendar->signal_connect( day_selected_double_click => sub {
3640             $cell->{ _day_selected_double_click_callback }->( @_ )
3641             if ( exists( $cell->{ _day_selected_double_click_callback } ) );
3642             } );
3643            
3644             $calendar->signal_connect( month_changed => sub {
3645             $cell->{ _month_changed }->( @_ )
3646             if ( exists( $cell->{ _month_changed } ) );
3647             } );
3648            
3649             $hbox->pack_start( $today, 1, 1, 0 );
3650             $hbox->pack_start( $none, 1, 1, 0 );
3651            
3652             $vbox->pack_start( $calendar, 1, 1, 0 );
3653             $vbox->pack_start( $hbox, 0, 0, 0 );
3654            
3655             # Find out if the click happended outside of our window. If so, hide it.
3656             # Largely copied from Planner (the former MrProject).
3657            
3658             # Implement via Gtk2::get_event_widget?
3659             $popup->signal_connect( button_press_event => sub {
3660             my ( $popup, $event ) = @_;
3661            
3662             if ( $event->button() == 1 ) {
3663             my ( $x, $y ) = ( $event->x_root(), $event->y_root() );
3664             my ( $xoffset, $yoffset ) = $popup->window()->get_root_origin();
3665            
3666             my $allocation = $popup->allocation();
3667            
3668             my $x1 = $xoffset + 2 * $allocation->x();
3669             my $y1 = $yoffset + 2 * $allocation->y();
3670             my $x2 = $x1 + $allocation->width();
3671             my $y2 = $y1 + $allocation->height();
3672            
3673             unless ( $x > $x1 && $x < $x2 && $y > $y1 && $y < $y2 ) {
3674             $cell->hide_popup();
3675             return 1;
3676             }
3677             }
3678            
3679             return 0;
3680             } );
3681            
3682             $popup->add( $vbox );
3683            
3684             $cell->{ _popup } = $popup;
3685             $cell->{ _calendar } = $calendar;
3686             }
3687              
3688             sub START_EDITING {
3689            
3690             my ( $cell, $event, $view, $path, $background_area, $cell_area, $flags ) = @_;
3691            
3692             my $popup = $cell -> { _popup };
3693             my $calendar = $cell->{ _calendar };
3694            
3695             # Specify the callbacks. Will be called by the signal handlers set up in
3696             # INIT_INSTANCE.
3697             $cell->{ _today_clicked_callback } = sub {
3698            
3699             my ($button) = @_;
3700             my ($year, $month, $day) = $cell -> get_today();
3701            
3702             $cell->signal_emit( edited=>$path, join( "-", $cell->add_padding( $year, $month, $day ) ) );
3703             $cell->hide_popup();
3704            
3705             };
3706            
3707             $cell->{ _none_clicked_callback } = sub {
3708            
3709             my ( $button ) = @_;
3710            
3711             $cell->signal_emit( edited=>$path, "" );
3712             $cell->hide_popup();
3713            
3714             };
3715            
3716             $cell->{ _day_selected_double_click_callback } = sub {
3717            
3718             my ( $calendar ) = @_;
3719             my ( $year, $month, $day ) = $calendar->get_date();
3720            
3721             $cell->signal_emit( edited => $path, join( "-", $cell -> add_padding( $year, ++$month, $day ) ) );
3722             $cell->hide_popup();
3723            
3724             };
3725            
3726             $cell->{ _month_changed } = sub {
3727            
3728             my ( $calendar ) = @_;
3729            
3730             my ( $selected_year, $selected_month ) = $calendar->get_date();
3731             my ( $current_year, $current_month, $current_day ) = $cell->get_today();
3732            
3733             if ( $selected_year == $current_year && ++$selected_month == $current_month ) {
3734             $calendar->mark_day( $current_day );
3735             }
3736             else {
3737             $calendar->unmark_day( $current_day );
3738             }
3739            
3740             };
3741            
3742             my ( $year, $month, $day ) = $cell->get_date();
3743            
3744             $calendar->select_month( $month - 1, $year );
3745             $calendar->select_day( $day );
3746            
3747             # Necessary to get the correct allocation of the popup.
3748             $popup->move( -500, -500 );
3749             $popup->show_all();
3750            
3751             # Figure out where to put the popup - ie don't put it offscreen,
3752             # as it's not movable ( by the user )
3753             my $screen_height = $popup->get_screen->get_height;
3754             my $requisition = $popup->size_request();
3755             my $popup_width = $requisition->width;
3756             my $popup_height = $requisition->height;
3757             my ( $x_origin, $y_origin ) = $view->get_bin_window()->get_origin();
3758             my ( $popup_x, $popup_y );
3759            
3760             $popup_x = $x_origin + $cell_area->x() + $cell_area->width() - $popup_width;
3761             $popup_x = 0 if $popup_x < 0;
3762            
3763             $popup_y = $y_origin + $cell_area->y() + $cell_area->height();
3764            
3765             if ( $popup_y + $popup_height > $screen_height ) {
3766             $popup_y = $y_origin + $cell_area->y() - $popup_height;
3767             }
3768            
3769             $popup->move( $popup_x, $popup_y );
3770            
3771             # Grab the focus and the pointer.
3772             Gtk2->grab_add( $popup );
3773             $popup->grab_focus();
3774            
3775             Gtk2::Gdk -> pointer_grab(
3776             $popup -> window(),
3777             1,
3778             [ qw( button-press-mask button-release-mask pointer-motion-mask ) ],
3779             undef,
3780             undef,
3781             0
3782             );
3783            
3784             return;
3785            
3786             }
3787              
3788             sub get_date_string {
3789            
3790             my ( $cell ) = @_;
3791            
3792             return $cell->get('date');
3793            
3794             }
3795              
3796             sub calc_size {
3797            
3798             my ( $cell, $layout ) = @_;
3799            
3800             my ( $width, $height ) = $layout -> get_pixel_size();
3801            
3802             return (
3803             0,
3804             0,
3805             $width + x_padding * 2 + arrow_width,
3806             $height + y_padding * 2
3807             );
3808            
3809             }
3810              
3811             sub GET_SIZE {
3812            
3813             my ( $cell, $widget, $cell_area ) = @_;
3814            
3815             my $layout = $cell->get_layout( $widget );
3816             $layout->set_text( $cell->get_date_string() || '' );
3817            
3818             return $cell->calc_size( $layout );
3819            
3820             }
3821              
3822             sub get_layout {
3823            
3824             my ( $cell, $widget ) = @_;
3825            
3826             return $widget->create_pango_layout("");
3827            
3828             }
3829              
3830             sub RENDER {
3831            
3832             my ( $cell, $window, $widget, $background_area, $cell_area, $expose_area, $flags ) = @_;
3833            
3834             my $state;
3835            
3836             if ( $flags & 'selected' ) {
3837             $state = $widget->has_focus()
3838             ? 'selected'
3839             : 'active';
3840             } else {
3841             $state = $widget->state() eq 'insensitive'
3842             ? 'insensitive'
3843             : 'normal';
3844             }
3845            
3846             my $layout = $cell->get_layout( $widget );
3847            
3848             my $datestring = $cell->get_date_string() || '';
3849            
3850             if ( $datestring eq '0000-00-00' ) {
3851             $datestring = '';
3852             }
3853            
3854             if ( $cell->get('format') eq "dd-mm-yyyy" && $datestring ne '' ) {
3855             my ( $yyyy, $mm, $dd ) = split /-/, $datestring;
3856             $datestring = $dd . "-" . $mm . "-" . $yyyy;
3857             } elsif ( $cell->get('format') eq "dd-mm-yy" && $datestring ne '' ) {
3858             my ( $yyyy, $mm, $dd ) = split /-/, $datestring;
3859             $datestring = $dd . "-" . $mm . "-" . substr( $yyyy, 2, 2 );
3860             }
3861            
3862             $layout->set_font_description( Gtk2::Pango::FontDescription->from_string( "Arial " . $cell->get( "font" ) ) );
3863             $layout->set_text( $datestring );
3864            
3865             my ( $x_offset, $y_offset, $width, $height ) = $cell->calc_size( $layout );
3866            
3867             $widget->get_style->paint_layout(
3868             $window,
3869             $state,
3870             1,
3871             $cell_area,
3872             $widget,
3873             "cellrenderertext",
3874             $cell_area->x() + $x_offset + x_padding,
3875             $cell_area->y() + $y_offset + y_padding,
3876             $layout
3877             );
3878            
3879             $widget->get_style->paint_arrow (
3880             $window,
3881             $widget->state,
3882             'none',
3883             $cell_area,
3884             $cell->{ _arrow },
3885             "",
3886             "down",
3887             1,
3888             $cell_area->x + $cell_area->width - arrow_width,
3889             $cell_area->y + $cell_area->height - arrow_height + 3, #
3890             arrow_width - 3,
3891             arrow_height
3892             );
3893             }
3894              
3895             1;
3896              
3897             #######################################################################################
3898             # CellRendererTime
3899             #######################################################################################
3900              
3901             # Copyright (C) 2005 by Daniel Kasak ...
3902             # ... basically a slightly modified CellRendererDate ( see above )
3903             #
3904             # This library is free software; you can redistribute it and/or modify it under
3905             # the terms of the GNU Library General Public License as published by the Free
3906             # Software Foundation; either version 2.1 of the License, or (at your option)
3907             # any later version.
3908             #
3909             # This library is distributed in the hope that it will be useful, but WITHOUT
3910             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
3911             # FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
3912             # more details.
3913             #
3914             # You should have received a copy of the GNU Library General Public License
3915             # along with this library; if not, write to the Free Software Foundation, Inc.,
3916             # 59 Temple Place - Suite 330, Boston, MA 02111-1307 USA.
3917              
3918             use strict;
3919             use Gtk2 -init;
3920              
3921             package Gtk2::Ex::Datasheet::DBI::CellRendererTime;
3922              
3923             use Glib::Object::Subclass
3924             "Gtk2::CellRenderer",
3925             signals => {
3926             edited => {
3927             flags => [qw(run-last)],
3928             param_types => [qw(Glib::String Glib::Scalar)],
3929             },
3930             },
3931             properties => [
3932             Glib::ParamSpec -> boolean("editable", "Editable", "Can I change that?", 0, [qw(readable writable)]),
3933             Glib::ParamSpec -> string("time", "Time", "What's the time again?", "", [qw(readable writable)]),
3934             ]
3935             ;
3936              
3937             use constant x_padding => 2;
3938             use constant y_padding => 3;
3939              
3940             use constant arrow_width => 15;
3941             use constant arrow_height => 15;
3942              
3943             sub hide_popup {
3944            
3945             my $cell = shift;
3946            
3947             Gtk2->grab_remove( $cell->{ _popup } );
3948             $cell->{ _popup }->hide();
3949            
3950             }
3951              
3952             sub get_time {
3953            
3954             my $cell = shift;
3955            
3956             my $text = $cell->get("time");
3957             my ( $h, $m, $s ) = split( /:/, $text );
3958            
3959             return ( $h, $m, $s );
3960            
3961             }
3962              
3963             sub add_padding {
3964            
3965             my ( $cell, $h, $m, $s ) = @_;
3966            
3967             return ( sprintf("%02d",$h), sprintf("%02d", $m), sprintf("%02d", $s) );
3968            
3969             }
3970              
3971             sub INIT_INSTANCE {
3972            
3973             my $cell = shift;
3974            
3975             my $popup = Gtk2::Window -> new ('popup');
3976             my $vbox = Gtk2::VBox -> new( 0, 0 );
3977            
3978             my $h_spinbutton = Gtk2::SpinButton -> new_with_range( 0, 23, 1 );
3979             my $m_spinbutton = Gtk2::SpinButton -> new_with_range( 0, 59, 1 );
3980             my $s_spinbutton = Gtk2::SpinButton -> new_with_range( 0, 59, 1 );
3981             my $colon_1 = Gtk2::Label->new(":");
3982             my $colon_2 = Gtk2::Label->new(":");
3983            
3984             my $spin_hbox = Gtk2::HBox -> new( 0, 0 );
3985             my $buttons_hbox = Gtk2::HBox->new( 0, 0 );
3986            
3987             $cell -> {_arrow} = Gtk2::Arrow -> new("down", "none");
3988            
3989             $spin_hbox->pack_start( $h_spinbutton, 1, 1, 0 );
3990             $spin_hbox->pack_start( $colon_1, 1, 1, 0 );
3991             $spin_hbox->pack_start( $m_spinbutton, 1, 1, 0 );
3992             $spin_hbox->pack_start( $colon_2, 1, 1, 0 );
3993             $spin_hbox->pack_start( $s_spinbutton, 1, 1, 0 );
3994            
3995             my $ok = Gtk2::Button->new_from_stock('gtk-ok');
3996            
3997             $buttons_hbox->pack_start( $ok, 1, 1, 0 );
3998            
3999             $vbox -> pack_start( $spin_hbox, 0, 0, 0 );
4000             $vbox->pack_start( $buttons_hbox, 0, 0, 0 );
4001            
4002             # We can't just provide the callbacks now because they might need access to
4003             # cell-specific variables. And we can't just connect the signals in
4004             # START_EDITING because we'd be connecting many signal handlers to the same
4005             # widgets.
4006            
4007             $ok -> signal_connect(
4008             clicked => sub {
4009             $cell -> { _ok_clicked_callback } -> (@_)
4010             if ( exists( $cell -> { _ok_clicked_callback } ) );
4011             }
4012             );
4013            
4014             # Find out if the click happended outside of our window. If so, hide it.
4015             # Largely copied from Planner (the former MrProject).
4016            
4017             # Implement via Gtk2::get_event_widget?
4018             $popup -> signal_connect(button_press_event => sub {
4019             my ($popup, $event) = @_;
4020            
4021             if ($event -> button() == 1) {
4022             my ($x, $y) = ($event -> x_root(), $event -> y_root());
4023             my ($xoffset, $yoffset) = $popup -> window() -> get_root_origin();
4024            
4025             my $allocation = $popup -> allocation();
4026            
4027             my $x1 = $xoffset + 2 * $allocation -> x();
4028             my $y1 = $yoffset + 2 * $allocation -> y();
4029             my $x2 = $x1 + $allocation -> width();
4030             my $y2 = $y1 + $allocation -> height();
4031            
4032             unless ($x > $x1 && $x < $x2 && $y > $y1 && $y < $y2) {
4033             $cell -> hide_popup();
4034             return 1;
4035             }
4036             }
4037            
4038             return 0;
4039             });
4040            
4041             $popup -> add($vbox);
4042            
4043             $cell -> { _popup } = $popup;
4044             $cell -> { _h_spinbutton } = $h_spinbutton;
4045             $cell -> { _m_spinbutton } = $m_spinbutton;
4046             $cell -> { _s_spinbutton } = $s_spinbutton;
4047            
4048             }
4049              
4050             sub START_EDITING {
4051            
4052             my ( $cell, $event, $view, $path, $background_area, $cell_area, $flags ) = @_;
4053            
4054             my $popup = $cell -> { _popup };
4055             my $h_spinbutton = $cell -> { _h_spinbutton };
4056             my $m_spinbutton = $cell -> { _m_spinbutton };
4057             my $s_spinbutton = $cell -> { _s_spinbutton };
4058            
4059             my ( $h, $m, $s ) = $cell -> get_time();
4060            
4061             $h_spinbutton->set_text( $h );
4062             $m_spinbutton->set_text( $m );
4063             $s_spinbutton->set_text( $s );
4064            
4065             $cell -> { _ok_clicked_callback } = sub {
4066            
4067             my ( $button ) = @_;
4068             my $h = $h_spinbutton->get_text;
4069             my $m = $m_spinbutton->get_text;
4070             my $s = $s_spinbutton->get_text;
4071            
4072             $cell -> signal_emit(
4073             edited => $path,
4074             join( ":", $cell -> add_padding($h, $m, $s ) )
4075             );
4076             $cell -> hide_popup();
4077             };
4078            
4079             # Necessary to get the correct allocation of the popup.
4080             $popup -> move(-500, -500);
4081             $popup -> show_all();
4082            
4083             # Figure out where to put the popup - ie don't put it offscreen,
4084             # as it's not movable ( by the user )
4085             my $screen_height = $popup->get_screen->get_height;
4086             my $requisition = $popup->size_request();
4087             my $popup_width = $requisition->width;
4088             my $popup_height = $requisition->height;
4089             my ( $x_origin, $y_origin ) = $view -> get_bin_window() -> get_origin();
4090             my ( $popup_x, $popup_y );
4091            
4092             $popup_x = $x_origin + $cell_area->x() + $cell_area->width() - $popup_width;
4093             $popup_x = 0 if $popup_x < 0;
4094            
4095             $popup_y = $y_origin + $cell_area -> y() + $cell_area -> height();
4096            
4097             if ( $popup_y + $popup_height > $screen_height ) {
4098             $popup_y = $y_origin + $cell_area -> y() - $popup_height;
4099             }
4100            
4101             $popup -> move( $popup_x, $popup_y );
4102            
4103             # Grab the focus and the pointer.
4104             Gtk2 -> grab_add($popup);
4105             $popup -> grab_focus();
4106            
4107             Gtk2::Gdk->pointer_grab(
4108             $popup->window(),
4109             1,
4110             [ qw( button-press-mask button-release-mask pointer-motion-mask ) ],
4111             undef,
4112             undef,
4113             0
4114             );
4115            
4116             return;
4117            
4118             }
4119              
4120             sub get_time_string {
4121            
4122             my $cell = shift;
4123             return $cell->get('time');
4124            
4125             }
4126              
4127             sub calc_size {
4128            
4129             my ( $cell, $layout ) = @_;
4130             my ( $width, $height ) = $layout->get_pixel_size();
4131            
4132             return (
4133             0,
4134             0,
4135             $width + x_padding * 2 + arrow_width,
4136             $height + y_padding * 2
4137             );
4138            
4139             }
4140              
4141             sub GET_SIZE {
4142            
4143             my ( $cell, $widget, $cell_area ) = @_;
4144            
4145             my $layout = $cell->get_layout( $widget );
4146             $layout->set_text( $cell -> get_time_string() );
4147            
4148             return $cell->calc_size( $layout );
4149            
4150             }
4151              
4152             sub get_layout {
4153            
4154             my ( $cell, $widget ) = @_;
4155            
4156             return $widget->create_pango_layout( "" );
4157            
4158             }
4159              
4160             sub RENDER {
4161            
4162             my ( $cell, $window, $widget, $background_area, $cell_area, $expose_area, $flags ) = @_;
4163            
4164             my $state;
4165            
4166             if ( $flags & 'selected' ) {
4167            
4168             $state = $widget->has_focus()
4169             ? 'selected'
4170             : 'active';
4171            
4172             } else {
4173            
4174             $state = $widget->state() eq 'insensitive'
4175             ? 'insensitive'
4176             : 'normal';
4177            
4178             }
4179            
4180             my $layout = $cell->get_layout( $widget );
4181             $layout->set_text( $cell->get_time_string() );
4182            
4183             my ( $x_offset, $y_offset, $width, $height ) = $cell->calc_size( $layout );
4184            
4185             $widget->get_style->paint_layout(
4186             $window,
4187             $state,
4188             1,
4189             $cell_area,
4190             $widget,
4191             "cellrenderertext",
4192             $cell_area->x() + $x_offset + x_padding,
4193             $cell_area->y() + $y_offset + y_padding,
4194             $layout
4195             );
4196            
4197             $widget->get_style->paint_arrow (
4198             $window,
4199             $widget->state,
4200             'none',
4201             $cell_area,
4202             $cell->{ _arrow },
4203             "",
4204             "down",
4205             1,
4206             $cell_area->x + $cell_area->width - arrow_width,
4207             $cell_area->y + $cell_area->height - arrow_height - 2,
4208             arrow_width - 3,
4209             arrow_height
4210             );
4211             }
4212              
4213             1;
4214              
4215             #######################################################################################
4216              
4217              
4218             =head1 NAME
4219              
4220             Gtk2::Ex::Datasheet::DBI
4221              
4222             =head1 SYNOPSIS
4223              
4224             use DBI;
4225              
4226             use Gtk2 -init;
4227              
4228             use Gtk2::Ex::Datasheet::DBI;
4229              
4230             my $dbh = DBI->connect (
4231             "dbi:mysql:dbname=sales;host=screamer;port=3306",
4232             "some_username",
4233             "salespass",
4234             {
4235             PrintError => 0,
4236             RaiseError => 0,
4237             AutoCommit => 1
4238             }
4239             );
4240              
4241             my $datasheet_def = {
4242             dbh => $dbh,
4243             sql => {
4244             select => "FirstName, LastName, GroupNo, Active",
4245             from => "BirdsOfAFeather",
4246             order_by => "LastName"
4247             },
4248             treeview => $testwindow->get_widget( "BirdsOfAFeather_TreeView" ),
4249             fields => [
4250             {
4251             name => "First Name",
4252             x_percent => 35,
4253             validation => sub { &validate_first_name(@_); }
4254             },
4255             {
4256             name => "Last Name",
4257             x_percent => 35
4258             },
4259             {
4260             name => "Group",
4261             x_percent => 30,
4262             renderer => "combo",
4263             model_setup => {
4264             fields => [
4265             {
4266             name => "ID",
4267             type => "Glib::Int"
4268             },
4269             {
4270             name => "GroupName",
4271             type => "Glib::String"
4272             }
4273             ],
4274             sql => {
4275             from => "Groups",
4276             where_object => {
4277             where => "Active = 1 and Location = ?",
4278             bind_values => [ $some_location_id ]
4279             }
4280            
4281             }
4282             }
4283             },
4284             {
4285             name => "Active",
4286             x_absolute => 50,
4287             renderer => "toggle"
4288             }
4289             ],
4290             multi_select => TRUE
4291             };
4292            
4293             $birds_of_a_feather_datasheet = Gtk2::Ex::Datasheet::DBI->new( $datasheet_def )
4294             || die ("Error setting up Gtk2::Ex::Datasheet::DBI\n");
4295              
4296             =head1 DESCRIPTION
4297              
4298             This module automates the process of setting up a model and treeview based on field definitions you pass it,
4299             querying the database, populating the model, and updating the database with changes made by the user.
4300              
4301             Steps for use:
4302              
4303             * Open a DBI connection
4304              
4305             * Create a 'bare' Gtk2::TreeView - I use Gtk2::GladeXML, but I assume you can do it the old-fashioned way
4306              
4307             * Create a Gtk2::Ex::Datasheet::DBI object and pass it your TreeView object
4308              
4309             You would then typically create some buttons and connect them to the methods below to handle common actions
4310             such as inserting, deleting, etc.
4311              
4312             =head1 METHODS
4313              
4314             =head2 new
4315              
4316             =over 4
4317              
4318             Object constructor. For more info, see section on CONSTRUCTION below.
4319              
4320             =back
4321              
4322             =head2 query ( [ where_object ], [ dont_apply ] )
4323              
4324             =over 4
4325              
4326             Requeries the Database Server. If there are any outstanding changes that haven't been applied to the database,
4327             a dialog will be presented to the user asking if they want to apply updates before requerying.
4328              
4329             If a where object is passed, the relevent parts will be replaced ( the where clause and the
4330             bind_values ). Note that you don't have to provide both. For example, if you leave out the where
4331             clause and only supply bind_values, the original where clause will continue to be used.
4332              
4333             If dont_apply is set, *no* dialog will appear if there are outstanding changes to the data.
4334              
4335             The where_object is a hash:
4336              
4337             {
4338             where => a where clause - can include placeholders
4339             bind_values => an array of values to bind to placeholders ( optional )
4340             }
4341              
4342             The query method doubles as an 'undo' method if you set the dont_apply flag, eg:
4343              
4344             $datasheet->query ( undef, TRUE );
4345              
4346             This will requery and reset all the status indicators. See also undo method, below
4347              
4348             =back
4349              
4350             =head2 undo
4351              
4352             =over 4
4353              
4354             Basically a convenience function that calls $self->query( undef, TRUE ) ... see above.
4355             I've come to realise that having an undo method makes understanding your code a lot easier later.
4356              
4357             =back
4358              
4359             =head2 apply
4360              
4361             =over 4
4362              
4363             Applies all changes ( inserts, deletes, alterations ) in the datasheet to the database.
4364             As changes are applied, the record status indicator will be changed back to the original 'synchronised' icon.
4365              
4366             If any errors are encountered, a dialog will be presented with details of the error, and the apply method
4367             will return FALSE without continuing through the records. The user will be able to tell where the apply failed
4368             by looking at the record status indicators ( and considering the error message they were presented ).
4369              
4370             =back
4371              
4372             =head2 insert ( [ @columns_and_values ] )
4373              
4374             =over 4
4375              
4376             Inserts a new row in the *model*. The record status indicator will display an 'insert' icon until the record
4377             is applied to the database ( apply method ).
4378              
4379             You can optionally set default values by passing them as an array of column numbers and values, eg:
4380             $datasheet->insert(
4381             2 => "Default value for column 2",
4382             5 => "Another default - for column 5"
4383             );
4384              
4385             Note that there are a number of ways of fetching a column number. The recommended way is by accessing the
4386             'column_name_to_number_mapping' hash, eg:
4387              
4388             $datasheet->{name_to_number_mapping}->{some_column_name}
4389              
4390             As of version 0.8, default values from the database schema are automatically inserted into all columns that
4391             aren't explicitely set as above.
4392              
4393             =back
4394              
4395             =head2 delete
4396              
4397             =over 4
4398              
4399             Marks all selected records for deletion, and sets the record status indicator to a 'delete' icon.
4400             The records will remain in the database until the apply method is called.
4401              
4402             =back
4403              
4404             =head2 column_from_sql_name ( $sql_fieldname )
4405              
4406             =over 4
4407              
4408             DEPRECIATED - see COLUMN NAMING section
4409              
4410             Returns a field's column number in the model. Note that you *must* use the SQL fieldname,
4411             and not the column heading's name in the treeview.
4412              
4413             =back
4414              
4415             =head2 get_column_value ( $sql_fieldname )
4416              
4417             =over 4
4418              
4419             Returns the value of the requested column in the currently selected row.
4420             If multi_select is on and more than 1 row is selected, only the 1st value is returned.
4421             You *must* use the SQL fieldname, and not the column heading's name in the treeview.
4422              
4423             =back
4424              
4425             =head2 set_column_value ( $sql_fieldname, $value )
4426              
4427             =over 4
4428              
4429             Sets the value in the given field in the current recordset.
4430             You *must* use the SQL fieldname, and not the column heading's name in the treeview.
4431              
4432             =back
4433              
4434             =head2 replace_combo_model ( $column_no, $new_model )
4435              
4436             =over 4
4437              
4438             Replaces the model for a combo renderer with a new one.
4439             You should only use this to replace models for a normal 'combo' renderer.
4440             An example of when you'd want to do this is if the options in your combo depend on a value
4441             on your *main* form ( ie not in the datasheet ), and that value changes.
4442             If you instead want to base your list of options on a value *inside* the datasheet, use
4443             the 'dynamic_combo' renderer instead ( and don't use replace_combo_model on it ).
4444              
4445             =back
4446              
4447             =head2 sum_column ( $column, [ $conditions ] )
4448              
4449             =over 4
4450              
4451             This is a convenience function that returns the sum of all values in the given column ( by number ).
4452             Fetch the column number via the column_from_sql_name() or column_from_column_name() function.
4453             Optionally, a hash description of conditions can be passed to activate 'conditional sum' functionality.
4454             The conditions hash should contain:
4455              
4456             =head3 column
4457              
4458             =over 4
4459              
4460             The column number to perform the comparison on.
4461             Fetch the column via the column_from_sql_name() or column_from_column_name() function.
4462              
4463             =back
4464              
4465             =head3 operator
4466              
4467             =over 4
4468              
4469             The type of comparison operation, from a list of:
4470              
4471             =over 4
4472              
4473             == ... a numeric equals operator
4474              
4475             eq ... a string equals operator
4476              
4477             < ... less than
4478              
4479             > ... greater than
4480              
4481             =back
4482              
4483             =back
4484              
4485             =head3 value
4486              
4487             =over 4
4488              
4489             The value to compare the column data to.
4490              
4491             =back
4492              
4493             =back
4494              
4495             =head2 max_column( $column )
4496              
4497             =over 4
4498              
4499             Returns the maximum value in column $column
4500              
4501             =back
4502              
4503             =head2 average_column( $column )
4504              
4505             =over 4
4506              
4507             Returns the average value in column $column
4508              
4509             =back
4510              
4511             =head2 count ( $column, [ $conditions ] )
4512              
4513             =over 4
4514              
4515             This is a convenience function that counts the number of records.
4516             Fetch the column number via the column_from_sql_name() or column_from_column_name() function.
4517             Optionally, a column number AND a hash description of conditions can be passed to activate 'conditional count' functionality.
4518             The conditions hash should contain:
4519              
4520             =head3 column
4521              
4522             =over 4
4523              
4524             The column number to perform the comparison on.
4525             Fetch the column via the column_from_sql_name() or column_from_column_name() function.
4526              
4527             =back
4528              
4529             =head3 operator
4530              
4531             =over 4
4532              
4533             The type of comparison operation, from a list of:
4534              
4535             =over 4
4536              
4537             == ... a numeric equals operator
4538              
4539             eq ... a string equals operator
4540              
4541             < ... less than
4542              
4543             > ... greater than
4544              
4545             =back
4546              
4547             =back
4548              
4549             =head3 value
4550              
4551             =over 4
4552              
4553             The value to compare the column data to.
4554              
4555             =back
4556              
4557             =back
4558              
4559             =head1 CONSTRUCTION
4560              
4561             The new() method requires only 3 bits of information:
4562             - a dbh
4563             - an sql object
4564             - a treeview or vbox
4565              
4566             Usually, you would also supply a fields array. All possible keys in the constructor hash are:
4567              
4568             =head2 dbh
4569              
4570             =over 4
4571              
4572             a DBI database handle
4573              
4574             =back
4575              
4576             =head2 treeview
4577              
4578             =over 4
4579              
4580             A Gtk2::TreeView to attach to. You must either supply a treeview OR a vbox ( below )
4581              
4582             =back
4583              
4584             =head2 vbox
4585              
4586             =over 4
4587              
4588             A Gtk2::VBox to place treeviews in. You must either supply a treevie OR a vbox.
4589             You'd use a vbox instead of a treeview if you wanted to activate the 'footer' functionality
4590              
4591             =back
4592              
4593             =head2 sql
4594              
4595             =over 4
4596              
4597             a hash describing the SQL to execute.
4598             Note that each clause has it's directive ( ie 'select', 'from', where', 'order by' *ommitted* )
4599             The SQL object contains the following keys:
4600              
4601             =head3 select
4602              
4603             =over 4
4604              
4605             the select clause
4606              
4607             =back
4608              
4609             =head3 from
4610              
4611             =over 4
4612              
4613             the from clause
4614              
4615             =back
4616              
4617             =head3 where
4618              
4619             =over 4
4620              
4621             the where clause ( may contain values or placeholders )
4622              
4623             =back
4624              
4625             =head3 bind_values
4626              
4627             =over 4
4628              
4629             an array of values to bind to placeholders
4630              
4631             =back
4632              
4633             =head3 pass_through
4634              
4635             =over 4
4636              
4637             a command which is passsed directly to the Database Server ( that hopefully returns a recordset ).
4638             If a pass_through key is specified, all other keys are ignored. You can use this feature to
4639             either construct your own SQL directly, which can include executing a stored procedure that
4640             returns a recordset. Recordsets based on a pass_through query will be forced to read_only mode,
4641             as updates require that column_info is available. I'm only currently using this feature for
4642             executing stored procedures, and column_info doesn't work for these. If you want to enable
4643             updates for pass_through queries, you'll have to work on getting column_info working ...
4644              
4645             =back
4646              
4647             =back
4648              
4649             =head2 footer
4650              
4651             =over 4
4652              
4653             A boolean to activate the footer treeview. This feature requires you to pass a vbox instead
4654             of a treeview, and 2 treeviews will be created, with the footer treeview tracking changes
4655             in the main treeview. You will also have to set up footer functions in the field definitions
4656             ( see the fields section below )
4657              
4658             =back
4659              
4660             =head2 footer_treeview
4661              
4662             =over 4
4663              
4664             A Gtk2::TreeView to render the footer in. Pass one in if you want to arrange / format the
4665             treeview yourself, instead of having it automatically created for you ( as in the footer
4666             support, directly above )
4667              
4668             =back
4669              
4670             =head2 primary_key
4671              
4672             =over 4
4673              
4674             POSSIBLY DANGEROUS
4675              
4676             the primary key of the table you are querying. This is detected in most cases, so specifying it
4677             is not required. In some cases ( ie multi-table queries, which aren't exactly supported ), you can
4678             possibly specify a primary key here and then try to use the datasheet to update data ... but don't
4679             look at me if things go sour :)
4680              
4681             =back
4682              
4683             =head2 multi_select
4684              
4685             =over 4
4686              
4687             a boolean to turn on the TreeView's 'multiple' selection mode. Note that if you turn this on,
4688             the function get_column_value() will only a value return the 1st selected row. The default for this
4689             is FALSE.
4690              
4691             =back
4692              
4693             =head2 read_only
4694              
4695             =over 4
4696              
4697             a boolean to lock the entire datasheet to read-only ( record status indicator will also disappear ).
4698             Note that you can also set individual fields to read_only
4699              
4700             =back
4701              
4702             =head2 before_apply
4703              
4704             =over 4
4705              
4706             a coderef to a custom function to run *before* a record is applied.
4707             For more information, see USER-DEFINED CALL-BACKS below
4708              
4709             =back
4710              
4711             =head2 on_apply
4712              
4713             =over 4
4714              
4715             a coderef to a custom function to run *after* a recordset is applied.
4716             For more information, see USER-DEFINED CALL-BACKS below
4717              
4718             =back
4719              
4720             =head2 on_row_select
4721              
4722             =over 4
4723              
4724             a coderef to a custom function to run when a row is selected
4725             For more information, see USER-DEFINED CALL-BACKS below
4726              
4727             =back
4728              
4729             =head2 dump_on_error
4730              
4731             =over 4
4732              
4733             a boolean to turn on dumping of SQL string to the STDOUT on a DBI error
4734              
4735             =back
4736              
4737             =head2 friendly_table_name
4738              
4739             =over 4
4740              
4741             a string to use in dialogs ( eg apply changes to XXX before continuing, etc ).
4742             If you don't pass a friendly_table_name, the sql->{from} clause will be used
4743              
4744             =back
4745              
4746             =head2 custom_changed_text
4747              
4748             =over 4
4749              
4750             Some text ( including pango markup ) to use for a dialog to present to the user
4751             when there are changes to the datasheet that they're about to drop, eg if they close
4752             the window or requery without hitting apply. This is only needed if you want a CUSTOM
4753             message; a relatively decent one is already raised in these situations
4754              
4755             =back
4756              
4757             =head2 fields
4758              
4759             =over 4
4760              
4761             an array of hashes to describe each field ( column ) in the TreeView.
4762             If you don't supply any field definitions, they will be constructed for you, but
4763             you will loose the ( very useful ) ability to specify column widths.
4764             Each field, described by a hash, has the following possible keys:
4765              
4766             =head3 name
4767              
4768             =over 4
4769              
4770             the name of the column. This is the name that you pass to the function:
4771             column_from_column_name()
4772             The column name is also used in the column's header, unless you specify some header_markup
4773             ( see below )
4774              
4775             ! * ! * ! * ! * ! * ! * ! * ! BIG FAT WARNING ! * ! * ! * ! * ! * ! * ! * !
4776              
4777             It is *strongly* recommended that you use the SQL field-name as the column name. The reason I'm
4778             recommending this is that I'm considering dropping support completely for NOT doing this :) Things
4779             are getting overly complex for no real reason, having to deal with SQL names, field names, etc. The
4780             recommended way of accessing column numbers is now via the 'column_name_to_number_mapping' hash
4781             ( see below ). If this is going to cause you a problem, you should contact me now and tell me about
4782             it ...
4783              
4784             ! * ! * ! * ! * ! * ! * ! * ! BIG FAT WARNING ! * ! * ! * ! * ! * ! * ! * !
4785              
4786             =back
4787              
4788             =head3 header_markup
4789              
4790             =over 4
4791              
4792             some pango markup to use in the column's header - this will be used instead of the column's name
4793              
4794             =back
4795              
4796             =head3 align
4797              
4798             =over 4
4799              
4800             the text alignment for this field - possible values are:
4801              
4802             - left
4803             - centre OR center
4804             - right
4805             - a decimal value between 0 ( left aligned ) and 1 ( right aligned )
4806              
4807             =back
4808              
4809             =head3 x_percent
4810              
4811             =over 4
4812              
4813             percentage of the available width to use for this column. If you specify an x_percent,
4814             the actual width of the field is recalculated each time the treeview is resized. The total
4815             width of the treeview is calculated, then all the fields with absolute sizing ( see below )
4816             are taken away from the total width, and the remaining width is divided up amongst fields
4817             with percentage values set.
4818              
4819             =back
4820              
4821             =head3 x_absolute
4822              
4823             =over 4
4824              
4825             an absolute value to use for the width of this column - ie fixed field width
4826              
4827             =back
4828              
4829             =head3 renderer
4830              
4831             =over 4
4832              
4833             name of Gtk2::Ex::Datasheet::DBI renderer. For more information, see the section on
4834             RENDERERS below.
4835              
4836             =back
4837              
4838             =head3 number
4839              
4840             =over 4
4841              
4842             A hash describing numeric formatting. Possible keys are:
4843              
4844             =over 4
4845              
4846             - currency - boolean - activate currency formatting
4847             - decimals - number - decimal places to render
4848             - decimal_fill - boolean - fill values to decimal_places
4849             - null_if_zero - boolean - don't render zero values
4850             - red_if_negative - boolean - render negatives values in red
4851             - separate_thousands - boolean - separate thousands with commas
4852              
4853             Activating the 'currency' key will also activate:
4854             decimals => 2,
4855             decimal_fill => TRUE,
4856             separate_thousands => TRUE
4857              
4858             =back
4859              
4860             =back
4861              
4862             =head3 footer_function
4863              
4864             =over 4
4865              
4866             A string indicating which footer function to use in the footer treeview.
4867              
4868             Current options:
4869              
4870             =over 4
4871              
4872             - sum
4873             - max
4874             - average
4875              
4876             =back
4877              
4878             Adding more functions is trivial
4879              
4880             =back
4881              
4882             =head3 foreground_colour
4883              
4884             =over 4
4885              
4886             the colour to use for the foreground text for this field
4887              
4888             =back
4889              
4890             =head3 background_colour
4891              
4892             =over 4
4893              
4894             the colour to use for the background for this field
4895              
4896             =back
4897              
4898             =head3 font_size
4899              
4900             =over 4
4901              
4902             the size of font to use for the cell ( in render mode ... edit mode is different )
4903              
4904             =back
4905              
4906             =head3 bold
4907              
4908             =over 4
4909              
4910             a boolean flag to set bold font rendering for this field
4911              
4912             =back
4913              
4914             =head3 model
4915              
4916             =over 4
4917              
4918             a TreeModel to use for a combo renderer ( see COMBOS section below )
4919              
4920             =back
4921              
4922             =head3 model_setup
4923              
4924             =over 4
4925              
4926             hash describing the setup of a combo or dynamic_combo renderer ( see COMBOS section below )
4927              
4928             =back
4929              
4930             =head3 read_only
4931              
4932             =over 4
4933              
4934             a boolean flag that locks data in this field from user edits. Note that you can also set the
4935             entire Gtk2::Ex::Datasheet::DBI object to read_only as well.
4936              
4937             =back
4938              
4939             =head3 validation
4940              
4941             =over 4
4942              
4943             a coderef to a custom function to validate data after editing and BEFORE the data is accepted.
4944             For more info, see the section on DATA VALIDATION, below.
4945              
4946             =back
4947              
4948             =head3 custom_render_functions
4949              
4950             =over 4
4951              
4952             an ARRAY of CODEREFs of custom functions to perform when rendering the field. These get attached to
4953             the CellRenderer via $renderer->set_cell_data_func .... with the added bonus that you can string one
4954             after the other easily. These custom render functions get executed in the order that they are specified
4955             in, and as a whole they get executed AFTER any builtin_render_functions ( see below )
4956              
4957             Your custom render function wil be passed:
4958              
4959             ( $tree_column, $renderer, $model, $iter, @other_stuff )
4960              
4961             ... ie @other_stuff is where you'll get anything that you pass into the function when you set it up.
4962            
4963             To allow these functions to be chained together,
4964             we copy the value from the model into the $tree_column hash, and then
4965             ALL FUNCTIONS SHOULD USE THIS VALUE AND UPDATE IT ACCORDINGLY
4966              
4967             ie In your custom render functions, you should pull the value from
4968             $tree_column->{render_value}
4969              
4970             =back
4971              
4972             =head3 builtin_render_functions
4973              
4974             =over 4
4975              
4976             an ARRAY of strings specifying built-in ( ie internal to Gtk2::Ex::Datasheet::DBI ) render functions to
4977             format or modify field data when the cell is rendered. As with custom_render_functions ( above ), these
4978             are attached to the CellRenderer via $renderer->set_cell_data-func. Built-in render functions are executed
4979             in the order that they are specified, and get executed BEFORE any custom_render_functions.
4980             Current built-in functions to choose from are:
4981              
4982             =over 4
4983              
4984             =head3 access_time
4985              
4986             A Microsoft workaround that understands MS Access' ridiculous time format.
4987             While I don't expect people to use Gtk2::Ex::Datasheet::DBI to talk to
4988             MS Access (!), people might have DATETIME fields in their database servers to stores TIME data for
4989             MS Access. This renderer understands, and sympathises with such problems ... ie values will have:
4990             '1899-12-30' prepended to them, so Access recognizes them as 'time' values.
4991              
4992             =back
4993              
4994             =head3 date_only
4995              
4996             =over 4
4997              
4998             This function strips off trailing garbage from data before rendering, and is excellent for
4999             dealing with Microsoft SQL Server's idiotic lack of a DATE type - ie SQL Server insists that
5000             all date values have 00:00:00 appended to the end of them. This function should *only* be
5001             used in conjunction with date renderers
5002              
5003             =back
5004              
5005             =head3 date_only_text
5006              
5007             =over 4
5008              
5009             This function is the same as the date_only function ( above ), but for text renderers. ie
5010             if you manually force the renderer type to 'text', then use this render function instead of
5011             the above one
5012              
5013             =back
5014              
5015             =head3 dd-mm-yyyy
5016              
5017             =over 4
5018              
5019             This function converts dates in yyyy-mm-dd format to dd-mm-yyyy before rendering
5020              
5021             =back
5022              
5023              
5024              
5025             =back
5026            
5027             =head1 RENDERERS
5028              
5029             =over 4
5030              
5031             Gtk2::Ex::Datasheet::DBI offers a number of 'renderers', which are defined per-column ( or field ).
5032             The purpose of a renderer is to present data in the datasheet, and to allow you to edit the data with
5033             the most appropriate type of interface I can muster. Some of these trigger the use of stock
5034             Gtk2::CellRenderer objects, others trigger the use of custom-built Gtk2::CellRenderer objects,
5035             and yet others merely do some formatting of information. So they don't *exactly* map to 'renderers'
5036             in the sense of Gtk2::CellRenderers, but it's close enough anyway.
5037              
5038             Renderers currently available ( feel free to submit patches for more ), are:
5039              
5040             =head2 text
5041              
5042             =over 4
5043              
5044             default if no renderer defined, and suitable for all kinds of text :)
5045              
5046             =back
5047              
5048             =head2 combo
5049              
5050             =over 4
5051              
5052             static combo box with a pre-defined list of options. Note that the model used for this
5053             renderer *can* be replaced via the function replace_combo_model(). See below section on COMBOS
5054             for more info.
5055              
5056             =back
5057              
5058             =head2 dynamic_combo
5059              
5060             =over 4
5061              
5062             combo box with a list of options that depends on values in the current row. As well as cutting
5063             down on the list of options displayed, this actually improves performance significantly - particularly
5064             if you have a lot of data. See below section on COMBOS for more info.
5065              
5066             =back
5067              
5068             =head2 toggle
5069              
5070             =over 4
5071              
5072             great for boolean values, and good looking too :)
5073              
5074             =back
5075              
5076             =head2 date
5077              
5078             =over 4
5079              
5080             good for dates. MUST be in YYYY-MM-DD format ( ie most databases should be OK )
5081              
5082             =back
5083              
5084             =head2 time
5085              
5086             =over 4
5087              
5088             uses a cell renderer with 3 spin buttons for setting the time
5089              
5090             =back
5091              
5092             =head2 progress
5093              
5094             =over 4
5095              
5096             a progress bar. Give it a decimal between 0 and 1. Read-Only ... in fact I don't know what will happen
5097             if you try to apply a datasheet with a progress renderer - I've never tested. Looks nice in read-only
5098             datasheets ...
5099              
5100             =back
5101              
5102             =head2 hidden
5103              
5104             =over 4
5105              
5106             use this for ... hidden columns!
5107              
5108             =back
5109              
5110             =head2 number
5111              
5112             =over 4
5113              
5114             This renderer is currently broken and being defaulted back to the text renderer.
5115             I'm keeping it in place in the hope that someone will fix it.
5116             Alternatively, gtk-2.10.x has added a CellRendererSpinButton, which could be used here.
5117             Either way, I'm keeping this around with the intention of one day reactivating it. It's
5118             perfectly safe to define fields with a number renderer, and have them default back to text.
5119              
5120             =back
5121              
5122             =back
5123              
5124             =head1 Accessing columns
5125              
5126             =over 4
5127              
5128             The new way of accessing columns ( ie fetching a column number ) is via the 'column_name_to_number_mapping'
5129             hash, ie:
5130              
5131             $datasheet->{column_name_to_number_mapping}->{your_column_name}
5132              
5133             will give you the column number.
5134              
5135             This is meant to replace all the other dodgy BS such as:
5136              
5137             column_from_column_name()
5138             column_from_name()
5139             column_from_sql_name()
5140             column_name_to_sql_name()
5141              
5142             ... some of which wasn't documented anyway. If you use any of these functions, it's time to stop using
5143             them, or email me and tell me why I shouldn't remove them in the next release :)
5144            
5145             =back
5146            
5147             =head1 DATA VALIDATION
5148              
5149             You can specify a custom function to validate data as it's entered in a cell, and before
5150             the data is accepted into the cell. Your function will receive a hash containing:
5151              
5152             {
5153            
5154             renderer,
5155             text_path,
5156             new_text
5157            
5158             }
5159              
5160             You can also use functions such as get_column_value() to extract the values of other columns
5161             in the currently selected row ( as long as you don't have multi-select turned on ).
5162              
5163             Your sub should return TRUE to accept the changes, or FALSE to reject them. If you reject
5164             changes, you should provide your own error dialog ( eg via Gtk2::Ex::Dialogs ) explaining
5165             what's happening.
5166              
5167             =head1 COMBOS
5168              
5169             For combo and dynamic_combo renderers, the 'model_setup' hash should is ( unfortunately ) quite different.
5170             I'm planning on updating the dynamic_combo 'model_setup' hash to be more like everything else, but for now,
5171             it's different ...
5172              
5173             =head2 model_setup for combo renderers
5174              
5175             =over 4
5176              
5177             The model_setup is identical to the form in Gtk2::Ex::DBI. There is an example at the very top of this
5178             POD. Descriptions of each hash element:
5179              
5180             =head2 fields
5181              
5182             =over 4
5183              
5184             An array of field definitions. Each field definition is a hash with the following keys:
5185              
5186             =head2 name
5187              
5188             =over 4
5189              
5190             The SQL fieldname / expression
5191              
5192             =back
5193              
5194             =head2 type
5195              
5196             =over 4
5197              
5198             The ( Glib ) type of column to create for this field in the Gtk2::ListStore. Possible values are
5199             Glib::Int and Glib::String.
5200              
5201             =back
5202              
5203             =head2 cell_data_func ( optional )
5204              
5205             =over 4
5206              
5207             A reference to some perl code to use as this columns's renderer's custom cell_data_func.
5208             You can use this to perform formatting on the column ( or cell, whatever ) based on the
5209             current data. Your function will be passed ( $column, $cell, $model, $iter ), as well as anything
5210             else you pass in yourself.
5211              
5212             =back
5213              
5214             =back
5215              
5216             =back
5217              
5218             =head2 sql
5219              
5220             =over 4
5221              
5222             A hash of SQL related stuff. Possible keys are:
5223              
5224             =head2 from
5225              
5226             =over 4
5227              
5228             The from clause
5229              
5230             =back
5231              
5232             =head2 where_object
5233              
5234             =over 4
5235              
5236             This can either be a where clause, or a hash with the following keys:
5237              
5238             =head2 where
5239              
5240             =over 4
5241              
5242             The where key should contain the where clause, with placeholders ( ? ) for each value.
5243             Using placeholders is particularly important if you're assembling a query based on
5244             values taken from a form, as users can initiate an SQL injection attack if you
5245             insert values directly into your where clause.
5246              
5247             =back
5248              
5249             =head2 bind_values
5250              
5251             =over 4
5252              
5253             bind_values should be an array of values, one for each placeholder in your where clause.
5254              
5255             =back
5256              
5257             =back
5258              
5259             =head2 order_by
5260              
5261             =over 4
5262              
5263             An 'order by' clause
5264              
5265             =back
5266              
5267             =back
5268              
5269             =head2 alternate_dbh
5270              
5271             =over 4
5272              
5273             A DBI handle to use instead of the current Gtk2::Ex::DBI DBI handle
5274              
5275             =back
5276              
5277             =back
5278              
5279             ---
5280              
5281             =head2 model_setup for dynamic_combo renderers
5282              
5283             =over 4
5284              
5285             The current format for dynamic_combos is:
5286              
5287             {
5288              
5289             id => "ID"
5290             display => "Description",
5291             from => "SomeTable",
5292             criteria => [
5293             {
5294             field => "first_where_clause_field",
5295             column_name => "column_name_of_first_value_to_use"
5296             },
5297             {
5298             field => "second_where_clause_field",
5299             column_name => "column_name_of_second_value_to_use"
5300             }
5301             ],
5302             group_by => "group by ID, Description",
5303             order_by => "order by some_field_to_order_by"
5304              
5305             }
5306              
5307             Briefly ...
5308              
5309             The 'id' key defines the primary key in the table you are querying. This is the value that will be
5310             stored in the dynamic_combo column.
5311              
5312             The 'display' key defines the text value that will be *displayed* in the the dynamic_combo column,
5313             and also in the list of combo options.
5314              
5315             The 'table' key is the source table to query.
5316              
5317             The 'criteria' key is an array of hashes for you to define criteria. Inside each hash, you have:
5318              
5319             - 'field' key, which is the field in the table you are querying ( ie it will go into the where clause )
5320             - 'column_name' key, which is the *SQL* column name to use as limiting value in the where clause
5321              
5322             The 'group_by' key is a 'group by' clause. You *shouldn't* need one, but I've added support anyway...
5323              
5324             The 'order_by' key is an 'order by' clause
5325              
5326             =back
5327              
5328             =head1 USER-DEFINED CALL-BACKS
5329              
5330             =head2 before_apply
5331              
5332             =over 4
5333              
5334             You can specify a custom function to run *before* changes to a recordset are applied
5335             ( see new() method ). The function will be called for *every* record that has been changed.
5336             The user-defined code will be passed a reference to a hash:
5337              
5338             {
5339             status => a string, with possible values: 'inserted', 'changed', or 'deleted'
5340             primary_key => the primary key of the record in question
5341             }
5342              
5343             Your code *must* return a positive value to allow the record to be applied - if your code
5344             returns FALSE, the changes to the current record will NOT be applied.
5345              
5346             =back
5347              
5348             =head2 on_apply
5349              
5350             =over 4
5351              
5352             You can specify some code to run *after* changes to a recordset is applied ( see new() method ).
5353             It will be called for *every* record that has been changed. The user-defined code will be
5354             passed a reference to a hash:
5355              
5356             {
5357             status => a string, with possible values: 'inserted', 'changed', or 'deleted'
5358             primary_key => the primary key of the record in question
5359             }
5360              
5361             =back
5362              
5363             =head2 on_row_select
5364              
5365             =over 4
5366              
5367             You can specify some code to run when a row is selected ( see new() method ).
5368             Your code will be passed the Gtk2::TreeSelection object ( and anything else you pass yourself ).
5369             Nothing internal to Gtk2::Ex::Datasheet::DBI is currently passed to this code, as it is
5370             trivial to grab the data you need via get_column_value().
5371              
5372             =back
5373              
5374             =head1 GENERAL RANTING
5375              
5376             =head2 Automatic Column Widths
5377              
5378             =over 4
5379              
5380             You can use x_percent and x_absolute values to set up automatic column widths. Absolute values are set
5381             once - at the start. In this process, all absolute values ( including the record status column ) are
5382             added up and the total stored in $self->{sum_absolute_x}.
5383              
5384             Each time the TreeView is resized ( size_allocate signal ), the size_allocate method is called which resizes
5385             all columns that have an x_percent value set. The percentages should of course all add up to 100%, and the width
5386             of each column is their share of available width:
5387             ( total width of treeview ) - $self->{sum_absolute_x} * x_percent
5388              
5389             IMPORTANT NOTE:
5390             The size_allocate method interferes with the ability to resize *down*. I've found a simple way around this.
5391             When you create the TreeView, put it in a ScrolledWindow, and set the H_Policy to 'automatic'. I assume this allows
5392             you to resize the treeview down to smaller than the total width of columns ( which automatically creates the
5393             scrollbar in the scrolled window ). Immediately after the resize, when our size_allocate method recalculates the
5394             size of each column, the scrollbar will no longer be needed and will disappear. Not perfect, but it works. It also
5395             doesn't produce *too* much flicker on my system, but resize operations are noticably slower. What can I say?
5396             Patches appreciated :)
5397              
5398             =back
5399              
5400             =head2 $Gtk2::Ex::Datasheet::DBI::gtk2_main_iteration_in_query
5401              
5402             =over 4
5403              
5404             For slow network connections, your gtk2 GUI may appear to hang while populating the treeview from a large query.
5405             To make things feel more fluid, you can set $Gtk2::Ex::Datasheet::DBI::gtk2_main_in_query = TRUE in your
5406             application, which will trigger:
5407              
5408             Gtk2->main_iteration while ( Gtk2->events_pending );
5409              
5410             for each record appended to the treeview. While this slows down operation of your application, it *appears* to
5411             have the opposite effect, as the GUI remains responsive. This is even the case when using high speed networks.
5412              
5413             I am considering using a 2nd thread to fetch data, which will remove the need for this, but for now, it's a
5414             hack that works. Please supply patches for multi-threaded operation :)
5415              
5416             =back
5417              
5418             =head2 Use of Database Schema
5419              
5420             =over 4
5421              
5422             Version 0.8 introduces querying the database schema to inspect column attributes. This considerably streamlines
5423             the process of setting up the datasheet and inserting records.
5424              
5425             If you don't define a renderer, an appropriate one is selected for you based on the field type.
5426             The only renderers you should now have to explicitely define
5427             are 'hidden', 'combo', and 'dynamic_combo' - the latter 2 you will obviously still have to set up by providing
5428             a model.
5429              
5430             When inserting a new record, default values from the database field definitions are also used ( unless you
5431             specify another value via the insert() method ).
5432              
5433             =back
5434              
5435             =head2 CellRendererCombo
5436              
5437             =over 4
5438              
5439             If you have Gtk-2.6 or greater, you can use the new CellRendererCombo. Set the renderer to 'combo' and attach
5440             your model to the field definition. You currently *must* have a model with ( numeric ) ID / String pairs, which is the
5441             usual for database applications, so you shouldn't have any problems. See the example application for ... an example.
5442              
5443             =back
5444              
5445             =head1 AUTHORS
5446              
5447             Daniel Kasak - dan@entropy.homelinux.org
5448              
5449             =head1 CREDITS
5450              
5451             Muppet
5452              
5453             - tirelessly offered help and suggestions in response to my endless list of questions
5454              
5455             Torsten Schoenfeld
5456              
5457             - wrote custom CellRendererDate ( from the Gtk2-Perl examples )
5458             - wrote custom CellRendererText ( with improved focus policy ) in Odot which I used here
5459              
5460             Gtk2-Perl Authors
5461              
5462             - obviously without them, I wouldn't have gotten very far ...
5463              
5464             Gtk2-Perl list
5465              
5466             - yet more help, suggestions, and general words of encouragement
5467              
5468             =head1 BUGS
5469              
5470             I think you must be mistaken
5471              
5472             =head1 ISSUES
5473              
5474             That's right. These are 'issues', not 'bugs' :)
5475              
5476             =head2 CellRendererTime
5477              
5478             =over 4
5479              
5480             For some reason, the 1st time you go to edit a cell with a CellRendererTime, it doesn't receive
5481             the current value. It works every other time after this. Weird. Anyone know what's up?
5482              
5483             =back
5484              
5485             =head2 SQL Server compatibility
5486              
5487             =over 4
5488              
5489             To use SQL Server, you should use FreeTDS ==> UnixODBC ==> DBD::ODBC. Only this combination supports
5490             the use of bind values in SQL statements, which is a requirement of Gtk2::Ex::Datasheet::DBI. Please
5491             make sure you have the *very* *latest* versions of each.
5492              
5493             The only problem I've ( recently ) encountered with SQL Server is with the 'money' column type.
5494             Avoid using this type, and you should have flawless SQL Server action.
5495              
5496             =back
5497              
5498             =head1 Other cool things you should know about:
5499              
5500             This module is part of an umbrella 'Axis' project, which aims to make
5501             Rapid Application Development of database apps using open-source tools a reality.
5502             The project includes:
5503              
5504             Gtk2::Ex::DBI - forms
5505             Gtk2::Ex::Datasheet::DBI - datasheets
5506             PDF::ReportWriter - reports
5507              
5508             All the above modules are available via cpan, or for more information, screenshots, etc, see:
5509             http://entropy.homelinux.org/axis
5510              
5511             =head1 Crank ON!
5512              
5513             =cut