File: record.tcl

package info (click to toggle)
moomps 4.6-2
  • links: PTS
  • area: main
  • in suites: sarge
  • size: 2,444 kB
  • ctags: 2,307
  • sloc: tcl: 34,882; sh: 167; makefile: 91
file content (516 lines) | stat: -rw-r--r-- 33,674 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
# copyright (C) 1997-2005 Jean-Luc Fontaine (mailto:jfontain@free.fr)
# this program is free software: please read the COPYRIGHT file enclosed in this package or use the Help Copyright menu

# $Id: record.tcl,v 2.68 2005/01/02 00:45:07 jfontain Exp $


class record {

    proc record {this args} switched {$args} {
        switched::complete $this
    }

    proc ~record {this} {
        if {[info exists ($this,root)]} {
            dom::destroy $($this,root)                                                                           ;# cleanup XML data
        }
    }

    proc options {this} {
        return [list\
            [list -file {} {}]\
        ]
    }

    proc set-file {this value} {}

if {$global::withGUI} {                                                                                      ;# used only for saving

    # flag viewers requiring initialization configuration special treatment for options that actually are lists:
    array set series {
        ::store,comments {} ::thresholds,addresses {} ::dataTable,columnwidths {} ::freeText,cellindices {}
        ::summaryTable,cellrows {} ::summaryTable,columns {} ::summaryTable,columnwidths {} ::data2DPieChart,cellcolors {}
        ::data3DPieChart,cellcolors {} ::dataGraph,cellcolors {} ::dataStackedGraph,cellcolors {} ::dataBarChart,cellcolors {}
        ::dataSideBarChart,cellcolors {} ::dataStackedBarChart,cellcolors {} ::dataOverlapBarChart,cellcolors {}
        ::formulas::table,cellindexes {} ::formulas::table,cells {} ::formulas::table,rows {}
    }
    # Note: this method is more appropriate than using special names or characters in configuration switches, since that would imply
    # that the object would have to know that its configuration is to be saved in a special way. It is better to assume that the
    # code responsible for saving data knows about the nature of the data to be saved.
    # Note: all viewers options with switches ending with "text" are stored in text form so that embedded new lines are preserved

    # Warning: options with name ending with "text" or "data" have a special treatment
    proc write {this} {                     ;# save current configuration in XML form (synchronize code with currentConfiguration{})
        variable series

        if {[string length $switched::($this,-file)] == 0} {
            error {-file option undefined}
        }
        set document [dom::create]
        set root [dom::document createElement $document moodssConfiguration]
        dom::document createTextNode [dom::document createElement $root version] $global::applicationVersion
        set seconds [clock seconds]
        dom::document createTextNode [dom::document createElement $root date] [clock format $seconds -format %D]
        dom::document createTextNode [dom::document createElement $root time] [clock format $seconds -format %T]
        set node [dom::document createElement $root configuration]
        foreach name [configuration::variables 0] {
            if {[string equal $name viewerColors]} continue                                                            ;# skip lists
            dom::element setAttribute $node $name [set ::global::$name]
        }
        nodeFromList $node viewerColors $::global::viewerColors                                                    ;# list of colors
        # main window coordinates are not saved as it would be bad manners to force initial window placement
        # use main window size to ignore tool bar presence interference:
        dom::document createTextNode [dom::document createElement $root width] [winfo width $widget::($global::scroll,path)]
        dom::document createTextNode [dom::document createElement $root height] [winfo height $widget::($global::scroll,path)]
        dom::document createTextNode [dom::document createElement $root pollTime] $global::pollTime
        if {[info exists databaseInstances::singleton]} {                                                   ;# database history mode
            set node [dom::document createElement $root databaseRange]
            foreach {from to} [databaseInstances::cursorsRange] {}
            dom::element setAttribute $node from $from
            dom::element setAttribute $node to $to
            set node [dom::document createElement $root databaseViewer]
            set path $widget::($databaseInstances::singleton,path)
            foreach {x y width height} [canvasWindowManager::getGeometry $global::windowManager $path] {}
            foreach {xIcon yIcon} [canvasWindowManager::iconCoordinates $global::windowManager $path] {}
            dom::element setAttribute $node x $x; dom::element setAttribute $node y $y
            dom::element setAttribute $node width $width; dom::element setAttribute $node height $height
            dom::element setAttribute $node xIcon $xIcon; dom::element setAttribute $node yIcon $yIcon
        }
        set modules [dom::document createElement $root modules]
        foreach instance $modules::(instances) {                             ;# note: in modules list, modules are in creation order
            if {[string equal $modules::instance::($instance,module) formulas]} {
                continue                                                  ;# formulas modules are created by formulas tables viewers
            }
            set namespace $modules::instance::($instance,namespace)
            set module [dom::document createElement $modules module]
            dom::element setAttribute $module namespace $namespace
            dom::document createTextNode [dom::document createElement $module arguments] $modules::instance::($instance,arguments)
            set tables [dom::document createElement $module tables]
            foreach table $dataTable::(list) {                                 ;# note: in tables list, tables are in creation order
                # filter other module tables
                if {![string equal $namespace [namespace qualifiers [composite::cget $table -data]]]} continue
                foreach {x y width height} [canvasWindowManager::getGeometry $global::windowManager $widget::($table,path)] {}
                set level [canvasWindowManager::getStackLevel $global::windowManager $widget::($table,path)]
                foreach {xIcon yIcon} [canvasWindowManager::iconCoordinates $global::windowManager $widget::($table,path)] {}
                # note: icon coordinates are empty if table is not minimized
                set node [dom::document createElement $tables table]
                dom::element setAttribute $node x $x; dom::element setAttribute $node y $y
                dom::element setAttribute $node width $width; dom::element setAttribute $node height $height
                dom::element setAttribute $node level $level
                dom::element setAttribute $node xIcon $xIcon; dom::element setAttribute $node yIcon $yIcon
                set list [dataTable::initializationConfiguration $table]
                if {[llength $list] > 0} {
                    set options [dom::document createElement $node configuration]
                    foreach {switch value} $list {
                        set switch [string trimleft $switch -]      ;# remove heading dash (invalid name start) and restore it later
                        if {[info exists series(::dataTable,$switch)]} {                                    ;# it is actually a list
                            nodeFromList $options $switch $value                                      ;# so store as an encoded list
                        } else {
                            dom::element setAttribute $options $switch $value
                        }
                    }
                }
            }
        }
        set viewers [dom::document createElement $root viewers]
        foreach viewer $viewer::(list) {                                     ;# note: in viewers list, viewers are in creation order
            if {![viewer::saved $viewer]} continue                                               ;# viewer does not want to be saved
            set node [dom::document createElement $viewers viewer]
            set class [classof $viewer]
            dom::element setAttribute $node class $class
            if {[viewer::manageable $viewer]} {      ;# some viewers, such as the thresholds viewer, handle their display themselves
                foreach {x y width height} [canvasWindowManager::getGeometry $global::windowManager $widget::($viewer,path)] {}
                set level [canvasWindowManager::getStackLevel $global::windowManager $widget::($viewer,path)]
                dom::element setAttribute $node x $x; dom::element setAttribute $node y $y
                dom::element setAttribute $node width $width; dom::element setAttribute $node height $height
                dom::element setAttribute $node level $level
                foreach {xIcon yIcon} [canvasWindowManager::iconCoordinates $global::windowManager $widget::($viewer,path)] {}
                if {[string length $xIcon] > 0} {                                        ;# iconfied viewer (such as formulas table)
                    dom::element setAttribute $node xIcon $xIcon; dom::element setAttribute $node yIcon $yIcon
                }
            }
            nodeFromList $node cells [viewer::cells $viewer]                                                        ;# list of cells
            set list [viewer::initializationConfiguration $viewer]
            if {[llength $list] > 0} {
                catch {unset configurationNode}
                foreach {switch value} $list {
                    set switch [string trimleft $switch -]          ;# remove heading dash (invalid name start) and restore it later
                    if {[string equal $switch configurations]} {
                        # some viewers, such as thresholds, need to save several arguments lists instead of just one, so they pass
                        # them as a list of lists under the -configurations switch, which is reserved for that particular usage
                        foreach sublist $value {                    ;# use one entry per list otherwise it looks ugly in a XML sense
                            # use configurations reserved word as a flag so the list of lists can be regenerated at read time:
                            set options [dom::document createElement $node configurations]
                            foreach {switch value} $sublist {
                                set switch [string trimleft $switch -]                   ;# remove heading dash and restore it later
                                if {[info exists series($class,$switch)]} {                                 ;# it is actually a list
                                    nodeFromList $options $switch $value                              ;# so store as an encoded list
                                } else {                                     ;# handle text with embedded new lines and base 64 data
                                    switch -glob [string tolower $switch] {
                                        *text {dom::document createTextNode [dom::document createElement $options $switch] $value}
                                        *data\
                                            {dom::document createCDATASection [dom::document createElement $options $switch] $value}
                                        default {dom::element setAttribute $options $switch $value}
                                    }
                                }
                            }
                        }
                    } else {
                        if {![info exists configurationNode]} {
                            set configurationNode [dom::document createElement $node configuration]
                        }
                        set options $configurationNode
                        if {[info exists series($class,$switch)]} {                                         ;# it is actually a list
                            nodeFromList $options $switch $value                                      ;# so store as an encoded list
                        } else {                                             ;# handle text with embedded new lines and base 64 data
                            switch -glob [string tolower $switch] {
                                *text {dom::document createTextNode [dom::document createElement $options $switch] $value}
                                *data {dom::document createCDATASection [dom::document createElement $options $switch] $value}
                                default {dom::element setAttribute $options $switch $value}
                            }
                        }
                    }
                }
            }
        }
        set images [dom::document createElement $root images]
        foreach {file format data} [images::values] {
            set node [dom::document createElement $images image]
            dom::element setAttribute $node file $file
            dom::element setAttribute $node format $format
            dom::document createCDATASection $node \n$data\n                                             ;# save in a separate block
        }
        set file [open $switched::($this,-file) w+]                                                           ;# create or overwrite
        dom::document configure $document -encoding [fconfigure $file -encoding]                  ;# use fine automatic Tcl encoding
        set data [serialize $document]
        dom::destroy $root                                                                                                ;# cleanup
        puts $file $data
        close $file
    }

}

    proc read {this} {
        if {[string length $switched::($this,-file)] == 0} {
            error {-file option undefined}
        }
        if {[catch {set file [open $switched::($this,-file)]} message]} {
            puts stderr $message
            exit 1
        }
        set line [gets $file]                                                                                 ;# retrieve first line
        seek $file 0                                                                                                       ;# rewind
        if {[catch {set ($this,root) [dom::parse [::read $file]]} message]} {
            puts stderr "file $switched::($this,-file) is not a valid moodss configuration file:\n$message"
            exit 1
        }
        close $file
        # moodss before 19.0 saved namespace of the first instance of a module without its number suffix (<0>)
        set ($this,convertNamespaces) [expr {[package vcompare [version $this] 19.0] < 0}]
    }

    proc modules {this} {
        set list {}
        foreach node [dom::selectNode $($this,root) /moodssConfiguration/modules/module] {   ;# modules were saved in creation order
            set namespace [dom::element getAttribute $node namespace]
            if {$($this,convertNamespaces)} {
                foreach {name index} [modules::decoded $namespace] {}
                if {[string length $index] == 0} {append namespace <0>}                                     ;# non indexed namespace
            }
            lappend list $namespace
        }
        return $list
    }

    proc modulesWithArguments {this {validateCommand {}}} {  ;# validate command allows filtering out some modules, such as instance
        set list {}
        foreach node [dom::selectNode $($this,root) /moodssConfiguration/modules/module] {
            # not evaluated because namespace may contain interpreted characters, such as ;, $, ...:
            set namespace [dom::element getAttribute $node namespace]
            if {([string length $validateCommand] > 0) && ![uplevel #0 $validateCommand $namespace]} continue
            lappend list $namespace
            eval lappend list [dom::node stringValue [dom::selectNode $node arguments]]
        }
        return $list                                            ;# format: module [-option [value] -option ...] module [-option ...]
    }

    proc pollTime {this} {
        return [dom::node stringValue [dom::selectNode $($this,root) /moodssConfiguration/pollTime]]
    }

    proc sizes {this} {
        return [list\
            [dom::node stringValue [dom::selectNode $($this,root) /moodssConfiguration/width]]\
            [dom::node stringValue [dom::selectNode $($this,root) /moodssConfiguration/height]]\
        ]
    }

    # Note: all viewers options with switches ending with "text" were stored in text form so that embedded new lines are preserved,
    # so apply corresponding special processing.
    # Warning: options with name ending with "text" or "data" have a special treatment
    proc viewersData {this} {
        set list {}
        foreach viewerNode [dom::selectNode $($this,root) /moodssConfiguration/viewers/viewer] {
            set class [dom::element getAttribute $viewerNode class]
            if {$($this,convertNamespaces)} {
                set cells [convertedCells [listFromNode $viewerNode cells]]
            } else {
                set cells [listFromNode $viewerNode cells]
            }
            # note: coordinates, sizes and level may be empty (for thresholds viewer for example)
            lappend list $class $cells [dom::element getAttribute $viewerNode x] [dom::element getAttribute $viewerNode y]\
                [dom::element getAttribute $viewerNode width] [dom::element getAttribute $viewerNode height]\
                [dom::element getAttribute $viewerNode level] [dom::element getAttribute $viewerNode xIcon]\
                [dom::element getAttribute $viewerNode yIcon]    ;# note: icon attributes values returned empty if they do not exist
            set options {}                                                                 ;# in case configuration(s) is(are) empty
            set node [dom::selectNode $viewerNode configuration]
            if {[string length $node] > 0} {                                                          ;# simple viewer configuration
                foreach {name value} [array get [dom::node cget $node -attributes]] {
                    if {$($this,convertNamespaces)} {                      ;# single cell type option for data graphs, bars and pies
                        switch $name totalcell - ymaximumcell {set value [converted $value]}
                    }
                    lappend options -$name $value                                       ;# heading dashes were stripped at save time
                }
                foreach node [dom::selectNode $node *] {                ;# if there are children, they are list or text type options
                    set name [dom::node cget $node -nodeName]                 ;# handle text with embedded new lines or base 64 data
                    switch -glob [string tolower $name] {
                        *text - *data {lappend options -$name [dom::node stringValue $node]}
                        default {lappend options -$name [listFromNode $node]}
                    }
                }
            }
            set nodes [dom::selectNode $viewerNode configurations]
            if {[llength $nodes] > 0} {                             ;# viewer (such as thresholds) with multiple configuration lists
                set lists {}
                foreach node $nodes {
                    set append {}
                    foreach {name value} [array get [dom::node cget $node -attributes]] {
                        lappend append -$name $value                                    ;# heading dashes were stripped at save time
                    }
                    foreach node [dom::selectNode $node *] {                         ;# if there are children, they are list options
                        set name [dom::node cget $node -nodeName]                    ;# text with embedded new lines or base 64 data
                        switch -glob [string tolower $name] {
                            *text - *data {lappend append -$name [dom::node stringValue $node]}
                            default {
                                if {\
                                    $($this,convertNamespaces) &&\
                                    [string equal $class ::formulas::table] && [string equal $name cells]\
                                } {
                                    lappend append -$name [convertedCells [listFromNode $node]]
                                } else {
                                    lappend append -$name [listFromNode $node]
                                }
                            }
                        }
                    }
                    lappend lists $append
                }
                lappend options -configurations $lists
            }                                                               ;# else there may not be any switched configuration data
            lappend list $options
        }
        return $list
    }

    proc tableNode {this namespace creationIndex} {                             ;# index is module data table creation index, from 0
        if {$($this,convertNamespaces) && [string match *<0> $namespace]} {
            # remove trailing namespace index for reading recorded data of the first instance of a module, saved without index
            regsub {<0>$} $namespace {} namespace
        }
        set node [dom::selectNode $($this,root) /moodssConfiguration/modules/module\[@namespace=\"$namespace\"\]]
        if {[string length $node] == 0} {error {internal error: please report to author}}
        # note: table entry may not exist if new views were added to module after the file was saved
        return [lindex [dom::selectNode $node tables/table] $creationIndex]                    ;# tables are saved in creation order
    }

    proc tableWindowManagerData {this namespace creationIndex} {       ;# index is module data table creation index, starting with 0
        if {[string length [set node [tableNode $this $namespace $creationIndex]]] == 0} {
            return {}
        }
        array set data [array get [dom::node cget $node -attributes]]
        return [list $data(x) $data(y) $data(width) $data(height) $data(level) $data(xIcon) $data(yIcon)]
    }

    proc tableOptions {this namespace creationIndex} {                 ;# index is module data table creation index, starting with 0
        if {[string length [set node [tableNode $this $namespace $creationIndex]]] == 0} {
            return {}
        }
        set options {}
        set node [dom::selectNode $node configuration]
        if {[string length $node] > 0} {                                               ;# some switched configuration data was saved
            foreach {name value} [array get [dom::node cget $node -attributes]] {
                lappend options -$name $value                                           ;# heading dashes were stripped at save time
            }
            foreach node [dom::selectNode $node *] {                                 ;# if there are children, they are list options
                lappend options -[dom::node cget $node -nodeName] [listFromNode $node]
            }
        }
        return $options
    }

    proc configurationData {this} {                                                             ;# return a global name / value list
        set node [dom::selectNode $($this,root) /moodssConfiguration/configuration]
        set list [array get [dom::node cget $node -attributes]]
        lappend list viewerColors [listFromNode $node viewerColors]
        return $list
    }

    proc version {this} {                                      ;# return the version of the application that generated the save file
        return [dom::node stringValue [dom::selectNode $($this,root) /moodssConfiguration/version]]
    }

    proc databaseRange {this} {                                               ;# return a list of 2 integers: from and to in seconds
        set node [dom::selectNode $($this,root) /moodssConfiguration/databaseRange]
        if {[string length $node] == 0} {return {}}                                       ;# that must be a real-time type dashboard
        array set data [array get [dom::node cget $node -attributes]]
        return [list $data(from) $data(to)]
    }

    proc databaseViewerWindowManagerData {this} {
        set node [dom::selectNode $($this,root) /moodssConfiguration/databaseViewer]
        if {[string length $node] == 0} {return {}}                                       ;# that must be a real-time type dashboard
        array set data [array get [dom::node cget $node -attributes]]
        return [list $data(x) $data(y) $data(width) $data(height) $data(xIcon) $data(yIcon)]
    }

    proc converted {cell} {      ;# convert cell with non indexed namespace (moodss before 19.0) to cell with namespace indexed at 0
        if {[string length $cell] == 0} {return {}}
        viewer::parse $cell array row column ignore
        set namespace [namespace qualifiers $array]
        foreach {name index} [modules::decoded $namespace] {}
        if {[string length $index] == 0} {                                                                  ;# non indexed namespace
            set cell $namespace<0>::[namespace tail $array]($row,$column)
        }
        return $cell
    }
    proc convertedCells {list} {
        set cells {}
        foreach cell $list {lappend cells [converted $cell]}
        return $cells
    }

if {$global::withGUI} {                                                                                      ;# used only for saving

    proc imagesData {this} {                                                                       ;# note: used from moodss 18.2 on
        set list {}
        foreach node [dom::selectNode $($this,root) /moodssConfiguration/images/image] {
            lappend list [dom::element getAttribute $node file] [string trim [dom::node stringValue $node]]     ;# remove formatting
            dom::destroy $node                                                                        ;# free potentially big memory
        }
        return $list
    }

    # Warning: options with name ending with "data" have a special treatment
    proc currentConfiguration {} {       ;# current configuration in a high performance data storage (synchronize code with write{})
        set root [new container]
        # ignore version, data and time which always change between snapshots
        container::bind $root [set container [new container configuration]]
        foreach name [configuration::variables 0] {
            container::set $container $name [set ::global::$name]
        }
        container::set $root width [winfo width $widget::($global::scroll,path)]
        container::set $root height [winfo height $widget::($global::scroll,path)]
        container::set $root pollTime $global::pollTime
        if {[info exists databaseInstances::singleton]} {                                                   ;# database history mode
            container::bind $root [set container [new container databaseRange]]
            foreach {from to} [databaseInstances::cursorsRange] {}
            container::set $container from $from
            container::set $container to $to
            container::bind $root [set container [new container databaseViewer]]
            set path $widget::($databaseInstances::singleton,path)
            foreach {x y width height} [canvasWindowManager::getGeometry $global::windowManager $path] {}
            foreach {xIcon yIcon} [canvasWindowManager::iconCoordinates $global::windowManager $path] {}
            container::set $container x $x; container::set $container y $y
            container::set $container width $width; container::set $container height $height
            container::set $container xIcon $xIcon; container::set $container yIcon $yIcon
        }
        container::bind $root [set modules [new container modules]]
        foreach instance $modules::(instances) {                             ;# note: in modules list, modules are in creation order
            set namespace $modules::instance::($instance,namespace)
            container::bind $modules [set module [new container module]]
            container::set $module namespace $namespace
            container::set $module arguments $modules::instance::($instance,arguments)
            container::bind $module [set tables [new container tables]]
            foreach table $dataTable::(list) {                                 ;# note: in tables list, tables are in creation order
                # filter other module tables
                if {![string equal $namespace [namespace qualifiers [composite::cget $table -data]]]} continue
                foreach {x y width height} [canvasWindowManager::getGeometry $global::windowManager $widget::($table,path)] {}
                set level [canvasWindowManager::getStackLevel $global::windowManager $widget::($table,path)]
                foreach {xIcon yIcon} [canvasWindowManager::iconCoordinates $global::windowManager $widget::($table,path)] {}
                # note: icon coordinates are empty if table is not minimized
                container::bind $tables [set container [new container table]]
                container::set $container x $x; container::set $container y $y
                container::set $container width $width; container::set $container height $height
                container::set $container level $level
                container::set $container xIcon $xIcon; container::set $container yIcon $yIcon
                set list [dataTable::initializationConfiguration $table]
                if {[llength $list] > 0} {
                    container::bind $container [set options [new container configuration]]
                    foreach {switch value} $list {
                        container::set $options $switch $value
                    }
                }
            }
        }
        container::bind $root [set viewers [new container viewers]]
        foreach viewer $viewer::(list) {                                     ;# note: in viewers list, viewers are in creation order
            if {![viewer::saved $viewer]} continue                                               ;# viewer does not want to be saved
            container::bind $viewers [set container [new container viewer]]
            container::set $container class [classof $viewer]
            if {[viewer::manageable $viewer]} {      ;# some viewers, such as the thresholds viewer, handle their display themselves
                foreach {x y width height} [canvasWindowManager::getGeometry $global::windowManager $widget::($viewer,path)] {}
                set level [canvasWindowManager::getStackLevel $global::windowManager $widget::($viewer,path)]
                container::set $container x $x; container::set $container y $y
                container::set $container width $width; container::set $container height $height
                container::set $container level $level
                foreach {xIcon yIcon} [canvasWindowManager::iconCoordinates $global::windowManager $widget::($viewer,path)] {}
                if {[string length $xIcon] > 0} {                                        ;# iconfied viewer (such as formulas table)
                    container::set $container xIcon $xIcon; container::set $container yIcon $yIcon
                }
            }
            container::set $container cells [viewer::cells $viewer]
            set list [viewer::initializationConfiguration $viewer]
            if {[llength $list] > 0} {
                container::bind $container [set options [new container configuration]]
                foreach {switch value} $list {
                    if {[string match -nocase *data $switch]} continue                                          ;# skip base 64 data
                    if {[string equal $switch -configurations]} {
                        foreach list $value {
                            container::bind $options [set configurations [new container configurations]]
                            foreach {switch value} $list {
                                container::set $configurations $switch $value
                            }
                        }
                    } else {
                        container::set $options $switch $value
                    }
                }
            }
        }
        container::bind $root [set images [new container images]]
        foreach file [images::names] {                                                                          ;# ignore image data
            container::bind $images [set container [new container image]]
            container::set $container file $file
        }
        return $root
    }

    proc snapshot {} {                                                                             ;# remember current configuration
        if {[info exists (data)]} {delete $(data)}
        set (data) [currentConfiguration]
    }

    proc changed {} {                                                        ;# see if configuration has changed since last snapshot
        if {[info exists (data)]} {
            set container [currentConfiguration]
            set equal [container::equal $(data) [currentConfiguration]]
            delete $container
            return [expr {!$equal}]
        } else {    ;# snapshot was not yet taken, assume there was no change (can happen when user closes window right after start)
            return 0
        }
    }

}

}