# 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

# $Id: modgui.tcl,v 2.63 2006/10/25 18:10:12 jfontain Exp $


class moduleOperations {

    variable internal                                                                ;# internally used options to hide in interface
    set internal(--daemon) {}

    proc moduleOperations {this action} {                                                   ;# action can be display, load or manage
        if {[info exists (displayed)]} {
            delete $($(displayed),dialog)                      ;# only a single module operations dialog box can be active at a time
        }
        set (displayed) $this
        switch $action {
            display {set others {}}
            load {set others load}
            manage {set others {unload reload new}}
            default {error "bad action $action"}
        }
        set dialog [new dialogBox .\
            -buttons hx -default x -helpcommand "moduleOperations::help $this" -x [winfo pointerx .] -y [winfo pointery .]\
            -grab release -deletecommand "delete $this" -closecommand "moduleOperations::close $this" -otherbuttons $others\
        ]
        wm geometry $widget::($dialog,path) 400x200
        set frame [frame $widget::($dialog,path).frame]

        set ($this,label) [label $frame.label -font $font::(mediumNormal) -anchor nw]
        grid $($this,label) -row 0 -column 0 -sticky nw -columnspan 2
        set list [new scrollList $frame -font $font::(mediumBold) -width 0]
        grid $widget::($list,path) -row 1 -column 0 -sticky nws

        set entries [frame $frame.entries]
        # use a table in order to be able to combine expanding entries and a scrollbar:
        set container [table $entries.container\
            -colstretchmode last -rows 0 -cols 2 -highlightthickness 0 -takefocus 0 -borderwidth 0 -cursor {} -bordercursor {}\
            -padx 1 -pady 1 -state disabled -exportselection 0\
        ]
        $container tag configure sel -background {}                          ;# so that selection on sole row is invisible initially
        set scroll [scrollbar $entries.scroll -orient vertical -highlightthickness 0]
        $container configure -yscrollcommand "moduleOperations::updateScrollBar $scroll"
        $scroll configure -command "$container yview"
        grid rowconfigure $entries 0 -weight 1
        grid columnconfigure $entries 0 -weight 1
        grid $container -row 0 -column 0 -sticky nsew
        set ($this,container) $container

        grid rowconfigure $frame 1 -weight 1
        grid columnconfigure $frame 1 -weight 1
        grid $entries -row 1 -column 1 -columnspan 2 -sticky nsew

        dialogBox::display $dialog $frame
        set ($this,dialog) $dialog
        set ($this,frame) $frame
        set ($this,list) $list
        set ($this,action) $action
        set ($this,tips) {}
        lappend ($this,tips) [new widgetTip\
            -path $composite::($dialog,help,path) -text [mc {display help on this dialog box or on selected module}]\
        ]
        switch $action {
            display {
                composite::configure $dialog -title [mc {moodss: Loaded modules}]
                loaded $this
            }
            load {
                composite::configure $dialog -title [mc {moodss: Load modules}]
                composite::configure $dialog load -text [mc Load] -command "moduleOperations::load $this" -state disabled
                lappend ($this,tips)\
                    [new widgetTip -path $composite::($dialog,load,path) -text [mc {load selected module with specified options}]]
                discover $this
            }
            manage {
                composite::configure $dialog -title [mc {moodss: Manage modules}]
                composite::configure $dialog unload -text [mc Unload] -command "moduleOperations::unload $this"
                composite::configure $dialog reload -text [mc Reload] -command "moduleOperations::reload $this"
                composite::configure $dialog new -text [mc New]\
                    -command "moduleOperations::load $this; moduleOperations::loaded $this"
                lappend ($this,tips)\
                    [new widgetTip -path $composite::($dialog,unload,path) -text [mc {unload selected module}]]\
                    [new widgetTip -path $composite::($dialog,reload,path)\
                        -text [mc {unload the selected module then load a new instance using the updated parameters}]\
                    ]\
                    [new widgetTip -path $composite::($dialog,new,path)\
                        -text [mc {load a new instance of the selected module using the updated parameters}]\
                    ]
                loaded $this
            }
        }
        set ($this,index) {}                                                                               ;# selected listbox index
    }

    proc ~moduleOperations {this} {
        eval delete $($this,tips)
        delete $($this,list)                                                                 ;# delete objects other than dialog box
        unset (displayed)
    }

    proc discover {this} {
        variable internal

        if {![info exists (discoveredModules)]} {         ;# cache modules and their switches during the lifetime of the application
            $($this,label) configure -text [mc {searching for modules...}]
            busy 1 $($this,frame)                                                                      ;# show user that we are busy
            update                                       ;# we could use idle tasks update here but it does not work well on windows
            set format [mc {scanning module %s...}]
            modules::available "
                lappend moduleOperations::(discoveredModules) %M
                set moduleOperations::(%M,discoveredSwitches) %S                                 ;# save module switches for caching
                scrollList::insert $($this,list) 0 %M                                        ;# display just discovered module first
                update idletasks                                                   ;# keep refreshing while preventing interruptions
            " "
                $($this,label) configure -text \[format {$format} %M\]               ;# show scanned module for better user feedback
                update idletasks                                                   ;# keep refreshing while preventing interruptions
            "
            busy 0 $($this,frame)
        }
        if {[info exists (discoveredModules)]} {
            $($this,label) configure -text [mc {Select module (view its documentation with Help button):}]
            update idletasks
            set modules [lsort -dictionary $(discoveredModules)]                                 ;# sort modules for easy navigation
            set index 0
            foreach module $modules {
                set switches {}
                foreach {option argument} $($module,discoveredSwitches) {
                    if {[info exists internal($option)]} continue                                  ;# ignore internally used options
                    lappend switches $option $argument
                }
                foreach {option argument} $switches {
                    if {[info exists (last,$module,$option)]} {
                        set ($this,$module,$option) $(last,$module,$option)       ;# use module options used by last loaded instance
                    } elseif {$argument} {                                                           ;# the option takes an argument
                        set ($this,$module,$option) {}                                    ;# the argument is always empty by default
                    } else {
                        set ($this,$module,$option) 0                                 ;# a boolean option is always false by default
                    }
                }
                set ($module,switches) $switches
                set ($this,indexNamespace,$index) $module
                incr index
            }
            composite::configure $($this,list) -list $modules
            bind $composite::($($this,list),listbox,path) <<ListboxSelect>> "moduleOperations::selection $this"
        } else {
            $($this,label) configure -text [mc {Found no modules:}]
        }
    }

    proc selection {this} {
        set index [lindex [scrollList::curselection $($this,list)] 0]
        if {($index eq "") || ($index eq $($this,index))} return

        if {($($this,action) eq "manage") && ($($this,index) ne "") && [changed $this $($this,index)]} {
            # in manage mode, there was a previous selection and some last selected module option values have changed
            # deselect next selection:
            scrollList::selection $($this,list) clear $index; scrollList::selection $($this,list) set $($this,index)
            if {![interactiveQuery\
                $this [mc {moodss: Module parameters}] [mc {Ignore changes and continue the selection of another module?}]\
            ]} return                                                                                     ;# user canceled selection
            set module $($this,indexNamespace,$($this,index))                                                ;# last selected module
            foreach {option argument} $($module,switches) value $($this,values) {
                set ($this,$module,$option) $value                                 ;# restore original values for previous selection
            }
            # reselect next selection:
            scrollList::selection $($this,list) clear $($this,index); scrollList::selection $($this,list) set $index
        }
        set module $($this,indexNamespace,$index)                                       ;# module (discovered) or namespace (loaded)
        switch $($this,action) {
            display {
                set state disabled
            }
            load {
                set state normal
                composite::configure $($this,dialog) load -state normal
            }
            manage {
                set state normal
                composite::configure $($this,dialog) unload -state normal
                composite::configure $($this,dialog) reload -state normal
                composite::configure $($this,dialog) new -state normal
            }
        }
        cleanOptions $this
        set table $($this,container)
        $table configure -rows [expr {[llength $($module,switches)] / 2}]
        set row 0
        set width 0
        set ($this,values) {}                         ;# remember original values to check for changes when selecting another module
        foreach {option argument} $($module,switches) {
            set label [label $table.$row,0 -font $font::(mediumBold) -text $option]
            $table window configure $row,0 -window $label
            if {[winfo reqwidth $label] > $width} {                                             ;# keep track of labels column width
                set width [winfo reqwidth $label]
            }
            if {$argument} {
                set path [entry $table.$row,1\
                    -font $font::(mediumNormal) -width 0 -textvariable moduleOperations::($this,$module,$option) -state $state\
                ]
                if {[regexp $global::passwordOptionExpression $option]} {
                    $path configure -show *                                                              ;# do not display passwords
                }
                $table window configure $row,1 -window $path -sticky ew
            } else {
                set path [checkbutton $table.$row,1 -variable moduleOperations::($this,$module,$option) -state $state]
                $table window configure $row,1 -window $path -sticky w
            }
            lappend ($this,values) $($this,$module,$option)
            incr row
        }
        if {$row == 0} {                                                                               ;# no options for this module
            set label [label $table.$row,0 -font $font::(mediumItalic) -text [mc {no options}]]
            $table window configure $row,0 -window $label
            $table window configure $row,1 -window [label $table.$row,1] -sticky ew
            set width [winfo reqwidth $label]
        }
        $table width 0 -$width
        set ($this,index) $index
    }

    proc load {this} {
        set module $($this,indexNamespace,$($this,index))
        # only name must be used, not index to avoid clashes with loaded instances of the same module:
        foreach {name index} [modules::decoded $module] {}
        array unset {} last,$name,*                                                     ;# reset last loaded options for that module
        set string $name
        foreach {option argument} $($module,switches) {
            if {$argument} {
                if {$($this,$module,$option) ne ""} {
                    append string " $option [list $($this,$module,$option)]"                          ;# properly quote option value
                }
            } else {
                if {$($this,$module,$option)} {
                    append string " $option"
                }
            }
            set (last,$name,$option) $($this,$module,$option)                               ;# remember option value for module name
        }
        if {[catch {dynamicallyLoadModules 0 $string} message]} {
            tk_messageBox -title [mc {moodss: Error loading module}]\
                 -type ok -icon error -message $message -parent $widget::($($this,dialog),path)
        }
    }

    proc loaded {this} {
        variable internal

        $($this,label) configure -text [mc {Select loaded module (view its documentation with Help button):}]
        cleanOptions $this
        set index 0
        foreach values [lsort -dictionary -index 1 [modulesData]] {
            foreach {namespace identifier options} $values {}                            ;# sort by identifier for user-friendliness
            lappend list $identifier
            set switches {}
            foreach {switch argument value} $options {
                if {[info exists internal($switch)]} continue                                      ;# ignore internally used options
                lappend switches $switch $argument
                set ($this,$namespace,$switch) $value
            }
            set ($namespace,switches) $switches
            set ($this,indexNamespace,$index) $namespace
            incr index
        }
        if {![info exists list]} {error {no loaded modules}}                      ;# user interface should never allow this to occur
        composite::configure $($this,list) -list {}                                                    ;# first clean displayed list
        set ($this,index) {}                                                         ;# reset selected index since selection is gone
        composite::configure $($this,list) -list $list                                                                ;# then update
        bind $composite::($($this,list),listbox,path) <<ListboxSelect>> "moduleOperations::selection $this"
        if {$($this,action) eq "manage"} {
            composite::configure $($this,dialog) unload -state disabled                                   ;# since selection is gone
            composite::configure $($this,dialog) reload -state disabled
            composite::configure $($this,dialog) new -state disabled
        }
    }

    proc unload {this} {
        dynamicallyUnloadModule $($this,indexNamespace,$($this,index))
        if {[llength [modulesData]] == 0} {
            # nothing to display: just vanish once module is completely unloaded (### unreproducible bug ###):
            after idle delete $($this,dialog)
        } else {
            loaded $this                                                                                          ;# refresh display
            composite::configure $($this,dialog) unload -state disabled
        }
    }

    proc reload {this} {
        dynamicallyUnloadModule $($this,indexNamespace,$($this,index))
        load $this                ;# replace the module with a new instance but which keeps the just deleted instance number, if any
        if {[llength [modulesData]] == 0} {
            # if the module failed to reload (due to bad options, for example), there may be no loaded modules left at this point
            after idle delete $($this,dialog)
        } else {
            loaded $this
        }
    }

    proc help {this} {                                                                            ;# module must be already selected
        if {$($this,index) ne ""} {                                                            ;# a module is selected
            set module $($this,indexNamespace,$($this,index))                                                 ;# module or namespace
            foreach {name index} [modules::decoded $module] {}                                   ;# only name is required, not index
            moduleHelpWindow $name [modules::helpHTMLData $name]
        } else {                                                                                             ;# display general help
            switch $($this,action) {
                display {generalHelpWindow #menus.file.modules.loaded}
                load {generalHelpWindow #menus.file.modules.load}
                manage {generalHelpWindow #menus.file.modules.manage}
            }
        }
    }

    proc updateScrollBar {widget beginning end} {
        $widget set $beginning $end
        if {($end - $beginning) < 1} {                                                                ;# display only when necessary
            grid $widget -row 0 -column 1 -sticky ns
        } else {
            grid forget $widget
        }
    }

    proc cleanOptions {this} {
        set table $($this,container)
        foreach cell [$table window names] {                                            ;# possibly remove existing switches display
            destroy $table.$cell
        }
    }

    proc changed {this index} {
        set module $($this,indexNamespace,$index)
        foreach {option argument} $($module,switches) value $($this,values) {
            if {$($this,$module,$option) ne $value} {
                return 1                                                                                 ;# stop at first difference
            }
        }
        return 0
    }

    proc interactiveQuery {this title message} {
        return [string equal\
            [tk_messageBox -title $title -type yesno -icon question -parent $widget::($($this,dialog),path) -message $message] yes\
        ]
    }

    proc close {this} {
        if {$($this,action) ne "manage"} {return 1}                                          ;# always close when not in manage mode
        if {($($this,index) eq "") || ![changed $this $($this,index)]} {                         ;# no module selected or no changes
            return 1                                                                                                ;# safe to close
        } elseif {[interactiveQuery $this [mc {moodss: Module parameters}] [mc {Ignore changes and close the dialog box?}]]} {
            return 1                                                                             ;# user chose to ignore the changes
        } else {
            return 0
        }
    }

    proc modulesData {} {
        set list {}
        foreach {namespace identifier options} [modules::loaded] {
            switch [lindex [modules::decoded $namespace] 0] {formulas - peer continue}                      ;# skip internal modules
            regsub {<0>$} $identifier {} identifier                ;# remove trailing namespace index for first instance of a module
            lappend list [list $namespace $identifier $options]
        }
        return $list
    }

}
