# copyright (C) 1997-2006 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


# various procedures mostly used in moodss.tcl

# $Id: procs.tcl,v 1.119 2006/09/27 21:28:08 jfontain Exp $


set dialogBoxErrorIcon [image create photo -data {
    R0lGODlhIAAgAOfJAIkAAIoAAIsAAIwAAI4AAI8AAJAAAJEAAJIAAJQAAJUAAJYAAJcAAJMCApgAAJkAAJoAAJsAAJwAAJMEBJ0AAJ4AAJ8AAKAAAKEAAKMA
    AJcGBqUAAKYAAKcAAKgAAKkAAKoAAKsAAKwAAJ8GBq0AAK4AAK8AALEAAKEHB7IAAKkEBLMAAJwKCrQAALUAALYAALcAALgAALkAALUCAroAAKELC7sAALwA
    AL0AAL4AAL8AAMAAAMEAAKoKCsIAAMMAAMQAAMUAAMYAAMgAAMkAAMoAAMsAAMwAALwHB80AAM4AANAAALcLC9UAANgAANoAANwAAN4AAN8AALcSEs0JCeIA
    AK0XF64XF68XF9AJCeYAALEXF+4AAN0JCfIAAMoSEvQAAPcAANcODuoGBvoAAPkBAf4AAP0BAf8CAvkICP8HB+4VFfkREf8QEMcpKfUWFv8SEvEZGfQYGM0p
    Kc4pKfUZGesfH9YpKfkaGv8dHfUiIvkiIu8oKP8iIvgmJvomJuouLvcpKf8oKPcvL/gwMPowMPE2NvQ2Nv8zM/80NPM6Ov81Ne0/P/M+Pu5DQ+pFRexFRf8/
    P/9FRf9KSv9RUftTU/dWVvpVVftVVf9WVvdcXP9nZ/hra/9sbPVxcfZ0dPl0dP5ycvxzc/9ycvl7e/95ef+Kiv6QkP+amvqfn/yfn/2fn/6fn/+fn/6goP+g
    oPykpP+jo/+np/ysrP+rq/+trfyvr/+xsf+4uP+8vP++vv6/v//Bwf/Fxf/Hx//Kyv/MzP3Nzf7Nzf/Ozv/U1P/b2//c3P/h4f/r6///////////////////
    ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////////yH5BAEKAP8ALAAAAAAgACAAAAj+AP8JHEiwoMGD
    CBEKCfLDxw4dOGwknCiQyJCFQpQkOWJEB40YMFxQNFhkyBEodjSRWkmKE6AmOFqkGCnQSBEjUizZQsazJ89goMS4MEGCos0lfGAZW8q06VJhjmiIAJGwSBEn
    jXYB28q1q1dPQT50QFhkiaJcaH+hXcs2l1q0n1xwOEjEyBpXtGgBS3Ysr1+/x5IB88voQ4aCFqWIQoWKV7LHxRhLRlXscTJejE9lwXCBoEk9pky9svyYWOjQ
    xEgnexX6EogKBIVEqVSq9l7Sw2oPUw0sFKZBcoZIGLhwzKbjx3up9uVL9axAXqJ7qRLhgUAgQt5k2s5dl2rVqaT+i7fgQOAPJXskqV8vCdf3x+HFSy+xQCCP
    I3gQ6d+v/9b3+PJFR0ICAu1gBBt9JKhggrX8F6B0IiAgUA5FpNHGhRi2Ict7yaQCxocgfhjCAQLdoEMZaKSo4mgcJqNKGDDGGAYDBghkgwxcnKGjjq6otsoq
    PpIhpJBaFEDAQDE8YcaSZrCiWitLtqIaK0yaIcSRA72QgxpcLqJaLFxyGYtqi4QpwQAEtbBCF3C02YllsrQpJxwbPtaJnFQMIEBBJ8xQRx6AjpIMLYAWWigt
    yYxSaBwNBHBQCUj8IciklExq6aWCVDqpHyg4epAIIXxRSCKklmrqqYkQMkUAACT0gQdOTBwSyay01lqrITWwShEHG6hwxyTABisssHNMoCtNnPVAByTccfeI
    GywM4ClNAllAQQQQjLAFFldYoYGR0lJ70AMMKJAAAgcYKe66NAUEADs=
}]

proc loadFromFile {name} {                                         ;# possibly unload all existing modules and load from a save file
    lifoLabel::push $global::messenger [format [mc {loading dashboard from %s...}] $name]
    ::update idletasks
    clearModulesAndViewers
    set global::saveFile $name
    set global::fileDirectory [file dirname $name]
    set initializer [new record -file $name]
    record::read $initializer
    configuration::load [record::configurationData $initializer]                                 ;# set global values from save file
    modules::parse 1 [record::modulesWithArguments $initializer]      ;# recursive; modules are supposed to be valid if in dashboard
    set modules::(initialized) [record::modules $initializer]
    lifoLabel::pop $global::messenger
    return $initializer
}

proc clearModulesAndViewers {} {
    foreach viewer $viewer::(list) {                                         ;# delete all existing viewers except thresholds viewer
        set class [classof $viewer]
        switch $class {
            ::store - ::store::dialog - ::thresholdLabel - ::thresholds {
                ${class}::reset $viewer                                         ;# make sure to remove now obsolete existing entries
            }
            default {
                delete $viewer       ;# note: in formulas and peer tables cases, that also unloads the corresponding formulas module
            }
        }
    }
    if {[info exists databaseInstances::singleton]} {
        delete $databaseInstances::singleton
    }
    foreach instance [modules::instancesWithout [list formulas peer]] {
        dynamicallyUnloadModule $modules::instance::($instance,namespace)
    }
    if {[llength [modules::instancesWithout]] > 0} {
        error {internal moodss error: please report to author with error trace}
    }
}

proc clear {} {                  ;# return true if user went ahead and cleared main window or return false in case of change in mind
    static busy 0

    if {$busy} return             ;# protection against overlapping invocations from fast clicking, as a lot of updates occur within
    if {[needsSaving]} {                             ;# see if there are changes that the user may want to be saved before reloading
        switch [inquireSaving] {
            yes {
                save
                if {[needsSaving]} {return 0}                        ;# data was not saved: assume the user wants to abort reloading
            }
            cancel {return 0}
        }
    }
    set busy 1
    if {[info exists ::initializer]} {
        delete $::initializer
        unset ::initializer
    }
    clearModulesAndViewers
    databaseConnection 0                                                                                 ;# disconnect from database
    set global::saveFile {}
    updateFileSaveHelp {}                                                                      ;# to reflect save file disappearance
    updateTitle
    updateMenuWidget
    updateToolBar
    updateDragAndDropZone
    configuration::load [preferences::read]                                                             ;# reinitialize from rc file
    record::snapshot   ;# take a snapshot of new configuration os that user is not asked to save wnen trying to open a new file next
    set busy 0
    return 1
}

proc reload {} {                                                                      ;# restart by loading modules from a save file
    if {[needsSaving]} {                                    ;# there are changes that the user may want to be saved before reloading
        switch [inquireSaving] {
            yes {
                save
                if {[needsSaving]} return                            ;# data was not saved: assume the user wants to abort reloading
            }
            cancel return
        }
    }
    set file [tk_getOpenFile\
        -title [mc {moodss: Open}] -initialdir $global::fileDirectory -defaultextension .moo\
        -filetypes [list [list [mc {moodss dashboard}] .moo]]\
    ]
    if {[string length $file] == 0} return                                                                  ;# user canceled loading
    databaseConnection 0                                                                                 ;# disconnect from database
    set global::fileDirectory [file dirname $file]
    if {[info exists ::initializer]} {
        delete $::initializer
        unset ::initializer
    }
    updateCanvasImage {}; set global::canvasImageFile {}                                       ;# possibly clear existing background
    set ::initializer [loadFromFile $file]
    $global::canvas configure -background $global::canvasBackground
    wm geometry . {}
    foreach {width height} [record::sizes $::initializer] {}                                                 ;# used stored geometry
    composite::configure $global::scroll -width $width -height $height
    updateCanvasImage $global::canvasImageFile 1
    modules::initialize 0 initializationErrorMessageBox
    modules::setPollTimes [record::pollTime $::initializer]
    foreach instance [modules::instancesWithout [list formulas peer]] {
        displayModule $instance $::draggable
    }
    # so that data names are generated as when first started:
    summaryTable::reset; currentValueTable::reset; formulas::table::reset; peer::table::reset
    createSavedImages $::initializer     ;# note: existing images, if any, were deleted at the same time as their associated viewers
    createSavedViewers $::initializer
    if {[pages::current] == 0} {                                                                                         ;# no pages
        manageScrolledCanvas 1
    } else {
        pages::manageScrolledCanvas 1
    }
    updateTitle
    updateMenuWidget
    updateToolBar
    updateDragAndDropZone
    updateFileSaveHelp $file                                                                  ;# since current save file has changed
    updateCanvasImagesPosition
    refresh                                                                               ;# make sure data is immediately displayed
    update                                               ;# required so that table and viewer windows sizes are correct for snapshot
    record::snapshot                       ;# take a snap shot of new configuration so any future changes are detected as meaningful
}

proc createNewCellsViewer {class {cells {}}} {
    set viewer [viewer::new $class]
    if {$viewer == 0} {
        return 0
    }
    if {[viewer::view $viewer $cells]} {
        if {[viewer::manageable $viewer]} {
            if {$class eq "::predictor"} {
                manageViewer $viewer 1 -static $global::static -setwidth 550 -setheight 450 -dragobject $viewer
            } else {
                manageViewer $viewer 1 -static $global::static -dragobject $viewer
            }
        }
        if {$class eq "::canvas::iconic"} {
            foreach {left top right bottom} [$global::canvas cget -scrollregion] {}
            composite::configure $viewer -x $left -y $top               ;# place new icon at the top left corner of the current page
            canvas::viewer::flash $viewer                                         ;# in case it is hidden by other graphical objects
        }
        updateViewObjectsMenu
        return $viewer
    } else {                                                                   ;# do not create new viewer if cells cannot be viewed
        delete $viewer
        return 0
    }
}

proc createNewPeerViewer {export draggable static} {
    array set data [peer::dialog::configuration $export]
    if {[array size data] == 0} {return 0}                                                                         ;# user cancelled
    if {$export} {
        set viewer [new peer::table $global::canvas -draggable $draggable -port $data(port)]
    } else {
        set viewer [new peer::table $global::canvas -source $data(host) -draggable $draggable -port $data(port)]
    }
    manageViewer $viewer 1\
        -static $static -dragobject $viewer -title [peer::table::title $viewer] -tags $peer::table::($viewer,namespace)
    updateMenuWidget; updateToolBar                              ;# since peer viewer lives and dies with its associated peer module
    composite::configure $viewer -deletecommand "
        [composite::cget $viewer -deletecommand]
        after idle {updateMenuWidget; updateToolBar}                       ;# wait till viewer is actually removed from viewers list
    "
    modules::setPollTimes                           ;# since a module was just loaded and that is required by future related viewers
    updateViewObjectsMenu
    return $viewer
}

proc createNewFormulasViewer {object category draggable static} {
    set viewer [new formulas::table $global::canvas -draggable $draggable -object $object -category $category]
    manageViewer $viewer 1 -static $static -dragobject $viewer -title [formulas::table::title $viewer]
    updateViewObjectsMenu
    return $viewer
}

proc manageViewer {viewer destroyable args} {        ;# viewer or table, arguments are window manager configuration switched options
    set path $widget::($viewer,path)
    canvasWindowManager::manage $global::windowManager $path $viewer
    eval canvasWindowManager::configure $global::windowManager $path $args
    if {$destroyable} {
        composite::configure $viewer -deletecommand "canvasWindowManager::unmanage $global::windowManager $path"
    }
}

proc save {{ask 0}} {                           ;# save current configuration in user defined file or currently defined storage file
    if {$ask || ([string length $global::saveFile] == 0)} {
        set file [tk_getSaveFile\
            -title [mc {moodss: Save}] -initialdir $global::fileDirectory -defaultextension .moo\
            -filetypes [list [list [mc {moodss dashboard}] .moo]] -initialfile $global::saveFile\
        ]
        if {[string length $file] == 0} return                                                               ;# user canceled saving
        set global::saveFile $file
        set global::fileDirectory [file dirname $file]
        updateFileSaveHelp $file
    }
    lifoLabel::push $global::messenger [format [mc {saving in %s...}] $global::saveFile]
    update idletasks                                                                                 ;# make sure message is visible
    set record [new record -file $global::saveFile]
    set error [catch {record::write $record} message]
    lifoLabel::pop $global::messenger
    if {$error} {
        tk_messageBox -title [mc {moodss: Save}] -type ok -icon error -message $message
    }
    delete $record
    if {!$error} record::snapshot                                                                  ;# remember current configuration
}

proc refresh {} {
    static updateEvent

    catch {after cancel $updateEvent}                                                               ;# possibly cancel current event
    if {[llength $modules::(synchronous)] == 0} return                                                              ;# nothing to do
    foreach instance $modules::(synchronous) {
        set namespace $modules::instance::($instance,namespace)
        ${namespace}::update                                                                ;# ask module to update its dynamic data
    }
    foreach viewer $viewer::(list) {                                                                       ;# update formulas tables
        if {[string equal [classof $viewer] ::formulas::table]} {
            formulas::table::update $viewer                                                  ;# direct update bypassing viewer layer
        }
    }
    if {$global::pollTime > 0} {                                                                   ;# any synchronous modules loaded
        set updateEvent [after [expr {1000 * $global::pollTime}] refresh]                                 ;# convert to milliseconds
    }
}

# invoked by thresholds code when a threshold condition occurs (color and level are empty for resetting)
proc cellThresholdCondition {array row column color level summary} {    ;# summary is a short description of the threshold condition
    dataTable::cellThresholdCondition $array $row $column
    viewer::cellThresholdCondition $array $row $column $color $level $summary
}

proc inquireSaving {} {
    if {[string length $::global::saveFile] > 0} {                                                           ;# there is a save file
        set message [format [mc {There are unsaved configuration changes. Do you want them saved to file: %s?}] $::global::saveFile]
    } else {
        set message [mc {There are unsaved configuration changes. Do you want them saved to file?}]
    }
    array set answer {0 yes 1 no 2 cancel}
    return $answer([tk_dialog .saveorexit [mc {moodss: Save}] $message question 0 [mc Yes] [mc No] [mc Cancel]])
}

proc needsSaving {} {             ;# no need to save if there are no loaded modules (note: do not ignore peer tables, even if alone)
    return [expr {[record::changed] && ([llength [modules::instancesWithout formulas]] > 0)}]
}

proc manageToolBar {{save 1}} {                                                              ;# whether to save state in preferences
    set bar [updateToolBar]
    if {$global::showToolBar} {
        grid $bar -row 0 -column 0 -sticky we
    } else {
        grid forget $bar
    }
    if {$save} {
        preferences::update
    }
}

proc createSavedImages {record} {
    foreach {file data} [record::imagesData $record] {
        images::load $file {} $data
    }
}

proc createSavedViewers {record} {                                                                   ;# strictly viewers, not tables
    if {[llength [set range [record::databaseRange $record]]] > 0} {                               ;# saved in database history mode
        monitorDatabaseInstances $range                                                    ;# display loaded instance modules viewer
    }
    set viewers {}
    set data [record::viewersData $record]
    foreach {class cells x y width height level xIcon yIcon switchedOptions} $data {
        # process peer table viewers first since they create namespaces and thus data that other viewers (formulas, ...) may display
        if {![string equal $class ::peer::table]} continue
        set viewer [eval new ::peer::table $global::canvas $switchedOptions -draggable $::draggable]
        manageViewer $viewer 1 -static $global::static -setx $x -sety $y -setwidth $width -setheight $height -level $level\
            -dragobject $viewer -iconx $xIcon -icony $yIcon -title [peer::table::title $viewer]\
            -tags $peer::table::($viewer,namespace)                                                             ;# always manageable
        set viewerCells($viewer) $cells                                                                              ;# gather cells
        lappend viewers $viewer
    }
    if {[llength $viewers] > 0} {                              ;# there are some peer viewers therefore some peer modules now loaded
        modules::setPollTimes    ;# recalculate valid poll times in case there are only peer modules but do not change current value
    }
    foreach {class cells x y width height level xIcon yIcon switchedOptions} $data {
        # process formulas table viewers second since they create namespaces and thus data that other viewers may display
        if {![string equal $class ::formulas::table]} continue
        set viewer [eval new ::formulas::table $global::canvas $switchedOptions -draggable $::draggable]
        manageViewer $viewer 1 -static $global::static -setx $x -sety $y -setwidth $width -setheight $height -level $level\
            -dragobject $viewer -iconx $xIcon -icony $yIcon -title [formulas::table::title $viewer]             ;# always manageable
        set viewerCells($viewer) $cells                                                                              ;# gather cells
        lappend viewers $viewer
    }
    foreach {class cells x y width height level xIcon yIcon switchedOptions} $data {                    ;# process remaining viewers
        switch $class {
            ::formulas::table - ::peer::table continue
            ::store - ::thresholds {
                set viewer [set ${class}::singleton]         ;# store and thresholds viewers are special cases and are not displayed
                eval switched::configure $viewer $switchedOptions
            }
            ::thresholdLabel {
                set viewer [set ${class}::singleton]                ;# threshold label viewer is a special case and is not displayed
                eval composite::configure $viewer $switchedOptions
            }
            ::currentValueTable {                                 ;# needs to know mode (real time or database) at construction time
                set viewer [eval new currentValueTable\
                    $global::canvas $global::pollTime $switchedOptions -interval $global::pollTime -draggable $::draggable\
                ]
                manageViewer $viewer 1 -static $global::static -setx $x -sety $y -setwidth $width -setheight $height\
                    -level $level -dragobject $viewer
            }
            ::canvas::iconic {
                set viewer [eval new $class $global::canvas $switchedOptions -draggable $::draggable -static $global::static]
            }
            ::page {
                set viewer [eval new $class $global::canvas $switchedOptions -draggable $::draggable]
                set background {}
                foreach {switch value} $switchedOptions {
                    if {[string equal $switch -background]} {
                        set background $value
                        break
                    }
                }
                if {[string length $background] == 0} {                                                   ;# pre-18.4 dashboard file
                    composite::configure $viewer -background $global::canvasBackground                     ;# use default background
                }
            }
            default {
                set viewer [eval new $class $global::canvas $switchedOptions -draggable $::draggable]
                foreach list [composite::configure $viewer] {
                    if {[string equal [lindex $list 0] -interval]} {                              ;# viewer supports interval option
                        composite::configure $viewer -interval $global::pollTime                          ;# so use current interval
                        break                                                                                                ;# done
                    }
                }
                if {[viewer::manageable $viewer]} {
                    manageViewer $viewer 1 -static $global::static -setx $x -sety $y -setwidth $width -setheight $height\
                        -level $level -dragobject $viewer
                }
            }
        }
        set viewerCells($viewer) $cells                                                                              ;# gather cells
        lappend viewers $viewer
    }
    # monitor cells now that all viewers exist (for example, summary tables have their own data and thus need be created before
    # other viewers can reference them), and in order of creation (really important for peer and formulas viewers)
    foreach viewer $viewers {
        viewer::view $viewer $viewerCells($viewer)
    }
}

# must be invoked only when the application is running, that is after all the save file and command line modules have been loaded
proc dynamicallyLoadModules {relax arguments} {  ;# arguments list format: module [-option [value] -option ...] module [-option ...]
    set instances [modules::instancesWithout [list formulas peer]]
    modules::parse $relax $arguments
    modules::initialize                                               ;# initializes only modules that have not yet been initialized
    modules::setPollTimes                                            ;# recalculate valid poll times but do not change current value
    set first 1
    foreach instance [modules::instancesWithout [list formulas peer]] {
        if {[lsearch -exact $instances $instance] >= 0} continue
        # new module or instance:
        if {$first} {                ;# reset next module table coordinates so that successively loaded modules do not go off screen
            displayModule $instance $::draggable 1
            set first 0
        } else {
            displayModule $instance $::draggable
        }
    }
    viewer::updateInterval $global::pollTime
    updateTitle
    updateMenuWidget
    updateToolBar
    refresh                                                                               ;# make sure data is immediately displayed
}

proc dynamicallyUnloadModule {namespace} {
    foreach instance [modules::instancesWithout [list formulas peer]] {
        if {[string equal $modules::instance::($instance,namespace) $namespace]} break                                      ;# found
    }
    if {[lindex $modules::instance::($instance,times) 0] >= 0} {                                    ;# then if module is synchronous
        ldelete modules::(synchronous) $instance                                                              ;# remove it from list
    }
    foreach table $dataTable::(list) {                                                                      ;# delete related tables
        if {[string equal [modules::namespaceFromArray [composite::cget $table -data]] $namespace]} {
            canvasWindowManager::unmanage $global::windowManager $widget::($table,path)
            delete $table
        }
    }
    modules::instance::empty $instance                             ;# empty module data so that related viewers can show empty cells
    modules::unload $instance
    modules::setPollTimes                                            ;# recalculate valid poll times but do not change current value
    updateTitle
    updateMenuWidget
    updateToolBar
}

proc residentTraceModule {display} {                                                     ;# initialize display resident trace module
    if {![winfo exists .trace]} {
        toplevel .trace
        wm withdraw .trace
        wm group .trace .                                            ;# for proper window manager (windowmaker for example) behavior
        wm title .trace [mc {moodss: Trace}]
        set namespace $modules::instance::($modules::(trace),namespace)
        set table [new dataTable .trace -data ${namespace}::data]
        dataTable::update $table                                      ;# force refreshing of the display so data appears immediately
        # handle closing via window manager:
        wm protocol .trace WM_DELETE_WINDOW "wm withdraw .trace; set global::showTrace 0"
        pack $widget::($table,path) -fill both -expand 1
    }
    if {$display} {
        wm deiconify .trace
    } else {
        wm withdraw .trace
    }
    set global::showTrace $display                                       ;# let menu know in case this procedure is invoked directly
    after idle {focus .}                                                                        ;# keep the focus on the main window
}

proc displayModule {instance draggable {resetOrigin 0}} {
    static x
    static y

    if {![info exists x] || $resetOrigin} {
        foreach {x y dummy dummy} [$global::canvas cget -scrollregion] {}                           ;# next module table coordinates
    }
    if {[lindex $modules::instance::($instance,times) 0] >= 0} {                                         ;# if module is synchronous
        lappend modules::(synchronous) $instance                                             ;### should be done in modules code ###
    }
    if {[info exists modules::instance::($instance,views)]} {                             ;# check whether several views are defined
        set viewMembers $modules::instance::($instance,views)                             ;# create and manage a table for each view
    } else {
        set viewMembers {{}}                                                           ;# there is a single table (the default view)
    }
    set index 0
    set namespace $modules::instance::($instance,namespace)
    if {[info exists modules::instance::($instance,identifier)]} {                                    ;# set a title for data tables
        set title $modules::instance::($instance,identifier)                                        ;# favor identifier if it exists
    } else {                                                                                               ;# simply use module name
        set title $namespace
    }
    regsub {<0>$} $title {} title                                  ;# remove trailing namespace index for first instance of a module
    lifoLabel::push $global::messenger [format [mc {initializing %s module display...}] $title]
    ::update idletasks
    foreach members $viewMembers {
        set initialize [expr {[info exists ::initializer] && ([lsearch -exact $modules::(initialized) $namespace] >= 0)}]
        if {$initialize} {
            set arguments [record::tableOptions $::initializer $namespace $index]                ;# extra stored arguments for table
        } else {
            set arguments {}
        }
        if {![catch {set ${namespace}::data(resizableColumns)} resizable]} {
            lappend arguments -resizablecolumns $resizable
        }
        if {[llength $members] > 0} {                                                                                ;# it is a view
            array set ::view$instance $members
            set table\
                [eval new dataTable $global::canvas -data ${namespace}::data -view ::view$instance -draggable $draggable $arguments]
            unset ::view$instance
        } else {                                                                                     ;# use single default data view
            set table [eval new dataTable $global::canvas -data ${namespace}::data -draggable $draggable $arguments]
        }
        if {$initialize} {                                                                       ;# setup initialized modules tables
            # use stored window manager initialization data for table if it exists:
            set list [record::tableWindowManagerData $::initializer $namespace $index]
            if {[llength $list] > 0} {
                foreach {x y width height level xIcon yIcon} $list {}
                manageViewer $table 0 -static $global::static -setx $x -sety $y -setwidth $width -setheight $height\
                    -level $level -title $title -iconx $xIcon -icony $yIcon -tags $namespace
            } else {
                manageViewer $table 0 -static $global::static -setx $x -sety $y -title $title -tags $namespace
            }
        } else {
            manageViewer $table 0 -static $global::static -setx $x -sety $y -title $title -tags $namespace
        }
        set x [expr {$x + $global::xWindowManagerInitialOffset}]                          ;# offset tables to achieve a nicer layout
        set y [expr {$y + $global::yWindowManagerInitialOffset}]
        incr index                                                                                      ;# next view for initializer
    }
    lifoLabel::pop $global::messenger
}

proc initializationErrorMessageBox {namespace message} {                                                  ;# namespace of the module
    set top [new toplevel .]
    set path $widget::($top,path)
    wm transient $path .
    regsub {<0>$} $namespace {} namespace                          ;# remove trailing namespace index for first instance of a module
    wm title $path [format [mc {moodss: Error initializing module "%s"}] $namespace]
    wm group $path .                                                 ;# for proper window manager (windowmaker for example) behavior
    wm protocol $path WM_DELETE_WINDOW "delete $top"                                                                 ;# self cleanup
    set text [message $path.message -text $message -font $font::(mediumNormal) -justify left -width 640]
    grid rowconfigure $path 0 -weight 1
    grid columnconfigure $path 1 -weight 1
    grid [label $path.icon -image $::dialogBoxErrorIcon] -row 0 -column 0 -sticky nw -padx 2 -pady 2
    grid $text -row 0 -column 1 -sticky nw
    grid [frame $path.separator -relief sunken -borderwidth 1 -height 2] -row 1 -column 0  -columnspan 100 -stick we -pady 2
    grid [button $path.close -text [mc Close] -command "destroy $path"] -row 2 -column 0 -columnspan 100 -padx 2 -pady 2
}

proc databaseConnection {connect {readOnly 0}} {                                  ;# boolean: or disconnect, procedure is idempotent
    if {$connect} {
        if {$global::database != 0} return                                                                      ;# already connected
        lifoLabel::push $global::messenger [mc {connecting to database...}]
    } else {
        if {$global::database == 0} return                                                                   ;# already disconnected
        lifoLabel::push $global::messenger [mc {disconnecting from database...}]
    }
    busy 1 .
    if {$connect} {
        set database [eval new database [concat $global::databaseOptions -readonly $readOnly]]
        if {[string length $database::($database,error)] > 0} {              ;# there was a problem probably due to misconfiguration
            tk_messageBox -title [mc {moodss: Database error}] -type ok -icon error -message $database::($database,error)
            delete $database
        } else {
            if {$database::($database,created)} {
                modules::trace {} moodss(database) [mc {created tables in moodss database}]
            }
            set global::database $database
        }
    } else {
        delete $global::database                                                                                       ;# disconnect
        set global::database 0
    }
    busy 0 .
    lifoLabel::pop $global::messenger
}

proc loadFromDatabase {draggable static} {                                    ;# get ready to load data cell histories from database
    if {![info exists databaseInstances::singleton]} {                                               ;# not in database mode already
        if {![clear]} return                                                      ;# possibly unload all modules unless user aborted
        databaseConnection 1 1                                                              ;# connect to database in read-only mode
        if {$global::database == 0} return                                                             ;# database connection failed
    }
    database::displayAndSelectInstances             ;# show dialog box with modules instances, data cells histories, ... in database
    createInstancesViewer $draggable $static
    updateMenuWidget
    updateToolBar
    updateDragAndDropZone
    # monitor selected instance if any and possibly delete empty instances viewer when closed:
    switched::configure $database::(dialog) -command "databaseInstances::monitor $databaseInstances::singleton"\
       -deletecommand {after idle {databaseInstances::deleteEmpty}}  ;# note: avoid loop in mutual destruction with instances viewer
}

proc createInstancesViewer {draggable static} {
    if {[info exists databaseInstances::singleton]} return
    set instances [new databaseInstances $global::canvas -draggable $draggable]               ;# create database instances singleton
    set path $widget::($instances,path)
    canvasWindowManager::manage $global::windowManager $path $instances
    set title [mc {database module instances}]
    if {[info exists ::initializer] && ([llength [set list [record::databaseViewerWindowManagerData $::initializer]]] > 0)} {
        foreach {x y width height xIcon yIcon} $list {}                                     ;# recorded window manager configuration
        canvasWindowManager::configure $global::windowManager $path\
            -setx $x -sety $y -setwidth $width -setheight $height -iconx $xIcon -icony $yIcon\
            -static $static -title $title -level $global::32BitIntegerMinimum                ;# always stack below all other windows
    } else {
        canvasWindowManager::configure $global::windowManager $path\
            -static $static -title $title -level $global::32BitIntegerMinimum                ;# always stack below all other windows
    }
    # clear on self deletion to avoid recursion in clear{}, delayed to avoid loop in mutual destruction with instances dialog box
    composite::configure $instances -selfdeletecommand {after idle clear}\
        -deletecommand "canvasWindowManager::unmanage $global::windowManager $path; database::removeInstances"
}

proc databaseRecording {start} {                                                                           ;# boolean: start or stop
    if {$start} {
        databaseConnection 1                                                                                  ;# connect to database
        if {$global::database == 0} return                                                             ;# database connection failed
        refresh                           ;# attempt data storage from store class so that errors if any can be detected immediately
    } else {
        databaseConnection 0                                                                             ;# disconnect from database
    }
    updateMenuWidget
    updateToolBar
}

proc monitorDatabaseInstances {presetRange} {                                  ;# used when loading a database history configuration
    databaseConnection 1 1                                                                  ;# connect to database in read-only mode
    if {$global::database == 0} {exit 1}                                             ;# database connection failure is a fatal error
    createInstancesViewer [expr {!$global::readOnly}] $global::static
    foreach instance [modules::instancesWithout [list formulas peer]] {          ;# display all instance modules in instances viewer
        foreach {name index} [modules::decoded $modules::instance::($instance,namespace)] {}    ;# index is database instance number
        if {![string equal $name instance]} {error "not an instance module in history mode: $name"}
        array set option $modules::instance::($instance,arguments)
        databaseInstances::monitor $databaseInstances::singleton [list $index $name $option(-identifier) $option(-arguments)] 0
    }
    eval databaseInstances::updateAxis $databaseInstances::singleton $presetRange
    eval databaseInstances::setCursors $databaseInstances::singleton $presetRange
}

proc updateCanvasImage {file {initialize 0}} {                                ;# note: does not update canvas image global variables
    # use full file path, as current directory may change during the lifetime of the application or dashboard
    set file [file normalize $file]
    if {[string equal $file $global::canvasImageFile] && !$initialize} return                                           ;# no change
    if {([string length $global::canvasImageFile] > 0) && !$initialize} {
        images::release $global::canvasImageFile
    }
    if {[string length $file] == 0} {
        if {[info exists global::canvasImageItem]} {
            $global::canvas delete $global::canvasImageItem; unset global::canvasImageItem
        }
    } else {
        images::load $file $file {}
        set image [images::use $file]
        if {[info exists global::canvasImageItem]} {
            $global::canvas itemconfigure $global::canvasImageItem -image $image
        } else {
            set global::canvasImageItem [$global::canvas create image 0 0 -image $image]
        }
        $global::canvas lower $global::canvasImageItem                             ;# background image should be below anything else
    }
}

proc updateCanvasImagePosition {item position {offset 0}} {
    set canvas $global::canvas
    foreach {left top right bottom} [bounds $canvas] {}                                                           ;# top is always 0
    switch $position {
        nw {
            $canvas itemconfigure $item -anchor nw
            $canvas coords $item $offset 0
        }
        default {                                                                                                          ;# center
            $canvas itemconfigure $item -anchor center
            $canvas coords $item [expr {$offset + (($right - $left) / 2.0)}] [expr {$bottom / 2.0}]
        }
    }
}

proc updateCanvasImagesPosition {} {
    if {[info exists global::canvasImageItem]} {
        updateCanvasImagePosition $global::canvasImageItem $global::canvasImagePosition
    }
    pages::updateImagesPositions
}

proc traceDialog {title message {exit 0}} {            ;# with text area so that message cutting and pasting is possible by the user
    set dialog [new dialogBox . -title $title -buttons x -default x -x [winfo pointerx .] -y [winfo pointery .]]
    if {$exit} {composite::configure $dialog -labels [list x [mc Exit]]}
    set frame [frame $widget::($dialog,path).frame]
    set scroll [new scroll text $frame -height 100]
    composite::configure $dialog -deletecommand "delete $scroll; set ::traceDialogDone {}"
    set text $composite::($scroll,scrolled,path)
    $text insert end $message
    $text configure -font $font::(mediumNormal) -wrap word -state disabled
    pack $widget::($scroll,path) -fill both -expand 1
    dialogBox::display $dialog $frame
    vwait ::traceDialogDone
}

proc manageScrolledCanvas {show} {
    set path $widget::($global::scroll,path)
    if {$show} {
        if {[llength [grid info $path]] == 0} {
            grid $path -row 2 -column 0 -sticky nsew
        }
    } else {
        grid forget $path
    }
}

proc createNewPage {} {
    updateCanvasImage {}; set global::canvasImageFile {}                                       ;# possibly clear existing background
    set pages [expr {[pages::current] != 0}]
    if {!$pages} {manageScrolledCanvas 0}                                                                              ;# first page
    set page [new page . -background $global::canvasBackground -draggable [expr {!$global::readOnly}]]
    if {!$pages} {pages::manageScrolledCanvas 1}
    pages::edit $page
    updateViewObjectsMenu
}

proc formulasDialog {{formulasTable 0} {selectedFormula 0}} {
    if {[info exists global::formulasDialog]} {
        formulas::dialog::raise $global::formulasDialog                                                           ;# make it visible
    } else {
        set command "formulasDialogValidated $formulasTable"
        set deletion {unset global::formulasDialog}
        if {$formulasTable == 0} {
            set list {}
            set global::formulasDialog [new formulas::dialog -command $command -deletecommand $deletion]
        } else {
            composite::configure $formulasTable -state disabled         ;# so that user cannot modify table while it is being edited
            append deletion "; composite::configure $formulasTable -state normal"
            set list [formulas::table::formulas $formulasTable]
            set global::formulasDialog [new formulas::dialog\
                -command $command -deletecommand $deletion -formulas $list\
                -object [composite::cget $formulasTable -object] -category [composite::cget $formulasTable -category] -initial 0\
            ]
        }
        if {([llength $list] > 0) && ($selectedFormula > 0)} {
            formulas::dialog::select $global::formulasDialog $selectedFormula
        }
    }
}

proc formulasDialogValidated {table object category formulas} {
    if {$table == 0} {                                                                                         ;# create a new table
        formulas::table::manage [createNewFormulasViewer $object $category 1 $global::static] $formulas
    } else {                                                                                                ;# update existing table
        if {[formulas::table::manage $table $formulas 1]} {                   ;# the name of at least 1 existing formula was changed
            foreach viewer $viewer::(list) {
                viewer::updateLabels $viewer                  ;# update labels in case one pointed to one of the formulas data cells
            }
        }
    }
}

proc raiseExistingFormulasDialog {} {
    if {[info exists global::formulasDialog]} {
        formulas::dialog::raise $global::formulasDialog
    }
}

proc displayModuleState {namespace value} {
    # use grayish colors: yellow for busy, red for error and green for idle
    switch $value {
        busy {set color #C0C0C0}
        error {set color #D0A0A0}
        idle {set color #A0D0A0}
        default {error "invalid color: $value"}
    }
    canvasWindowManager::colorTagged $global::windowManager $namespace $color  ;# color all tables and viewers related to the module
}
