Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

tkinter copy-pasting to Entry doesn't remove selected text

When I copy some text and paste (crtl + v) it in a tkinter Entry , if there is selected text, it won't remove it from the entry. I'm on Linux (Mint) 64-bit.

Here I copy "d" with (ctrl + c): enter image description here

Then I select "b": enter image description here

Now I paste "d" (ctrl + v) onto it but the result is this: enter image description here

First: I want to know if this is a bug specific to Linux or this is how it was supposed to be?

Second: I was thinking of a workaround for this with validatecommand but I got another problem:

If I am to remove the selected text in a command, I have to know the index of selection in the entry. Otherwise if there are multiple instances of the selected text directly after and before the cursor, I wouldn't know which one to delete and replace with the new text. Because the cursor could be on either side of the selection (depending on if the person dragged the mouse form right to left or left to right on the text).

Now is there a way to get the index of selection in the entry? or another way to workaround this problem?

Here's some code with an example of the problem:

import tkinter as tk

root = tk.Tk()

def validation(after_text, before_text, validation_text, cursor_index):
    cursor_index = int(cursor_index)
    print('cursor index:', cursor_index)
    print('text after change:', after_text)
    print('text before change:', before_text)
    print('text in need of validation:', validation_text)

    try:
        selection = root.selection_get()
    except:
        selection = ''
    print('selection:', selection)

    # EXAMPLE:

    # validation_text = 'd'
    # before text = "bb"

    # now if someone dragged from right to left on the 2nd b:
    # cursor position will be 1 (imagine | as the cursor): 'b|b' 
    # cursor_index = 1
    # after_text = 'bdb' --> but should be 'bd'

    # now if someone dragged from left to right on the 2nd b:
    # cursor position will be 2 (imagine | as the cursor): 'bb|' 
    # cursor_index = 2
    # after_text = 'bbd' --> but should be 'bd'

    # so there is no way for me to know which of these b's I have
    # to replace with d based on cursor position alone. I have to
    # know the index of selection itself in before_text to be
    # able to replace the text properly.

    # I don't know how to get that.

    return True

txtvar = tk.StringVar(value = 'a-b-c-d-e')
entry = tk.Entry(root, textvariable = txtvar)
font = tk.font.Font(family = entry.cget('font'), size = -50)

entry.config(validate = 'all',
    vcmd = (root.register(validation),'%P', '%s', '%S', '%i'),
    font = font)
entry.pack()

root.mainloop()
like image 452
Arash Rohani Avatar asked Mar 07 '23 20:03

Arash Rohani


1 Answers

It's not a bug. If it were a bug, somebody would have noticed it and fixed it a decade ago. Tkinter has been around a long time, and fundamental things like this don't go unnoticed.

The implementation of paste on X11-based systems will not delete the selected text before pasting. The following is the actual underlying Tcl code as of the time I write this:

bind Entry <<Paste>> {
    global tcl_platform
    catch {
        if {[tk windowingsystem] ne "x11"} {
            catch {
                %W delete sel.first sel.last
            }
        }
        %W insert insert [::tk::GetSelection %W CLIPBOARD]
        tk::EntrySeeInsert %W
    }
}

Using the validation feature is definitely the wrong way to solve this. Validation is specifically for what the name implies: validation. The right solution is to create your own binding to the <<Paste>> event.

Now is there a way to get the index of selection in the entry? or another way to workaround this problem?

Yes, the entry widget has the special index sel.first which represents the first character in the selection, and sel.last represents the character just after the selection.

A fairly literal translation of the above code into python (minus the check for x11) would look something like this:

def custom_paste(event):
    try:
        event.widget.delete("sel.first", "sel.last")
    except:
        pass
    event.widget.insert("insert", event.widget.clipboard_get())
    return "break"

To have this apply to a specific widget, bind to the <<Paste>> event for that widget:

entry = tk.Entry(...)
entry.bind("<<Paste>>", custom_paste)

If you want to do a single binding that applies for every Entry widget, use bind_class:

root = tk.Tk()
...
root.bind_class("Entry", "<<Paste>>", custom_paste)
like image 134
Bryan Oakley Avatar answered Mar 31 '23 19:03

Bryan Oakley