pure-data/tcl/dialog_array.tcl

package provide dialog_array 0.1

namespace eval ::dialog_array:: {
    namespace export pdtk_array_dialog
    namespace export pdtk_array_listview_new
    namespace export pdtk_array_listview_fillpage
    namespace export pdtk_array_listview_setpage
    namespace export pdtk_array_listview_closeWindow
}

# global variables for the listview
array set ::dialog_array::listview_entry {}
array set ::dialog_array::listview_id {}
array set ::dialog_array::listview_page {}
array set ::dialog_array::listview_numpages {}
set ::dialog_array::listview_pagesize 1000
# this stores the state of the "save me" check button
array set ::dialog_array::saveme_button {}
# this stores the state of the "draw as" radio buttons
array set ::dialog_array::drawas_button {}
# this stores the state of the "in new graph"/"in last graph" radio buttons
# and the "delete array" checkbutton
array set ::dialog_array::otherflag_button {}

############ pdtk_array_dialog -- dialog window for arrays #########
proc ::dialog_array::listview_windowname {arrayName} {
    set id $::dialog_array::listview_id($arrayName)
    return "${id}_listview"
}
proc ::dialog_array::listview_lbname {arrayName} {
    set id $::dialog_array::listview_id($arrayName)
    return "${id}_listview.data.lb"
}

proc ::dialog_array::listview_setpage {arrayName page {numpages {}} {pagesize {}}} {
    set ::dialog_array::listview_page($arrayName) $page
    set ::dialog_array::listview_numpages($arrayName) $numpages
    if {$pagesize ne {} && [string is double $pagesize]} {
        set ::dialog_array::listview_pagesize $pagesize
    }
}
proc ::dialog_array::listview_setdata {arrayName startIndex args} {
    set lb [listview_lbname $arrayName]
    if { [catch {
        # treeview
        ${lb} delete [${lb} children {}]
        set idx $startIndex
        foreach x $args {
            ${lb} insert {} end -values [list $idx $x]
            incr idx
        }
    } ] } {
        # listbox
        ${lb} delete 0 end
        set idx 0
        foreach x $args {
            ${lb} insert $idx "[expr $startIndex + $idx]) $x"
            incr idx
        }
    }
}
proc ::dialog_array::listview_focus {arrayName item} {
    set lb [listview_lbname $arrayName]
    ${lb} yview $item
}

proc ::dialog_array::pdtk_array_listview_setpage {arrayName page} {
    listview_setpage $arrayName $page
}

proc ::dialog_array::listview_changepage {arrayName np} {
    pdtk_array_listview_setpage \
        $arrayName [expr $::dialog_array::listview_page($arrayName) + $np]
    pdtk_array_listview_fillpage $arrayName
}

proc ::dialog_array::pdtk_array_listview_fillpage {arrayName} {
    set lb [listview_lbname ${arrayName}]

    # get the index of the topmost visible element
    # (so the scroll does not change after updating the elements)
    if {[winfo exists $lb]} {
        if { [catch {
            # treeview

            # this is index of the 'selected' element
            # (not what we want, but a good fallback...)
            set topItem [$lb index [$lb focus]]

            # search for the first visible cell
            set xy 0
            for { set xy 0 } { $xy < 500 } { incr xy } {
                if { [$lb identify region $xy $xy ] eq "cell" } {
                    # usually the first cell we find is still hidden
                    # increment by one more pixel to get a valid one
                    incr xy

                    set item [$lb identify item $xy $xy]
                    set topItem [$lb index $item]
                    break
                }
            }
        } ] } {
            # listbox (much simpler)
            set topItem [expr [lindex [$lb yview] 0] * [$lb size]]
        }
        set cmd "$::dialog_array::listview_id($arrayName) \
               arrayviewlistfillpage \
               $::dialog_array::listview_page($arrayName) \
               $topItem"

        pdsend $cmd
    }
}

proc ::dialog_array::pdtk_array_listview_new {id arrayName page} {
    set ::dialog_array::listview_page($arrayName) $page
    set ::dialog_array::listview_id($arrayName) $id
    set windowName [listview_windowname ${arrayName}]
    destroy $windowName

    toplevel $windowName -class DialogWindow
    wm group $windowName .
    wm protocol $windowName WM_DELETE_WINDOW \
        "::dialog_array::listview_close $id \{$arrayName\}"
    wm title $windowName [concat $arrayName "(list view)"]


    frame $windowName.data
    pack $windowName.data -fill "both" -side top -expand 1
    frame $windowName.buttons
    pack $windowName.buttons -fill "x" -side bottom

    set lb $windowName.data.lb
    set sb $windowName.data.sb
    if { [ catch {
        # treeview
        ttk::treeview $lb \
            -columns {index value} -show headings \
            -height 20 \
            -selectmode extended \
            -yscrollcommand "$sb set"
        $lb heading index -text "#" -anchor center
        $lb heading value -text $arrayName -anchor center
        $lb column index -width 75 -anchor e
    } stderr ] } {
        # listview
        listbox $lb -height 20 -width 25 \
            -selectmode extended \
            -relief solid -background white -borderwidth 1 \
            -yscrollcommand "$sb set"
    }
    scrollbar $sb \
        -command "$lb yview" -orient vertical
    pack $lb -expand 1 -fill both -side left
    pack $sb -fill y -side right
    bind $lb <Double-ButtonPress-1> \
        "::dialog_array::listview_edit \{$arrayName\} $page"
    # handle copy/paste
    catch {
        # this probably only works on X11
        selection handle $lb \
            "::dialog_array::listview_lbselection \{$arrayName\}"
    }
    # a Copy/Paste popup menu
    bind $lb <ButtonPress-3> \
        "::dialog_array::listview_popup \{$arrayName\}"
    bind $lb <<Paste>> \
        "::dialog_array::listview_paste \{$arrayName\}; break"
    bind $lb <<Copy>> \
        "::dialog_array::listview_copy \{$arrayName\}; break"

    button $windowName.buttons.prev -text "\u2190" \
        -command "::dialog_array::listview_changepage \{$arrayName\} -1"
    button $windowName.buttons.next -text "\u2192" \
        -command "::dialog_array::listview_changepage \{$arrayName\} 1"

    frame $windowName.buttons.page

    entry $windowName.buttons.page.current -textvariable ::dialog_array::listview_page($arrayName) \
        -validate key -validatecommand "string is double %P" \
        -justify "right" -width 5
    bind $windowName.buttons.page.current <Return> \
        "::dialog_array::listview_changepage \{$arrayName\} 0"
    label $windowName.buttons.page.slash -text "/"
    label $windowName.buttons.page.total -text 0 -textvariable ::dialog_array::listview_numpages($arrayName)
    pack $windowName.buttons.page.current -side left
    pack $windowName.buttons.page.slash -side left
    pack $windowName.buttons.page.total -side left


    pack $windowName.buttons.prev -side left -ipadx 20 -pady 10 -anchor s
    pack $windowName.buttons.page -side left -padx 20 -pady 10 -anchor s
    pack $windowName.buttons.next -side right -ipadx 20 -pady 10 -anchor s
    focus $windowName
}

proc ::dialog_array::listview_lbselection {arrayName off size} {
    set lb [listview_lbname ${arrayName}]
    set items {}
    if { [catch {
        foreach idx [$lb selection] {
            lappend items [lindex [$lb item $idx -values] 1]
        }
    } ] } {
        foreach idx [$lb curselection] {
            set v [$lb get $idx]
            lappend items [string range $v [string first ") " $v]+2 end]
        }
    }

    return [join $items "\n"]
}

# parses 'data' into numbers, and sends them to the Pd-core so it
# can set the values in 'arrayName' starting from 'startIndex'
proc ::dialog_array::listview_edit+paste {arrayName startIndex data} {
    set values {}
    set offset [expr $startIndex \
                    + $::dialog_array::listview_pagesize \
                    * $::dialog_array::listview_page($arrayName)]
    foreach value [split $data ", \n"] {
        if {$value eq {}} {continue}
        if {! [string is double $value]} {continue}
        lappend values $value
    }
    if { $values ne {} } {
        pdsend "$::dialog_array::listview_id($arrayName) $offset $values"
        pdtk_array_listview_fillpage $arrayName
    }
}

# a popup menu for copy/paste
proc ::dialog_array::listview_popup {arrayName} {
    set windowName [listview_windowname ${arrayName}]
    set lb [listview_lbname ${arrayName}]
    set popup ${lb}.popup
    destroy $popup

    # check if there's no selection, disable the popup
    set cur {}
    if { [catch {
        set cur [$lb selection]
    } ] } {
        set cur [$lb curselection]
    }
    if { $cur eq {} } {
        return
    }

    menu $popup -tearoff false
    $popup add command -label [_ "Copy"] \
        -command "::dialog_array::listview_copy \{$arrayName\}; \
                  destroy $popup"
    $popup add command -label [_ "Paste"] \
        -command "::dialog_array::listview_paste \{$arrayName\}; \
                  destroy $popup"
    tk_popup $popup [winfo pointerx $windowName] \
        [winfo pointery $windowName] 0
}

# copy current selection to clipboard (called from the copy/paste popup)
proc ::dialog_array::listview_copy {arrayName} {
    set sel [listview_lbselection $arrayName {} {}]
    clipboard clear
    clipboard append $sel
}

# when data is pasted (called from the copy/paste popup), update the values
proc ::dialog_array::listview_paste {arrayName} {
    set sel {}
    set itemNum {}
    # get data from CLIPBOARD
    if { $sel eq {} } {catch { set sel [selection get -selection CLIPBOARD] }}
    # if that failed, get it from the PRIMARY copy buffer
    if { $sel eq {} } {catch { set sel [selection get -selection PRIMARY] }}

    if { $sel eq {} } {
        # giving up
        return
    }

    # get the selection start, so we know where to paste to
    set lb [::dialog_array::listview_lbname $arrayName]
    if { [catch {
        set itemId [lindex [$lb selection] 0]
        if { $itemId ne {} } {
            set itemNum [$lb index ${itemId} ]
        }
    } ] } {
        set itemNum [lindex [$lb curselection] 0]
    }

    if { $itemNum ne {} } {
        ::dialog_array::listview_edit+paste $arrayName $itemNum $sel
    }

}

proc ::dialog_array::listview_edit {arrayName page {font {}}} {
    set lb [listview_lbname ${arrayName}]
    set entry ${lb}.entry
    if {[winfo exists $entry]} {
        ::dialog_array::listview_update_entry \
            $arrayName $::dialog_array::listview_entry($arrayName)
        unset ::dialog_array::listview_entry($arrayName)
    }
    destroy $entry
    if { [catch {
        set focus [$lb focus]
        foreach {x y w h} [$lb bbox $focus 1] {break}
        entry $entry
        place configure ${lb}.entry -x ${x} -y ${y} -width ${w} -height ${h}
        set itemNum [$lb index $focus]
    } stderr ] } {
        set itemNum [$lb index active]

        set bbox [$lb bbox $itemNum]
        set y [expr [lindex $bbox 1] - 4]
        entry $entry
        place configure $entry -relx 0 -y $y -relwidth 1
    }
    set ::dialog_array::listview_entry($arrayName) $itemNum

    $entry insert 0 []
    lower $entry
    focus $entry
    bind $entry <Return> \
        "::dialog_array::listview_update_entry \{$arrayName\} $itemNum; break"
    bind $entry <Escape> \
        "destroy $entry; break"
}

proc ::dialog_array::listview_update_entry {arrayName itemNum} {
    set entry [listview_lbname $arrayName].entry
    ::dialog_array::listview_edit+paste $arrayName $itemNum [$entry get]
    destroy $entry
}

proc ::dialog_array::pdtk_array_listview_closeWindow {arrayName} {
    destroy [listview_windowname ${arrayName}]
}

proc ::dialog_array::listview_close {mytoplevel arrayName} {
    pdtk_array_listview_closeWindow $arrayName
    pdsend "$mytoplevel arrayviewclose"
}

proc ::dialog_array::apply {mytoplevel} {
    pdsend "$mytoplevel arraydialog \
            [::dialog_gatom::escape [$mytoplevel.array.name.entry get]] \
            [$mytoplevel.array.size.entry get] \
            [expr $::dialog_array::saveme_button($mytoplevel) + (2 * $::dialog_array::drawas_button($mytoplevel))] \
            $::dialog_array::otherflag_button($mytoplevel)"
}

proc ::dialog_array::openlistview {mytoplevel} {
    pdsend "$mytoplevel arrayviewlistnew"
}

proc ::dialog_array::cancel {mytoplevel} {
    pdsend "$mytoplevel cancel"
}

proc ::dialog_array::ok {mytoplevel} {
    ::dialog_array::apply $mytoplevel
    ::dialog_array::cancel $mytoplevel
}

proc ::dialog_array::pdtk_array_dialog {mytoplevel name size flags newone} {
    if {[winfo exists $mytoplevel]} {
        wm deiconify $mytoplevel
        raise $mytoplevel
        focus $mytoplevel
    } else {
        create_dialog $mytoplevel $newone
    }

    $mytoplevel.array.name.entry insert 0 [::dialog_gatom::unescape $name]
    $mytoplevel.array.size.entry insert 0 $size
    set ::dialog_array::saveme_button($mytoplevel) [expr $flags & 1]
    set ::dialog_array::drawas_button($mytoplevel) [expr ( $flags & 6 ) >> 1]
    set ::dialog_array::otherflag_button($mytoplevel) 0
# pd -> tcl
#  2 * (int)(template_getfloat(template_findbyname(sc->sc_template), gensym("style"), x->x_scalar->sc_vec, 1)));

# tcl->pd
#    int style = ((flags & 6) >> 1);
}

proc ::dialog_array::create_dialog {mytoplevel newone} {
    toplevel $mytoplevel -class DialogWindow
    wm title $mytoplevel [_ "Array Properties"]
    wm group $mytoplevel .
    wm resizable $mytoplevel 0 0
    wm transient $mytoplevel $::focused_window
    $mytoplevel configure -menu $::dialog_menubar
    $mytoplevel configure -padx 0 -pady 0
    ::pd_bindings::dialog_bindings $mytoplevel "array"

    # array
    labelframe $mytoplevel.array -borderwidth 1 -text [_ "Array"] -padx 5
    pack $mytoplevel.array -side top -fill x
    frame $mytoplevel.array.name -height 7 -padx 5
    pack $mytoplevel.array.name -side top -anchor e
    label $mytoplevel.array.name.label -text [_ "Name:"]
    entry $mytoplevel.array.name.entry -width 17
    pack $mytoplevel.array.name.entry $mytoplevel.array.name.label -side right

    frame $mytoplevel.array.size -height 7 -padx 5
    pack $mytoplevel.array.size -side top -anchor e
    label $mytoplevel.array.size.label -text [_ "Size:"]
    entry $mytoplevel.array.size.entry -width 17
    pack $mytoplevel.array.size.entry $mytoplevel.array.size.label -side right

    checkbutton $mytoplevel.array.saveme -text [_ "Save contents"] \
        -variable ::dialog_array::saveme_button($mytoplevel) -anchor w
    pack $mytoplevel.array.saveme -side top

    # draw as
    labelframe $mytoplevel.drawas -text [_ "Draw as:"] -padx 20 -borderwidth 1
    pack $mytoplevel.drawas -side top -fill x
    radiobutton $mytoplevel.drawas.points -value 0 \
        -variable ::dialog_array::drawas_button($mytoplevel) -text [_ "Polygon"]
    radiobutton $mytoplevel.drawas.polygon -value 1 \
        -variable ::dialog_array::drawas_button($mytoplevel) -text [_ "Points"]
    radiobutton $mytoplevel.drawas.bezier -value 2 \
        -variable ::dialog_array::drawas_button($mytoplevel) -text [_ "Bezier curve"]
    pack $mytoplevel.drawas.points -side top -anchor w
    pack $mytoplevel.drawas.polygon -side top -anchor w
    pack $mytoplevel.drawas.bezier -side top -anchor w

    # options
    if {$newone == 1} {
        labelframe $mytoplevel.options -text [_ "Put array into:"] -padx 20 -borderwidth 1
        pack $mytoplevel.options -side top -fill x
        radiobutton $mytoplevel.options.radio0 -value 0 \
            -variable ::dialog_array::otherflag_button($mytoplevel) -text [_ "New graph"]
        radiobutton $mytoplevel.options.radio1 -value 1 \
            -variable ::dialog_array::otherflag_button($mytoplevel) -text [_ "Last graph"]
        pack $mytoplevel.options.radio0 -side top -anchor w
        pack $mytoplevel.options.radio1 -side top -anchor w
    } else {
        labelframe $mytoplevel.options -text [_ "Options"] -padx 20 -borderwidth 1
        pack $mytoplevel.options -side top -fill x
        button $mytoplevel.options.listview -text [_ "Open List View..."] \
            -command "::dialog_array::openlistview $mytoplevel [$mytoplevel.array.name.entry get]"
        pack $mytoplevel.options.listview -side top
        checkbutton $mytoplevel.options.deletearray -text [_ "Delete array"] \
            -variable ::dialog_array::otherflag_button($mytoplevel) -anchor w
        pack $mytoplevel.options.deletearray -side top
    }

    # buttons
    frame $mytoplevel.buttonframe
    pack $mytoplevel.buttonframe -side bottom -pady 2m
    button $mytoplevel.buttonframe.cancel -text [_ "Cancel"] \
        -command "::dialog_array::cancel $mytoplevel"
    pack $mytoplevel.buttonframe.cancel -side left -expand 1 -fill x -padx 15 -ipadx 10
    if {$newone == 0 && $::windowingsystem ne "aqua"} {
        button $mytoplevel.buttonframe.apply -text [_ "Apply"] \
            -command "::dialog_array::apply $mytoplevel"
        pack $mytoplevel.buttonframe.apply -side left -expand 1 -fill x -padx 15 -ipadx 10
    }
    button $mytoplevel.buttonframe.ok -text [_ "OK"]\
        -command "::dialog_array::ok $mytoplevel" -default active
    pack $mytoplevel.buttonframe.ok -side left -expand 1 -fill x -padx 15 -ipadx 10

    # live widget updates on OSX in lieu of Apply button
    if {$::windowingsystem eq "aqua"} {

        # only bind if there is an existing array to edit
        if {$newone == 0} {

            # call apply on button changes
            $mytoplevel.array.saveme config -command [ concat ::dialog_array::apply $mytoplevel ]
            $mytoplevel.drawas.points config -command [ concat ::dialog_array::apply $mytoplevel ]
            $mytoplevel.drawas.polygon config -command [ concat ::dialog_array::apply $mytoplevel ]
            $mytoplevel.drawas.bezier config -command [ concat ::dialog_array::apply $mytoplevel ]

            # call apply on Return in entry boxes that are in focus & rebind Return to ok button
            bind $mytoplevel.array.name.entry <KeyPress-Return> "::dialog_array::apply_and_rebind_return $mytoplevel"
            bind $mytoplevel.array.size.entry <KeyPress-Return> "::dialog_array::apply_and_rebind_return $mytoplevel"

            # unbind Return from ok button when an entry takes focus
            $mytoplevel.array.name.entry config -validate focusin -vcmd "::dialog_array::unbind_return $mytoplevel"
            $mytoplevel.array.size.entry config -validate focusin -vcmd "::dialog_array::unbind_return $mytoplevel"
        }

        # remove cancel button from focus list since it's not activated on Return
        $mytoplevel.buttonframe.cancel config -takefocus 0

        # show active focus on the ok button as it *is* activated on Return
        $mytoplevel.buttonframe.ok config -default normal
        bind $mytoplevel.buttonframe.ok <FocusIn> "$mytoplevel.buttonframe.ok config -default active"
        bind $mytoplevel.buttonframe.ok <FocusOut> "$mytoplevel.buttonframe.ok config -default normal"

        # since we show the active focus, disable the highlight outline
        $mytoplevel.buttonframe.ok config -highlightthickness 0
        $mytoplevel.buttonframe.cancel config -highlightthickness 0
    }

    position_over_window ${mytoplevel} ${::focused_window}
}

# for live widget updates on OSX
proc ::dialog_array::apply_and_rebind_return {mytoplevel} {
    ::dialog_array::apply $mytoplevel
    bind $mytoplevel <KeyPress-Return> "::dialog_array::ok $mytoplevel"
    focus $mytoplevel.buttonframe.ok
    return 0
}

# for live widget updates on OSX
proc ::dialog_array::unbind_return {mytoplevel} {
    bind $mytoplevel <KeyPress-Return> break
    return 1
}