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 |