1 # vim: ft=tcl foldmethod=marker foldmarker=<<<,>>> ts=4 shiftwidth=4
4 # valid_status_changed(isvalid?, {reasons}) Fired when the form valid status
6 # onchange(datarray) Fired when some form data is changed
7 # onchange,field(newvalue) Fired when specific form element is changed
9 proc tlc::form {pathName args} {
10 return [uplevel [list tlc::Form $pathName] $args]
13 ::itk::usual tlc::Form {
14 # This is a placeholder. Flesh out with appropriate options
19 inherit tlc::Border tlc::Handlers tlc::Textvariable tlc::Signalsource
20 #inherit tlc::Formbase tlc::Border
25 itk_option define -padding padding Padding 0 {need_rerender}
26 itk_option define -tooltipdelay toolTipDelay ToolTipDelay 1500 {}
27 itk_option define -tooltiptimeout toolTipTimeout ToolTipTimeout 3500 {}
28 itk_option define -winding winding Winding v {need_rerender}
32 variable state "normal"
35 method set_data {args}
37 method get_data {args}
39 method itemconfig {name args}
41 method path_by_var {varname}
42 method set_tips {args}
44 method dirty_gate_ref {} ;# Obsolete, use signal_ref form_dirty
45 method valid_gate_ref {} ;# Obsolete, use signal_ref form_valid
46 method changed_dom_ref {}
48 method mark_dirty {state}
49 method item_attach_signal {name signal {sense normal}}
50 method item_attach_expression {name expression}
51 method varname_from_path {path}
52 method arm_dirty {state}
53 method force_form_valid_update {}
54 method valid_condition {new_condition desc fields}
55 method valid_signal {signal desc fields}
56 method default_options {type args}
60 method textvariable_changed {newvalue}
61 method handlers_debug {lvl msg} {log $lvl $msg}
62 method widget_destroyed {}
73 variable valid_toggles
75 variable expression_lists
76 variable valid_conditions {}
77 variable valid_signals
81 variable default_options
84 method need_rerender {}
85 method dat_changed {n1 n2 op}
86 method clear_valid_conditions {}
88 method valid_changed {condition desc fields newstate}
89 method update_valid_desc {}
90 method enabled_changed {newstate}
91 method form_valid_reasons_changed {}
92 method canonize_type {type}
98 configbody tlc::Form::schema { #<<<1
100 set schema [tlc::decomment $schema]
105 configbody tlc::Form::state { #<<<1
106 log debug "state: ($state)"
107 [stategate_ref] configure -default [expr {$state == "normal"}]
111 configbody tlc::Form::tooltipdelay { #<<<1
112 $w.tips configure -delay $itk_option(-tooltipdelay)
116 configbody tlc::Form::tooltiptimeout { #<<<1
117 $w.tips configure -showtime $itk_option(-tooltiptimeout)
121 body tlc::Form::constructor {args} { #<<<1
123 array set paths_by_var {}
125 array set valid_toggles {}
126 array set expressions {}
127 array set expression_lists {}
128 array set valid_signals {}
129 array set field_valid {}
131 array set default_options {}
133 tlc::Domino #auto dominos(need_rerender) -name "$w need_rerender"
134 tlc::Domino #auto dominos(need_valid_desc_update) \
135 -name "$w need_valid_desc_update"
136 tlc::Varwatch #auto signals(form_dirty) -name "$w form_dirty"
137 tlc::Vardomino #auto dominos(changed) -name "$w changed" \
138 -textvariable [scope dat]
139 $signals(form_dirty) attach_dirtyvar [scope dat]
140 $signals(form_dirty) register_handler onchange_info [code $this dat_changed]
141 tlc::Hoverbox $w.tips
142 tlc::Hoverbox $w.valid_tips
143 tlc::Gate #auto signals(enabled) -name "$w enabled" \
144 -mode "and" -default 1
145 tlc::Gate #auto signals(form_valid) -name "$w form_valid" \
146 -mode "and" -default 1
147 tlc::Domino #auto dominos(reasons_changed) -name "$w reasons_changed"
149 $signals(enabled) attach_input [stategate_ref]
150 $signals(enabled) attach_output [code $this enabled_changed]
152 $dominos(reasons_changed) attach_output \
153 [code $this form_valid_reasons_changed]
154 $signals(enabled) attach_output [list $dominos(reasons_changed) tip]
156 $dominos(need_rerender) attach_output [code $this rerender]
157 $dominos(need_valid_desc_update) attach_output \
158 [code $this update_valid_desc]
160 eval itk_initialize $args
165 log debug "Setting name ($name) for $w"
170 body tlc::Form::destructor {} { #<<<1
173 # Thse reference widgets that may not exist anymore (if we are here
174 # because of destroy rather than delete object, for instance), and
175 # the standard cleanup will have them change state, and hence try to
176 # configure their widgets. Doing this causes them to die before their
177 # inputs, which stops them trying to update their outputs
179 #foreach child [winfo children $w] {
180 # if {[itcl::is object -class tlc::Form $child]} {
181 # log debug "Pre-emptively deleting subform $child"
182 # delete object $child
188 body tlc::Form::set_data {args} { #<<<1
189 if {[llength $args] == 1} {
190 set data [lindex $args 0]
194 if {[llength $data] % 2 != 0} {
195 log error "Badly formatted data, must be a list of key value pairs"
196 error "Badly formatted data, must be a list of key value pairs" "" \
197 [list data_format_error]
200 $signals(form_dirty) disarm
204 foreach {varname exprs} [array get expression_lists] {
205 foreach exprname $exprs {
206 $expressions($exprname) reassess
210 $signals(form_dirty) arm
211 if {[$signals(form_dirty) is_armed]} {
212 $signals(form_dirty) set_state 0
217 body tlc::Form::set_key {args} { #<<<1
219 switch -- [llength $args] {
220 0 {error "No key specified" "" [list syntax_error no_key_specified]}
221 1 {error "No value specified" "" [list syntax_error no_value_specified]}
223 set key [lindex $args 0]
224 set val [lindex $args 1]
225 return [set_data $key $val]
229 set key [lindex $args 0]
230 set keys [lrange $args 1 end-1]
231 set val [lindex $args end]
232 set handler [path_by_var $key]
233 if {[itcl::is object $handler] && [$handler isa tlc::Form]} {
234 eval [list $handler set_key] $keys [list $val]
235 set dat($key) [$handler get_data]
237 # TODO: recursive setter that composes the nested array list on unwind
238 error "Only subforms are currently supported for deep key sets" "" \
239 [list not_implemented]
244 body tlc::Form::get_data {args} { #<<<1
245 $dominos(need_rerender) force_if_pending
247 if {[llength $args] == 0} {
248 return [array get dat]
249 } elseif {[llength $args] == 1} {
250 set key [lindex $args 0]
251 if {![info exists dat($key)]} {
252 error "Bad key: \"$key\", should be one of [join [array names tmp] {, }]" "" \
253 [list bad_key [list $key]]
257 set last [array get dat]
265 error "Cannot build array from key level [llength $so_far] \"[join $so_far ->]\": $errmsg" "" \
266 [list format_error $so_far]
270 if {![info exists tmp($key)]} {
271 error "Bad key: \"[join $so_far ->]\", should be one of [join [array names tmp] {, }]" "" \
272 [list bad_key $so_far]
281 body tlc::Form::dirty_gate_ref {} { #<<<1
282 log warning "dirty_gate_ref deprecated, use \"signal_ref form_dirty\" instead"
283 return $signals(form_dirty)
287 body tlc::Form::rerender {} { #<<<1
288 if {$schema == $oldschema} return
292 $signals(form_dirty) disarm
293 catch {eval destroy [lindex [tlc::intersect3 [winfo children $w] [list $w.tips $w.valid_tips]] 0]}
295 # Layout context init
297 label_col_args_sticky {}
298 label_cell_args_sticky {-anchor ne}
299 label_widget_args_sticky {}
300 cell_args_sticky {-anchor nw}
303 set layout(row_args_sticky) [list -pady $itk_option(-padding)]
305 catch {unset paths}; array set paths {}
306 catch {unset paths_by_var}; array set paths_by_var {}
307 catch {unset toggles}; array set toggles {}
308 catch {unset valid_toggles}; array set valid_toggles {}
309 catch {unset field_valid}; array set field_valid {}
310 catch {unset valid_signals}; array set valid_signals {}
311 catch {unset expressions}
312 catch {unset expression_lists}
313 clear_valid_conditions
318 foreach {label info} $schema {
324 set layout(label_cell_args) $layout(label_cell_args_sticky)
325 set layout(label_col_args) $layout(label_col_args_sticky)
326 set layout(label_widget_args) $layout(label_widget_args_sticky)
327 set layout(cell_args) $layout(cell_args_sticky)
328 set layout(row_args) $layout(row_args_sticky)
329 set layout(col_args) $layout(col_args_sticky)
332 if {[string index $label 0] == "_"} {
335 if {$label == "_layout"} {
336 switch -- [lindex $info 0] {
340 label_cell_args_sticky -
341 label_widget_args_sticky -
342 label_col_args_sticky {
343 set layout([lindex $info 0]) [lrange $info 1 end]
344 set layout([string range [lindex $info 0] 0 end-7]) \
348 log warning "_layout label_args_sticky is deprecated - use label_cell_args_sticky"
349 set layout(label_cell_args_sticky) [lrange $info 1 end]
350 set layout(label_cell_args) \
360 set layout([lindex $info 0]) [lrange $info 1 end]
364 log warning "_layout label_args is deprecated - use label_cell_args"
365 set layout(label_cell_args) [lrange $info 1 end]
372 switch -- [string index $itk_option(-winding) 0] {
376 set layout(row_args) $layout(row_args_sticky)
383 set layout(col_args) $layout(col_args_sticky)
394 switch -- [string index $itk_option(-winding) 0] {
396 set amnt [lindex $info 1]
397 if {$amnt == ""} {set amnt 1}
404 set amnt [lindex $info 1]
405 if {$amnt == ""} {set amnt 1}
412 log warning "default_options mode of _layout is deprecated. Use top level directive _default_options instead"
413 set type [canonize_type [lindex $info 1]]
414 set default_options($type) [lrange $info 2 end]
418 error "Unrecognised layout parameter: ([lindex $info 0])"
424 _validation - _validation_not_blank { #<<<
425 if {$label == "_validation"} {
426 valid_condition [lindex $info 0] [lindex $info 1] [lindex $info 2]
428 if {$label == "_validation_not_blank"} {
429 set template [lindex $info 0]
430 foreach {labelname varname} [lrange $info 1 end] {
431 set condition "\[string trim \$dat([string map {{ } {\ } {)} {\)}} $varname])\] != {}"
434 [string map [list %1 $labelname] $template] \
440 _tips - _tooltips { #<<<
448 _default_options { #<<<
449 set type [canonize_type [lindex $info 0]]
450 set default_options($type) [lrange $info 1 end]
454 error "Invalid meta directive \"$label\", must be one of _layout, _validation, _validation_not_blank, _tips or _tooltips" "" [list invalid_meta_directive $label]
463 foreach {varname type} $info break
464 lappend formvars $varname
465 set type [canonize_type $type]
467 if {[info exists default_options($type)]} {
468 set arglist [concat $default_options($type) [lrange $info 2 end]]
470 set arglist [lrange $info 2 end]
473 if {[string index $label 0] == " "} {
474 label $w.$row,$col,l -text "" -width 0 -highlightthickness 0 \
475 -background $itk_option(-background)
477 label $w.$row,$col,l -text $label \
478 -background $itk_option(-background)
480 if {[llength $layout(label_widget_args)] > 0} {
481 eval [list $w.$row,$col,l configure] $layout(label_widget_args)
484 if {![info exists dat($varname)]} {
487 if {[string first " " $varname] != -1} {
488 # Itcl bug (still as of 3.3) prevents scope / resolver working
489 # with arrays whose keys contain spaces. We craft our own here
490 # that works with the resolver
493 #set scoped_varname "[list @itcl $this] [namespace current]::dat($varname)"
494 set tmp [scope dat($varname)]
495 set scoped_varname [concat [lrange $tmp 0 1] [lindex $tmp 2]]
497 set scoped_varname [scope dat($varname)]
500 tlc::Gate #auto field_valid($varname) -name "$w field_valid $varname" \
501 -mode "and" -default 1
502 tlc::StateToggle #auto valid_toggles($varname) \
503 -mode "or" -default 1 \
505 -foreground {red black}
506 $valid_toggles($varname) attach_signal $signals(enabled) inverted
507 $valid_toggles($varname) attach_signal $field_valid($varname)
509 $valid_toggles($varname) attach_output \
510 [list $dominos(reasons_changed) tip]
512 set toggle {-state {disabled normal}}
515 entry $w.$row,$col,v -textvariable $scoped_varname
519 checkbutton $w.$row,$col,v -variable $scoped_varname
523 label $w.$row,$col,v -textvariable $scoped_varname \
525 -font [$tlc::theme setting boldfont]
529 tlc::vartextbox $w.$row,$col,v -textvariable $scoped_varname
533 button $w.$row,$col,v
537 tlc::mycombobox $w.$row,$col,v -textvariable $scoped_varname
541 tlc::Dateentry $w.$row,$col,v -textvariable $scoped_varname
545 tlc::Calendar $w.$row,$col,v -command [code set dat($varname)]
549 tlc::Fileselectbox $w.$row,$col,v -textvariable $scoped_varname
553 tlc::Browse_treeview_list $w.$row,$col,v \
554 -textvariable $scoped_varname \
555 -width 300 -height 110
559 tlc::Lookup $w.$row,$col,v -textvariable $scoped_varname
563 tlc::Spinint $w.$row,$col,v -textvariable $scoped_varname
567 tlc::Spinner $w.$row,$col,v -textvariable $scoped_varname
568 # TODO: wire valid_signal?
572 message $w.$row,$col,v \
573 -textvariable $scoped_varname \
574 -font [$tlc::theme setting boldfont]
579 entry $w.$row,$col,v -textvariable $scoped_varname \
581 -validatecommand {string is integer {%P}}
585 Tagentry $w.$row,$col,v -textvariable $scoped_varname
589 tlc::Radiogroup $w.$row,$col,v -textvariable $scoped_varname
593 tlc::Checkgroup $w.$row,$col,v -textvariable $scoped_varname
597 tlc::Form $w.$row,$col,v -textvariable $scoped_varname \
598 -name "$name -> $varname"
599 valid_signal [$w.$row,$col,v signal_ref form_valid] "" $varname
603 tlc::Tablelist $w.$row,$col,v -textvariable $scoped_varname
604 # TODO: wire valid_signal?
608 if {[info exists tlc::Form::custom_types($type)]} {
609 $tlc::Form::custom_types($type) $w.$row,$col,v \
610 -textvariable $scoped_varname
612 log error "Unknown type: ($type)"
617 set paths($label) $w.$row,$col,v
618 set paths_by_var($varname) $w.$row,$col,v
619 eval [list tlc::StateToggle #auto toggles($varname) $w.$row,$col,v] \
621 $toggles($varname) attach_signal $signals(enabled)
623 if {[llength $arglist] != 0} {
624 eval [list $w.$row,$col,v configure] $arglist
627 set dc [expr {$col+1}]
628 eval [list blt::table $w $w.$row,$col,l $row,$col] \
629 $layout(label_cell_args)
630 blt::table configure $w c$col -resize none
631 eval [list blt::table $w $w.$row,$col,v $row,$dc] $layout(cell_args)
632 eval [list blt::table configure $w c$col] $layout(label_col_args)
633 eval [list blt::table configure $w c$dc] $layout(col_args)
634 eval [list blt::table configure $w r$row] $layout(row_args)
636 switch -- [string index $itk_option(-winding) 0] {
651 if {![info exists signals(form_dirty)]} {
652 log error "Freak out - signals(form_dirty) missing\n$w\n[tlc::stackdump]"
655 $signals(form_dirty) arm
656 $signals(form_dirty) set_state 0
660 body tlc::Form::takefocus {} { #<<<1
661 $dominos(need_rerender) force_if_pending
666 body tlc::Form::need_rerender {} { #<<<1
667 $dominos(need_rerender) tip
671 body tlc::Form::itemconfig {name args} { #<<<1
672 set path [path $name]
673 eval [list $path configure] $args
677 body tlc::Form::path {name} { #<<<1
678 $dominos(need_rerender) force_if_pending
680 if {[string is integer -strict $name]} {
681 if {[winfo exists $w.$name,0,v]} {
684 error "No such item index: ($name)"
686 } elseif {[regexp {[0-9]+,[0-9]+} $name]} {
687 foreach {r c} [split $name ,] break
688 if {[winfo exists $w.$r,$c,v]} {
691 error "No such item index: ($name)"
694 if {![info exists paths($name)]} {
695 error "No such form item: ($name), choose from (\"[join [array names paths] {", "}]\")"
702 body tlc::Form::changed_dom_ref {} { #<<<1
703 return $dominos(changed)
707 body tlc::Form::set_tips {args} { #<<<1
708 if {[llength $args] == 1} {
709 # Support the variant syntax of one item packed with all the tips
711 set args [lindex $args 0]
713 foreach {label tip} $args {
714 set widget [path $label]
715 $w.tips attach $widget $tip
721 body tlc::Form::mark_dirty {state} { #<<<1
723 $signals(form_dirty) set_state $state
727 body tlc::Form::path_by_var {varname} { #<<<1
728 $dominos(need_rerender) force_if_pending
730 if {![info exists paths_by_var($varname)]} {
731 error "No such form item: ($varname)" "" \
732 [list invalid_form_item $varname]
734 return $paths_by_var($varname)
737 body tlc::Form::dat_changed {n1 n2 op} { #<<<1
739 log debug "dat($n2) changed: ($dat($n2))"
740 } elseif {$op == "u"} {
741 log debug "dat($n2) unset"
743 set_textvariable [get_data]
745 if {[$signals(form_dirty) is_armed]} {
747 if {[info exists dat($n2)]} {
748 if {[info exists expression_lists($n2)]} {
749 foreach exprname $expression_lists($n2) {
750 $expressions($exprname) reassess
753 invoke_handlers onchange,$n2 $dat($n2)
755 invoke_handlers onchange [array get dat]
757 $signals(form_dirty) set_state 1
762 body tlc::Form::clear_data {} { #<<<1
764 $signals(form_dirty) disarm
767 # TODO: figure out why this is necessary
768 foreach key [array names dat] {
773 #array set toggles {}
774 #array unset expressions
775 $signals(form_dirty) set_state 0
776 $signals(form_dirty) arm
780 body tlc::Form::item_attach_signal {name signal {sense normal}} { #<<<1
781 set path [path $name]
782 set varname [varname_from_path $path]
783 $toggles($varname) attach_signal $signal $sense
787 body tlc::Form::item_attach_expression {name expression} { #<<<1
788 set path [path $name]
789 set varname [varname_from_path $path]
790 #array unset expressions $varname
791 set exprname "$varname[incr expr_seq]"
792 tlc::Expression #auto expressions($exprname) -name "Form expression on $varname ($exprname)"
793 lappend expression_lists($varname) $exprname
794 set expression [string map [list \$value %<dat($varname)%>] $expression]
795 $expressions($exprname) set_expression $expression
797 return $expressions($exprname)
801 body tlc::Form::varname_from_path {path} { #<<<1
803 foreach {v p} [array get paths_by_var] {
814 error "No varname known for path: ($path)"
819 body tlc::Form::arm_dirty {state} { #<<<1
822 $signals(form_dirty) arm
824 $signals(form_dirty) disarm
829 body tlc::Form::valid_gate_ref {} { #<<<1
830 log warning "valid_gate_ref deprecated, use \"signal_ref form_valid\" instead"
831 return $signals(form_valid)
835 body tlc::Form::valid_condition {new_condition desc fields} { #<<<1
836 tlc::Signal #auto valid_signals($new_condition) -name "valid_signals($new_condition)"
837 lappend valid_conditions [list $new_condition $desc $fields $valid_signals($new_condition)]
839 foreach field $fields {
840 $field_valid($field) attach_input $valid_signals($new_condition)
841 $valid_toggles($field) attach_output \
842 [code $this valid_changed $new_condition $desc $fields]
847 $signals(form_valid) attach_input $valid_signals($new_condition)
851 body tlc::Form::valid_signal {signal desc fields} { #<<<1
852 $dominos(need_rerender) force_if_pending
854 lappend valid_conditions [list {} $desc $fields $signal]
856 foreach field $fields {
857 $field_valid($field) attach_input $signal
858 $valid_toggles($field) attach_output \
859 [code $this valid_changed {} $desc $fields]
862 $signals(form_valid) attach_input $signal
866 body tlc::Form::check_valid {} { #<<<1
867 foreach condition_desc $valid_conditions {
868 foreach {condition desc fields signal} $condition_desc break
869 if {$condition != {}} {
870 $signal set_state [expr $condition]
876 body tlc::Form::clear_valid_conditions {} { #<<<1
877 set valid_conditions {}
883 body tlc::Form::valid_changed {condition desc fields newstate} { #<<<1
884 $dominos(need_valid_desc_update) tip
888 body tlc::Form::update_valid_desc {} { #<<<1
889 array set new_desc {}
891 foreach condition_desc $valid_conditions {
892 foreach {condition desc fields signal} $condition_desc break
893 foreach field $fields {
894 if {![$valid_toggles($field) state]} {
895 lappend new_desc($field) $desc
900 set obsolete [lindex [tlc::intersect3 [array names paths_by_var] [array names new_desc]] 0]
901 foreach field $obsolete {
902 $w.valid_tips detach [$valid_toggles($field) target]
904 foreach {field desclist} [array get new_desc] {
905 $w.valid_tips attach [$valid_toggles($field) target] [join $desclist \n]
910 body tlc::Form::force_form_valid_update {} { #<<<1
911 form_valid_reasons_changed
915 body tlc::Form::enabled_changed {newstate} { #<<<1
917 $w.valid_tips popdown
922 body tlc::Form::form_valid_reasons_changed {} { #<<<1
923 set newstate [$signals(form_valid) state]
925 invoke_handlers valid_status_changed $newstate {}
927 if {[$signals(enabled) state]} {
929 foreach condition_desc $valid_conditions {
930 foreach {condition desc fields signal} $condition_desc break
931 if {![$signal state]} {
932 lappend reasons $desc
935 invoke_handlers valid_status_changed $newstate $reasons
937 invoke_handlers valid_status_changed $newstate "Form disabled"
943 body tlc::Form::textvariable_changed {newvalue} { #<<<1
944 log debug "$name newvalue: ($newvalue)"
945 array set tmp $newvalue
946 set existing [array names dat]
947 set new [array names tmp]
950 set removed_keys [lindex [tlc::intersect3 $existing $new] 2]
951 foreach key $removed_keys {
959 body tlc::Form::default_options {type args} { #<<<1
960 set type [canonize_type $type]
962 set default_options($type) $args
966 body tlc::Form::canonize_type {type} { #<<<1
969 entry {return "entry"}
971 checkbutton {return "checkbutton"}
972 label {return "label"}
976 button {return "button"}
978 mycombobox {return "combobox"}
980 dateentry {return "dateentry"}
981 calendar {return "calendar"}
982 fileselect {return "fileselect"}
984 lookup {return "lookup"}
985 spinint {return "spinint"}
986 spinner {return "spinner"}
987 message {return "message"}
988 intentry {return "intentry"}
989 tagentry {return "tagentry"}
990 radiogroup {return "radiogroup"}
991 checkgroup {return "checkgroup"}
993 form {return "subform"}
994 tablelist {return "tablelist"}
997 if {[info exists tlc::Form::custom_types($type)]} {
1000 error "Unknown type: ($type)"
1007 body tlc::Form::cleanup {} { #<<<1
1008 #log warning "$w:[tlc::stackdump]"
1010 array unset valid_toggles
1016 body tlc::Form::widget_destroyed {} { #<<<1
1019 #log warning "$w done"