This package `DoubleClick` extends text widget by generalized behaviour: deals with quotes `""` as well as international quotes such as „Deutsche Gänsefüßchen“, moreover all kinds of nesting parens – `(){}[]<>` – where the angles `<…>` are treatened depending of XML situation: either as comments – `` – or – or DOM tag elements – `

` – or as special markup. Triple click selects grammatical sentence. Put bindings to a single widget, or to tag `Text`, or to a newly-defined Tag to use with [bindtags]. Iʼd say, quite nützlich. In German we call such thing “Eierlegende Wollmilchsau”. Ask one who can translate it. ====== # # file: DoubleClick-0.1.tm # Usage: # package require DoubleClick # bindDoubleClick .textwindow ?yes|no? # # option yes is default # if "yes" then triple-click selects grammatical sentence # if "no" then no specific triple-click # package require Tcl 8.6.1 package provide DoubleClick 0.1 namespace eval DoubleClick { variable quotes { „ “ ‚ ‘ “ ” ‘ ’ » « › ‹ « » ‹ › } variable click none variable index 1.0 } proc DoubleClick::backslashed {win {index insert}} { # \{ \} set target [$win search -elide -backwards -regexp {[^\\]} $index 1.0] if {$target eq ""} then { return false } set found [$win get $target+1chars $index] set length [string length $found] if {$length % 2 == 1} then { return true } else { return false } } proc DoubleClick::extendSel {win target} { lassign "[$win tag ranges sel] insert" start $win tag add sel $start $target } proc DoubleClick::findCloseDquote {win {index insert}} { # " ... " set start $index while true { set target [$win search -elide \u0022 $start+2chars end] if {$target eq ""} then { return false } if {[info complete [$win get $index $target+2chars]]} then { extendSel $win $target+1chars return true } set start $target+1chars } } proc DoubleClick::findCloseSquote {win {index insert}} { # ' ... ' set start $index while true { set target [$win search -elide ' $start+1chars end] if {$target eq ""} then { return false } set txt [$win get $index $target+1chars] set txt\ [string map [list \u0022 " " \{ " " \} " " ' \u0022] $txt] if {[info complete $txt]} then { extendSel $win $target+1chars return true } set start $target+2chars } } proc DoubleClick::findCloseBrace {win {index insert}} { # { ... } set start $index while true { set target [$win search -elide \u007d $start end] if {$target eq ""} then { return false } if {[info complete [$win get $index $target+1chars]]} then { extendSel $win $target+1chars return true } set start $target+1chars } } proc DoubleClick::findCloseBracket {win {index insert}} { # [ ... ] lappend map \[ \{ \] \} set start $index while true { set target [$win search -elide \u005d $start end] if {$target eq ""} then { return false } set txt [$win get $index $target+1chars] if {[info complete [string map $map $txt]]} then { extendSel $win $target+1chars return true } set start $target+1chars } } proc DoubleClick::findCloseParen {win {index insert}} { # ( ... ) lappend map ( \{ ) \} set start $index while true { set target [$win search -elide ) $start end] if {$target eq ""} then { return false } set txt [$win get $index $target+1chars] if {[info complete [string map $map $txt]]} then { extendSel $win $target+1chars return true } set start $target+1chars } } proc DoubleClick::findIntlCloseQuote {win {index insert}} { # „international“ variable quotes set open [$win get $index] set close [dict get $quotes $open] set target [$win search -elide $close $index end] if {$target eq ""} then { return false } extendSel $win $target+1chars return true } proc DoubleClick::findCloseAngleExcl {win {index insert}} { # set target [$win search -elide > $index end] if {$target eq ""} then { return false } extendSel $win $target+1chars return true } proc DoubleClick::findCloseAngleQuest {win {index insert}} { # set target [$win search -elide ?> $index end] if {$target eq ""} then { return false } extendSel $win $target+2chars return true } proc DoubleClick::findEndOfComment {win {index insert}} { # set target [$win search -elide -- --> $index end] if {$target eq ""} then { return false } extendSel $win $target+3chars return true } proc DoubleClick::findEndOfCdata {win {index insert}} { # set target [$win search -elide {]]>} $index end] if {$target eq ""} then { return false } extendSel $win $target+3chars return true } proc DoubleClick::findNestedCloseAngle {win {index insert}} { # <> set start $index lappend map \u007b " " \u007d "" \u0022 " " < \u007b > \u007d while true { set target [$win search -elide > $start end] if {$target eq ""} then { return false } set txt [$win get $index $target+1chars] set txt [string map $map $txt] if {[info complete $txt]} then { extendSel $win $target+1chars return true } set start $target+1chars } } proc DoubleClick::insideComment {win {index insert}} { set start [$win search -elide -backwards $start end] if {$end eq ""} then { return false } if {[$win compare $end+3chars > $index]} then { return true } else { return false } } proc DoubleClick::insideCdata {win {index insert}} { set start [$win search -elide -backwards {} $start end] if {$end eq ""} then { return false } if {[$win compare $end+3chars > $index]} then { return true } else { return false } } proc DoubleClick::insideQuestionMarkup {win {index insert}} { set start [$win search -elide -backwards $start end] if {$end eq ""} then { return false } if {[$win compare $end+2chars > $index]} then { return true } else { return false } } proc DoubleClick::findEndOfEmptyTag {win {index insert}} { set pat {<[[:alnum:]:]+[^>]*/>} set target\ [$win search -elide -regexp -nolinestop -count c $pat $index end] if {$target eq ""} then { return false } elseif {[$win compare $index == $target]} then { extendSel $win "$index + $c chars" return true } else { return false } } proc DoubleClick::tokenIndices {win {from insert} {to end}} { set indices [$win search -elide -regexp -all\ -count count {]*>} $from $to] set result {} foreach index $indices length $count { lappend result $index [$win index "$index + $length chars"] } set result } proc DoubleClick::findClosingTag {win {index insert}} { lassign {no no no} commentAhead cDataAhead questAhead # ... set indices [tokenIndices $win $index] set stack {} foreach {start end} $indices { if { [insideComment $win $start] || [insideQuestionMarkup $win $start] || [insideCdata $win $start] } continue set tag [$win get $start $end] regexp {[[:alnum:]]+} $tag name if {![regexp {^<\s*/} $tag]} then { # opening tag set stack [concat $name $stack] } else { # closing tag if {$stack eq ""} then { return false } else { set stack [lassign $stack name1] if {$name1 ne $name} then { return false } } if {$stack eq ""} then { extendSel $win $end return true } } } return false } proc DoubleClick::findCloseAngle {win {index insert}} { set cdata findEndOfCdata $win $index } elseif {[$win get $index $index+4chars] eq $comment} then { # findEndOfComment $win $index } elseif {[$win get $index $index+2chars] eq $excl} then { # findCloseAngleExcl $win $index } elseif {[$win get $index $index+2chars] eq $quest} then { # findCloseAngleQuest $win $index } elseif {[$win get $index] eq $angle} then { # <... if {[findEndOfEmptyTag $win $index]} then { #
return true } elseif {[findClosingTag $win $index]} then { # ... return true } else { # <> findNestedCloseAngle $win $index } } else { return false } } proc DoubleClick::processChar {win {index insert}} { set char [$win get $index] switch -exact -- $char { < { findCloseAngle $win $index } „ - ‚ - “ - ‘ - » - › - « - ‹ { findIntlCloseQuote $win $index } default { if {[backslashed $win $index]} then { return false } switch -exact -- $char { \u0022 { # " ... " findCloseDquote $win $index } ' { findCloseSquote $win $index } ( { findCloseParen $win $index } \u007b { # { ... } findCloseBrace $win $index } \u005b { # [ ... ] findCloseBracket $win $index } default { return false } } } } } proc DoubleClick::sentenceStartIndex {win {index insert}} { set tagPat {(<[^>]+>||)} append pat\ {[.:?!]([›‹ ‘’][[:punct:]]*)?[»«“”]?} $tagPat?\\s+$tagPat* set target [$win search -elide -nolinestop -regexp -backwards -count c\ $pat $index 1.0] if {$target eq ""} then { set target\ [$win search -elide -regexp -count c {(<[^>]+>\s*)*?\m} 1.0 end] } set result [$win index "$target + $c chars"] while {[$win compare $result < $index] && [string is space [$win get $result]]} { set result $result+1chars } $win index $result } proc DoubleClick::sentenceEndIndex {win {index insert}} { set tagPat {(<[^>]+>||)} append pat\ {[.:?!]([›‹ ‘’][[:punct:]]*)?[»«“”]?} (?=$tagPat?\\s+$tagPat*) set target [$win search -elide -regexp -nolinestop -count c\ $pat $index end] if {$target eq ""} then { lassign {end-1char 0} target c } set result [$win index "$target + $c chars"] if {[string is space [$win get $result-1chars]]} then { set result [$win index $result-1chars] } set result } proc DoubleClick::selectSentence {win {clickpoint insert}} { set start [sentenceStartIndex $win] set end [sentenceEndIndex $win] $win tag remove sel 1.0 end $win tag add sel $start $end } proc DoubleClick::extendSentenceRange {win {clickpoint insert}} { variable index if {[$win compare $clickpoint < $index]} then { set start [sentenceStartIndex $win $clickpoint] set end [sentenceEndIndex $win $index] } else { set start [sentenceStartIndex $win $index] set end [sentenceEndIndex $win $clickpoint] } $win tag remove sel 1.0 end $win tag add sel $start $end } proc DoubleClick::bind+ {win event script} { bind $win $event +$script } proc DoubleClick::strCat args {join $args ""} proc DoubleClick::bindDoubleClick {tag {sentence yes}} { foreach pat { } { bind $tag $pat "" } bind $tag { set ::DoubleClick::click single } bind $tag { %W mark set insert [%W index @%x,%y] set ::DoubleClick::index [%W index insert] set ::DoubleClick::click double if {[::DoubleClick::processChar %W insert]} break } bind $tag { if {$DoubleClick::click eq "double" && [::DoubleClick::processChar %W @%x,%y]} then { %W mark set insert $::DoubleClick::index %W tag add sel insert sel.last break } } bind $tag { if {$DoubleClick::click eq "double" && [::DoubleClick::processChar %W @%x,%y]} then { %W mark set insert $::DoubleClick::index %W tag add sel insert sel.last break } } if {$sentence} then { bind $tag { set ::DoubleClick::click triple DoubleClick::selectSentence %W @%x,%y break } bind+ $tag { if {$DoubleClick::click eq "triple"} then { ::DoubleClick::extendSentenceRange %W @%x,%y break } } bind+ $tag { if {$DoubleClick::click eq "triple"} then { ::DoubleClick::extendSentenceRange %W @%x,%y break } } } } namespace eval DoubleClick namespace export bindDoubleClick namespace import DoubleClick::* ======