Having updated
doubleclick I made an extension of text widget which is intended to be small and
stable. For features see comment section.
Have fun.
#!/usr/bin/wish
# file: texteditor-0.1.tm
#
# This package extends Tk's text widget by usual bindings.
# This is done by extend the bindtags of given widget by new tag TextEditor.
#
# Technically:
# New namespace TextEditorBindings
# New widgets .textEditorContextMenu .textEditorSelContextMenu
# Relies on UNDOCUMENTED array variable ::tk::Priv(selectMode)
# but this is the ONLY undocumented property to use
#
# Usage:
# textEditor win - creates extended text widget $win
# with options on creation: wrap -word, -undo yes
# textToEditor win - extends given text widget $win
# editorToText win - removes extensions from text widget $win
# textEditorQuotes - sets internationaL quotes AND dash bindings
# arguments: de en en-AM fr ch C
#
# Extensions:
#
# Double-click on opening brace { selects to closing brace }, incl. nesting;
# same on [ to ], ( to ), " to ", opening <XMLtag> to closing </XMLtag>,
# additionaly selects any international quote, but non-nesting.
#
# Quotes are set according to textEditorQuotes.
# When quotes are set to any of de, fr, en, en-AM, ch,
# then key sequence Escape-Quote produces computer quotes (#34, #39).
#
# Context menu on <Button-3> to set some HTML tags and remove outermost paired tags
# with NO ACTION if selected tags are NOT PAIRED.
#
package require Tcl 8.6
package require Tk 8.6
package provide texteditor 0.1
namespace eval TextEditorBindings {
namespace export\
textEditor\
textToEditor\
editorToText\
textEditorQuotes
}
proc ::TextEditorBindings::selectToClosingChar {w x y} {
set i0 [$w index @$x,$y]
set transList [list \u007b \u007d\
\" \" ' ' „ “ ‚ ‘ “ ” ‘ ’\
\u00bb \u00ab \u00ab \u00bb \u203a \u2039 \u2039 \u203a\
\u005b \u005d < > \u0028 \u0029]
set c0 [$w get $i0]
set selectTo {{w i0 i1} {
if {[$w tag ranges sel] eq ""} then {
$w tag add sel $i0 $i1
$w mark set insert $i0
} else {
$w tag add sel sel.first $i1
$w mark set insert $i1
}
}}
if {![dict exists $transList $c0]} then {
return false
}
set c1 [dict get $transList $c0]
if {$c0 ni [list \[ \( \{ \" <]} then {
# Quotes - non-nestable
set i1 [$w search $c1 $i0+1chars end]
if {$i1 eq ""} then {
return false
}
# $w tag add sel $i0 $i1+1chars
# $w mark set insert $i0
apply $selectTo $w $i0 $i1+1chars
return true
} elseif {$c0 eq "<"} then {
# HTML tags?
set i1 [$w search > $i0+1chars end]
if {$i1 eq ""} then {
# no closing char > - not an HTML tag
return false
}
set src [$w get $i0 $i1+1chars]
if {[regexp {<\s*/} $src] ||
[regexp {/\s*>} $src] ||
[regexp {<\s*[?!]} $src]} then {
# closing or empty tag - non-nestable
apply $selectTo $w $i0 $i1+1chars
return true
} else {
# opening tag - nestable
set txt [string trim [$w get $i0+1chars $i1]]
set name [lindex [split $txt] 0]
set open <\\s*$name\[^>\]*>
set close <\\s*/\\s*$name\\s*>
set i1 $i0
while true {
set i1 [$w search -regexp $close $i1 end]
if {$i1 eq ""} then {
return false
}
set i1 [$w index [$w search > $i1 end]+1chars]
set txt [$w get $i0 $i1]
set txt [string map [list \{ " " \} " " \" " "] $txt]
regsub -all $open $txt \{ txt
regsub -all $close $txt \} txt
if {[info complete $txt]} then {
apply $selectTo $w $i0 $i1
return true
}
}
}
return false
} else {
# braces, brackets - nestable
if {$c0 in [list \{ \"]} then {
set map {}
} else {
set map [list \{ " " \} " " \" " " $c0 \{ $c1 \}]
}
set i1 $i0
while true {
set i1 [$w search $c1 $i1+1chars end]
if {$i1 eq ""} then {
return false
}
if {[info complete [string map $map [$w get $i0 $i1+1chars]]]} then {
apply $selectTo $w $i0 $i1+1chars
return true
}
}
}
}
proc ::TextEditorBindings::textEditorQuotes {{lang de}} {
switch -exact -- $lang {
de {
set quotes {„ “ ‚ ‘}
}
en - en-AM {
set quotes {“ ” ‘ ’}
}
fr {
set quotes {» « › ‹}
}
ch {
set quotes {« » ‹ ›}
}
default {
bind TextEditor <Key-quotedbl> ""
bind TextEditor <Key-quoteright> ""
bind TextEditor <Escape><Key-quotedbl> ""
bind TextEditor <Escape><Key-quoteright> ""
bind TextEditor <Key-minus><Key-space> ""
return
}
}
lassign $quotes doubleOpen doubleClose singleOpen singleClose
set insideTag {
{window index} {
set idx0 [$window search -backwards < $index 1.0]
if {$idx0 eq ""} then {
return false
}
set idx1 [$window search -backwards > $index 1.0]
if {$idx1 eq ""} then {
return true
}
if {[$window compare $idx0 < $idx1]} then {
return false
} else {
return true
}
}
}
set wordStart {
{text index} {
# index am Anfang oder vor Leerzeichen?
if {[$text compare $index == 1.0] ||
[regexp {[\s-]} [$text get $index-1chars]]} then {
return true
} else {
return false
}
}
}
bind TextEditor <Key-quotedbl> [subst -nocommand {
if {[apply {$insideTag} %W insert]} then continue
if {[apply {$wordStart} %W insert]} then {
%W insert insert $doubleOpen
} else {
%W insert insert $doubleClose
}
break
}]
bind TextEditor <Key-quoteright> [subst -nocommand {
if {[apply {$insideTag} %W insert]} then continue
if {[apply {$wordStart} %W insert]} then {
%W insert insert $singleOpen
} else {
%W insert insert $singleClose
}
break
}]
bind TextEditor <Escape><Key-quotedbl> {
%W insert insert \"
break
}
bind TextEditor <Escape><Key-quoteright> {
%W insert insert '
break
}
switch -exact -- $lang {
en-AM {
bind TextEditor <Key-minus><Key-space> {
if {[regexp {\s} [%W get insert-2c]]} then {
%W delete insert-2c insert
%W insert insert \u200b—\u200b
break
}
}
}
default {
bind TextEditor <Key-minus><Key-space> {
if {[regexp {\s} [%W get insert-2c]]} then {
%W delete insert-1c
%W insert insert –
}
}
}
}
}
namespace eval TextEditorBindings {
bind TextEditor <Double-Button-1> {
set tk::Priv(selectMode) word
if {[TextEditorBindings::selectToClosingChar %W %x %y]} then break
}
bind TextEditor <Shift-Button-1> {
if {$tk::Priv(selectMode) ne "word"} then continue
if {[TextEditorBindings::selectToClosingChar %W %x %y]} then break
}
bind TextEditor <B1-Motion> {
if {$tk::Priv(selectMode) ne "word"} then continue
if {[TextEditorBindings::selectToClosingChar %W %x %y]} then break
}
}
proc ::TextEditorBindings::textToEditor win {
set idx [lsearch [bindtags $win] TextEditor]
if {$idx < 0} then {
bindtags $win [lreplace [bindtags $win] 1 0 TextEditor]
}
$win configure -undo yes
}
proc ::TextEditorBindings::editorToText win {
set idx [lsearch [bindtags $win] TextEditor]
bindtags $win [lreplace [bindtags $win] $idx $idx]
$win configure -undo yes
}
proc ::TextEditorBindings::textEditor {win args} {
text $win -wrap word {*}$args
textToEditor $win
set win
}
proc ::TextEditorBindings::widgetTagIndex {text from to} {
set startIdx [$text search < $from $to]
if {$startIdx eq ""} then return
set endIdx [$text search > $startIdx $to]
if {$endIdx eq ""} then return
list [$text get $startIdx $endIdx+1chars] $startIdx
}
proc ::TextEditorBindings::formOfTag tag {
if {[regexp {<\s*/} $tag]} then {
return close
} elseif {[regexp {/\s*>} $tag] ||
[regexp {<\s*[[:punct:]]} $tag]} then {
return empty
} else {
return open
}
}
proc ::TextEditorBindings::nameOfTag tag {
lindex [split [string trim $tag </> ]] 0
}
proc ::TextEditorBindings::openingTagPattern openingTag {
append pattern < {\s*} [nameOfTag $openingTag] .*? >
}
proc ::TextEditorBindings::closingTagPattern openingTag {
append pattern < {\s*} / [nameOfTag $openingTag] {\s*} >
}
proc ::TextEditorBindings::widgetTokenList {text idx to {result {}}} {
while true {
lassign [widgetTagIndex $text $idx $to] tag idx
if {$idx eq ""} then break
lappend result $tag $idx
set idx [$text index $idx+[string length $tag]chars]
}
set result
}
proc ::TextEditorBindings::widgetRangeTagsBalanced {text from to} {
set level {}
foreach {tag idx} [widgetTokenList $text $from $to] {
set name [nameOfTag $tag]
switch [formOfTag $tag] open {
dict incr level $name
} close {
dict incr level $name -1
if {[dict get $level $name] < 0} then {
return false
}
}
}
foreach key [dict keys $level] {
if {[dict get $level $key] != 0} then {
return false
}
}
return true
}
namespace import\
::TextEditorBindings::textEditor\
::TextEditorBindings::textToEditor\
::TextEditorBindings::editorToText\
::TextEditorBindings::textEditorQuotes
proc ::TextEditorBindings::addTag {window tag start end} {
if {[widgetRangeTagsBalanced $window $start $end]} then {
while {[regexp {\s} [$window get $start]]} {
$window tag remove sel $start
if {[$window tag ranges sel] eq ""} then return
}
while {[regexp {\s} [$window get $end-1chars]]} {
$window tag remove sel $end-1chars
if {[$window tag ranges sel] eq ""} then return
}
$window edit separator
$window insert $end </$tag> sel
$window insert $start <$tag> sel
$window edit separator
if {[$window compare insert < sel.first]} then {
$window mark set insert sel.first
} elseif {[$window compare insert > sel.last]} then {
$window mark set insert sel.last
}
}
}
proc ::TextEditorBindings::delTag {window start end} {
while {[regexp {\s} [$window get $start]]} {
$window tag remove sel $start
if {[$window tag ranges sel] eq ""} then return
}
while {[regexp {\s} [$window get $end-1chars]]} {
$window tag remove sel $end-1chars
if {[$window tag ranges sel] eq ""} then return
}
set tokenList [widgetTokenList $window $start $end]
if {$tokenList eq ""} then return
lassign $tokenList tag0 idx0
if {[$window compare $idx0 != $start]} then return
lassign [lrange $tokenList end-1 end] tag1 idx1
if {[$window compare $idx1+[string length $tag1]chars != $end]} then return
if {[widgetRangeTagsBalanced $window $idx0+[string length $tag0]chars $idx1]} then {
$window edit separator
$window del $idx1 $idx1+[string length $tag1]chars
$window del $idx0 $idx0+[string length $tag0]chars
$window edit separator
if {[$window compare insert < sel.first]} then {
$window mark set insert sel.first
} elseif {[$window compare insert > sel.last]} then {
$window mark set insert sel.last
}
}
}
bind TextEditor <Button-3> {
if {[%W tag ranges sel] eq ""} then {
tk_popup .textEditorContextMenu %X %Y
} else {
tk_popup .textEditorSelContextMenu %X %Y
}
}
destroy .textEditorContextMenu .textEditorSelContextMenu
menu .textEditorContextMenu -tearoff no
.textEditorContextMenu add command -label Paste -command {
event generate [focus] <<Paste>>
}
.textEditorContextMenu add command -label "select all" -command {
[focus] tag add sel 1.0 end-1c
}
.textEditorContextMenu add separator
.textEditorContextMenu add cascade\
-label Quotes\
-menu [menu .textEditorContextMenu.quotes]
apply {
args {
foreach {label sign} $args {
.textEditorContextMenu.quotes add command\
-label $label\
-command "
::TextEditorBindings::textEditorQuotes $sign
"
}
}
} German de English en American en-AM French fr Swiss ch None C
menu .textEditorSelContextMenu -tearoff no
.textEditorSelContextMenu add command -label cut -command {
event generate [focus] <<Cut>>
}
.textEditorSelContextMenu add command -label copy -command {
event generate [focus] <<Copy>>
}
.textEditorSelContextMenu add command -label "select all" -command {
[focus] tag add sel 1.0 end-1c
}
.textEditorSelContextMenu add separator
.textEditorSelContextMenu add cascade\
-label Inline\
-menu [menu .textEditorSelContextMenu.inlinetag -tearoff no]
apply {
args {
foreach tag $args {
.textEditorSelContextMenu.inlinetag add command\
-label $tag\
-command [subst -nocommand {
::TextEditorBindings::addTag [focus] $tag sel.first sel.last
}]
}
}
} a q abbr em strong b i span
.textEditorSelContextMenu add cascade\
-label Block\
-menu [menu .textEditorSelContextMenu.blocktag -tearoff no]
apply {
args {
foreach tag $args {
.textEditorSelContextMenu.blocktag add command\
-label $tag\
-command [subst -nocommand {
::TextEditorBindings::addTag [focus] $tag sel.first sel.last
}]
}
}
} p h1 h2 h3 h4 h5 h6 blockquote div
.textEditorSelContextMenu add cascade\
-label List\
-menu [menu .textEditorSelContextMenu.listtag -tearoff no]
apply {
args {
foreach tag $args {
.textEditorSelContextMenu.listtag add command\
-label $tag\
-command [subst -nocommand {
::TextEditorBindings::addTag [focus] $tag sel.first sel.last
}]
}
}
} ul ol dl li dt dd
.textEditorSelContextMenu add cascade\
-label Document\
-menu [menu .textEditorSelContextMenu.doctag -tearoff no]
apply {
args {
foreach tag $args {
.textEditorSelContextMenu.doctag add command\
-label $tag\
-command [subst -nocommand {
::TextEditorBindings::addTag [focus] $tag sel.first sel.last
}]
}
}
} html head title body
.textEditorSelContextMenu add command\
-label "Remove outermost tags"\
-command {
::TextEditorBindings::delTag [focus] sel.first sel.last
}
.textEditorSelContextMenu add separator
.textEditorSelContextMenu add cascade\
-label Quotes\
-menu [.textEditorContextMenu entrycget Quotes -menu]