How can I customize the default Double Mouse Click behavior in a Tkinter text widget?

In tkinter text widget, by default, double clicking will select the text under the mouse.

The event will select all characters between "" (space) char.

So - suppose the text widget has: 1111111 222222

double click on the first word (all 1

) will only select it (and double click on the word 2

will select it)

I would like to have similar behavior, but add an extra char as working delimiters (eg .

, (

, )

) at the moment, if the text is 111111.222222

- double click anywhere on the text of all the characters (it will not separate the word from .

)

Is there a way to do this?

+3


source to share


2 answers


Changing what is a "word"

A double click is defined to select the "word" under the cursor. If you want to change the default behavior for all text widgets, tkinter has a way to tell it what the word character is. If you change what tkinter considers a "word", you change what is double-clicked. This requires us to directly invoke the built-in tcl interpreter that tkinter is based on.

Note. This will affect other aspects of the widget as well, such as key bindings for moving the cursor to the beginning or end of a word.

Here's an example:

import tkinter as tk

def set_word_boundaries(root):
    # this first statement triggers tcl to autoload the library
    # that defines the variables we want to override.  
    root.tk.call('tcl_wordBreakAfter', '', 0) 

    # this defines what tcl considers to be a "word". For more
    # information see http://www.tcl.tk/man/tcl8.5/TclCmd/library.htm#M19
    root.tk.call('set', 'tcl_wordchars', '[a-zA-Z0-9_.,]')
    root.tk.call('set', 'tcl_nonwordchars', '[^a-zA-Z0-9_.,]')

root = tk.Tk()
set_word_boundaries(root)

text = tk.Text(root)
text.pack(fill="both", expand=True)
text.insert("end", "foo 123.45,678 bar")

root.mainloop()

      



Custom key bindings

If you don't want to influence any widget other than one, or you don't want to influence other aspects of tkinter that depend on the definition of a word, you can create your own binding to select whatever you want.

It's important to remember that your anchor must return a string "break"

to prevent double-clicking the button by default:

def handle_double_click(event):
    <your code for selecting whatever you want>
    return "break"
...
text.bind("<Double-1>", handle_double_click)

      

To facilitate this, the text widget has a method search

that allows you to search back and forth in text for a given string or regular expression.

+2


source


Is there a way to do this?

Of course, and not even one way. But anyway, we need a custom class for our widget Text

, so let's start:

class CustomText(tk.Text):
    def __init__(self, parent, delimiters=[]):
        tk.Text.__init__(self, parent)
        #   test text
        self.insert('1.0', '1111111 222222'
                           '\n'
                           '1111111.222222'
                           '\n'
                           '1111111.222222,333333'
                           '\n'
                           '444444444444444444')
        #   binds
        self.bind('<Double-1>', self.on_dbl_click)
        self.bind('<<Selection>>', self.handle_selection)
        #   our delimiters
        self.delimiters = ''.join(delimiters)
        #   stat dictionary for double-click event
        self.dbl_click_stat = {'clicked': False,
                               'current': '',
                               'start': '',
                               'end': ''
                               }

      

Optional delimiters

results in two parameters:

  • If delimiters are provided, we can rely on RegEx search

    for delimiters.

  • If the delimiters are limited, we can rely on inline expressions, especially those two (dictionary boundaries like RegEx): wordstart

    and wordend

    . According to the docs :

    wordstart

    and wordend

    moves the index to the beginning (end) of the current word. Words are sequences of letters, numbers, and underscores, or single nonspatial characters.

The logic is simple - on double click - we track this event and store the indexes in the dictionary. After that, we process the change of selection and act according to the selected option (see above).



Here's the complete snippet:

try:
    import tkinter as tk
except ImportError:
    import Tkinter as tk


class CustomText(tk.Text):
    def __init__(self, parent, delimiters=[]):
        tk.Text.__init__(self, parent)
        #   test text
        self.insert('1.0', '1111111 222222'
                           '\n'
                           '1111111.222222'
                           '\n'
                           '1111111.222222,333333'
                           '\n'
                           '444444444444444444')

        #   binds
        self.bind('<Double-1>', self.on_dbl_click)
        self.bind('<<Selection>>', self.handle_selection)
        #   our delimiters
        self.delimiters = ''.join(delimiters)
        #   stat dictionary for double-click event
        self.dbl_click_stat = {'clicked': False,
                               'current': '',
                               'start': '',
                               'end': ''
                               }

    def on_dbl_click(self, event):
        #   store stats on dbl-click
        self.dbl_click_stat['clicked'] = True
        #   clicked position
        self.dbl_click_stat['current'] = self.index('@%s,%s' % (event.x, event.y))
        #   start boundary
        self.dbl_click_stat['start'] = self.index('@%s,%s wordstart' % (event.x, event.y))
        #   end boundary
        self.dbl_click_stat['end'] = self.index('@%s,%s wordend' % (event.x, event.y))


    def handle_selection(self, event):
        if self.dbl_click_stat['clicked']:
            #   False to prevent a loop
            self.dbl_click_stat['clicked'] = False
            if self.delimiters:
                #   Preserve "default" selection
                start = self.index('sel.first')
                end = self.index('sel.last')
                #   Remove "default" selection
                self.tag_remove('sel', '1.0', 'end')
                #   search for occurrences
                occurrence_forward = self.search(r'[%s]' % self.delimiters, index=self.dbl_click_stat['current'],
                                                 stopindex=end, regexp=True)
                occurrence_backward = self.search(r'[%s]' % self.delimiters, index=self.dbl_click_stat['current'],
                                                  stopindex=start, backwards=True, regexp=True)

                boundary_one = occurrence_backward + '+1c' if occurrence_backward else start
                boundary_two = occurrence_forward if occurrence_forward else end
                #   Add selection by boundaries
                self.tag_add('sel', boundary_one, boundary_two)
            else:
                #   Remove "default" selection
                self.tag_remove('sel', '1.0', 'end')
                #   Add selection by boundaries
                self.tag_add('sel', self.dbl_click_stat['start'], self.dbl_click_stat['end'])


root = tk.Tk()

text = CustomText(root)
text.pack()

root.mainloop()

      

In conclusion, if you don't really care about the delimiters and the words - the second option is ok, otherwise the first.

Update:

Many thanks to @Bryan Oakley for pointing out that 'break'

-string prevents the default behavior so the code can be shortened to a single callback, no more need for <<Selection>>

:

...
def on_dbl_click(self, event):
    if self.delimiters:
        #   click position
        current_idx = self.index('@%s,%s' % (event.x, event.y))
        #   start boundary
        start_idx = self.search(r'[%s\s]' % self.delimiters, index=current_idx,
                                stopindex='1.0', backwards=True, regexp=True)
        #   quick fix for first word
        start_idx = start_idx + '+1c' if start_idx else '1.0'
        #   end boundary
        end_idx = self.search(r'[%s\s]' % self.delimiters, index=current_idx,
                              stopindex='end', regexp=True)
    else:
        #   start boundary
        start_idx = self.index('@%s,%s wordstart' % (event.x, event.y))
        #   end boundary
        end_idx = self.index('@%s,%s wordend' % (event.x, event.y))

    self.tag_add('sel', start_idx, end_idx)
    return 'break'
...

      

0


source







All Articles