Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entry Widget not editable on Windows 10, but is on Linux Ubuntu 16.04 #87033

Closed
jmccabe mannequin opened this issue Jan 8, 2021 · 17 comments
Closed

Entry Widget not editable on Windows 10, but is on Linux Ubuntu 16.04 #87033

jmccabe mannequin opened this issue Jan 8, 2021 · 17 comments
Labels
3.8 only security fixes 3.9 only security fixes 3.10 only security fixes topic-tkinter type-bug An unexpected behavior, bug, or error

Comments

@jmccabe
Copy link
Mannequin

jmccabe mannequin commented Jan 8, 2021

BPO 42867
Nosy @terryjreedy, @tiran, @serhiy-storchaka, @E-Paine, @jmccabe

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = <Date 2021-01-09.00:44:15.910>
created_at = <Date 2021-01-08.12:46:12.432>
labels = ['3.8', 'type-bug', 'expert-tkinter', '3.9', '3.10']
title = 'Entry Widget not editable on Windows 10, but is on Linux Ubuntu 16.04'
updated_at = <Date 2021-01-11.10:27:51.881>
user = 'https://github.com/jmccabe'

bugs.python.org fields:

activity = <Date 2021-01-11.10:27:51.881>
actor = 'jmccabe'
assignee = 'none'
closed = True
closed_date = <Date 2021-01-09.00:44:15.910>
closer = 'terry.reedy'
components = ['Tkinter']
creation = <Date 2021-01-08.12:46:12.432>
creator = 'jmccabe'
dependencies = []
files = []
hgrepos = []
issue_num = 42867
keywords = []
message_count = 17.0
messages = ['384655', '384657', '384660', '384663', '384664', '384666', '384668', '384672', '384678', '384682', '384702', '384716', '384748', '384778', '384779', '384785', '384808']
nosy_count = 5.0
nosy_names = ['terry.reedy', 'christian.heimes', 'serhiy.storchaka', 'epaine', 'jmccabe']
pr_nums = []
priority = 'normal'
resolution = 'third party'
stage = 'resolved'
status = 'closed'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue42867'
versions = ['Python 3.8', 'Python 3.9', 'Python 3.10']

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 8, 2021

I've built an application using tkinter (see below). I'm fairly new to tkinter, despite having substantial software experience (33 years), so the layout might be a bit odd/wrong, and the example obviously doesn't follow PEP-8 guidelines, however...

Basically this application (which is a bit more than minimal, but should be easy to follow) is designed to manage pairs of text values as JSON, saving/loading from a file. When it starts, the user is asked if they want to read in an existing file of data; if they select NO, a fairly empty frame with 3 buttons showing "+", "Save" and "Quit" is displayed.

At this point, if "+" is pressed, a new row with two labels and two Entry widgets is added to the frame.

However (and this is the problem which appears to be identical to that reported in Issue bpo-9673), it is not possible to set focus into either of the Entry widgets on Windows 10; there is no problem doing this on Ubuntu 16.04 (although I've only got Python 3.5 on there)

If the "Save" button is then pressed, a message box pops up telling the use that no changes have been saved. Once OK has been pressed on that, it becomes possible to set focus into the Entry widgets.

One of the problems with Issue bpo-9673 was that no 'minimal' example was provided showing this behaviour, and the very minimal example given in https://bugs.python.org/issue9673#msg218765 doesn't exhibit the problem, hence the example below being a bit more than minimal, while still not being particularly complicated.

====
#!/usr/bin/python3

import os
import tkinter as tk
from tkinter import filedialog, messagebox
import json

class Application(tk.Frame):

    def __init__(self, master):
        super().__init__(master)
        self.master = master
        self.grid()
        self.originalJson = {}
        self.inFileName = ""
        self.leftRightEntries = []
        self.fileDlgOpts = { "initialdir"       : os.getcwd(),
                             "initialfile"      : "file.json",
                             "filetypes"        : (("JSON File", "*.json"), ("All Files","*.*")),
                             "defaultextension" : '.json',
                             "title"            : "Select A File" }
        self.createWidgets()

    def openInFile(self):
        fileName = ""
        reuse = tk.messagebox.askquestion("Use An Existing File", "Do you want to load and use an existing file?")
        if reuse == "yes":
            fileName = tk.filedialog.askopenfilename(**self.fileDlgOpts)
            if fileName is not "":
                try:
                    with open(fileName, 'r') as json_file:
                        self.originalJson = json.load(json_file)
                        json_file.close()
                except Exception:
                    tk.messagebox.showerror("Use An Existing File", "File could not be loaded; continuing without one.")
                    fileName = ""
            else:
                tk.messagebox.showwarning("Use An Existing File", "No existing file specified; continuing without one.")

        return fileName

    def createWidgets(self):
        self.inFileName = self.openInFile()

        # We add the buttons to some huge numbered row because we might want to insert more
        # rows, and the layout manager will collapse everything in between. Also we
        # add these first because of the way the tab order is set up
        self.addBtn = tk.Button(self.master, text = "+", command = self.addNew)
        self.addBtn.grid(row = 100, column = 0, sticky = tk.W)

        # Save button; pretty self-explanatory
        self.saveBtn = tk.Button(self.master, text = "Save", command = self.save)
        self.saveBtn.grid(row = 100, column = 2, sticky = tk.W)

        # Quit button; pretty self-explanatory
        self.quitBtn = tk.Button(self.master, text = "QUIT", fg = "red", command = self.quit)
        self.quitBtn.grid(row = 100, column = 3, sticky = tk.E)

        # If there is original json, work through each key and put the fields on the display
        rowNum = 0
        for leftText in sorted(self.originalJson.keys()):
            self.insertRow(rowNum, leftText);
            rowNum = rowNum + 1

        self.nextEmptyRow = rowNum

        self.redoPadding()

    def redoPadding(self):
        for child in self.master.winfo_children():
            child.grid_configure(padx = 5, pady = 5)

    def focusNextWidget(self, event):
        event.widget.tk_focusNext().focus()
        return("break")

    def insertRow(self, rowNum, initialLeft = None):
        tk.Label(self.master, height = 1, text = "Left: ").grid(row = rowNum, column = 0, sticky = tk.W)
        leftBox = tk.Entry(self.master, width = 20)
        leftBox.grid(row = rowNum, column = 1, sticky = tk.W)
        leftBox.bind("<Tab>", self.focusNextWidget)
        if initialLeft is not None:
            leftBox.insert(tk.END, initialLeft)
        tk.Label(self.master, height = 1, text = "Right: ").grid(row = rowNum, column = 2, sticky = tk.W)
        rightBox = tk.Entry(self.master, width = 20)
        rightBox.grid(row = rowNum, column = 3, sticky = tk.W)
        rightBox.bind("<Tab>", self.focusNextWidget)
        if initialLeft is not None:
            rightBox.insert(tk.END, initialLeft)
        self.leftRightEntries.append((leftBox, rightBox))
        leftBox.focus_set()

    def addNew(self):
        # Add a new row before the button
        self.insertRow(self.nextEmptyRow)
        self.nextEmptyRow = self.nextEmptyRow + 1
        self.redoPadding()

    def getCurrent(self):
        # Work through the rows and check stuff
        current = {}
        for (leftEntry, rightEntry) in self.leftRightEntries:
            leftText = leftEntry.get()
            rightText = rightEntry.get()
            if leftText == "" and rightText == "":
                pass
            elif leftText == "":
                print("No leftText specified for rightText [{}]".format(rightText))
            elif rightText == "":
                print("No rightText specified for leftText [{}]".format(leftText))
            else:
                print("lefText: {}, rightText: {}".format(leftText, rightText))
                current[leftText] = rightText
        return current

    def save(self):
        # Get the current values, and then dump the new json to a file, if it's changed!
        finalResult = self.getCurrent()
        if finalResult != self.originalJson:
            if self.inFileName == "":
                self.inFileName = tk.filedialog.asksaveasfilename(**self.fileDlgOpts)

            if self.inFileName != "":
                with open(self.inFileName, 'w') as json_file:
                    json.dump(finalResult, json_file, indent = 4)
                    self.originalJson = finalResult
                tk.messagebox.showinfo("Save Data", "Data saved to {}".format(self.inFileName))
            else:
                tk.messagebox.showwarning("Save Data", "Data has not been saved; no file name was supplied!")
        else:
            tk.messagebox.showwarning("Save Data", "Data has not been saved; there are no changes")

    def quit(self):
        # Deal with quitting when the file's been modified, check original vs current JSON
        reallyQuit = True
        finalResult = self.getCurrent()
        if finalResult != self.originalJson:
            answer = tk.messagebox.askquestion("Quit", "Data has changed; do you really want to quit?", icon = "warning")
            if answer != "yes":
                reallyQuit = False

        if reallyQuit:
            self.master.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    root.title("Inactive Entry Example")
    app = Application(root)
    root.protocol("WM_DELETE_WINDOW", app.quit)
    app.mainloop()

@jmccabe jmccabe mannequin added topic-tkinter type-bug An unexpected behavior, bug, or error labels Jan 8, 2021
@tiran
Copy link
Member

tiran commented Jan 8, 2021

Hi,

bugs.python.org is an issue tracker for bugs and feature requests. Please use platforms like Python user mailing list, stack overflow, or reddit for general help with Python and libraries.

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 8, 2021

Is behaviour that differs between platforms, using components that are listed in the "classification" -> "Components" section NOT a bug then?

@tiran
Copy link
Member

tiran commented Jan 8, 2021

Can your produce the issue with Python 3.8 or newer on any platform? 3.6 and 3.7 are in security fix-only mode. If it's truly a bug in Python, then we won't fix the issue any way.

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 8, 2021

It's reproducible in both 3.8 and 3.9 on Windows 10.

@jmccabe jmccabe mannequin added 3.9 only security fixes labels Jan 8, 2021
@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 8, 2021

In addition, changing "Entry" to "Text" (+ necessary associated changes) makes no difference to the outcome. Removing the call to tk.messagebox.askquestion() does. It appears that tk.messagebox.askquestion() is screwing something up on Windows; perhaps it's locking some resources, or not correctly releasing them.

@tiran
Copy link
Member

tiran commented Jan 8, 2021

I'm involving TJ and Serhiy. They might have free resources and might be able to assist.

I initially suggested to get assistance in user forums, because we have very limited resources. To give you an impression, there are more than 7,500 open bugs on BPO and more than 1,400 open PRs on Github..

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 8, 2021

Thank you.

Wrt to your initial suggestion, I recognise Python's popularity will make things busy, and I will ask if anyone knows of a workaround in other fora, but a bug's a bug and, IMO, if something behaves differently on different platforms, using packages that are part of the Python install, for no apparent reason, then that's a bug! (Especially as the same thing was reported over 10 years ago, on what would be a very different version of Python).

@E-Paine
Copy link
Mannequin

E-Paine mannequin commented Jan 8, 2021

This is a Tk/Windows issue, not tkinter. I tested the following on Windows 10 using Tk 8.6.9:

# Our entry
pack [entry .e]

# Causes the entry to fail
#tk_messageBox -title Title -message Message
#after 0 tk_messageBox -title Title -message Message

# Does not cause the entry to fail
#after 1 tk_messageBox -title Title -message Message
after idle tk_messageBox -title Title -message Message

I have not tried on a later version of Tk so it may be fixed but it also may be a fundamental Windows issue. The workaround would be to either use .after(1, ...) or .after_idle(...)

@E-Paine E-Paine mannequin added 3.8 only security fixes 3.10 only security fixes labels Jan 8, 2021
@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 8, 2021

@epaine Thank you for your comments. Although the order of events in the example you quoted isn't the same as in the application I'm using (tk.messagebox.askquestion() is called a long time before the Enter widget is created in the application, not the other way round), what you've suggested, and a bit of extra thought, has led to a solution for me. I wrapped the self.createWidgets call in self.after_idle() (so self.after_idle(self.createWidgets)) and that appears to do the job.

Many thanks; it's appreciated.

@terryjreedy
Copy link
Member

I added this note to bpo-9673 after rereading the posts and the 2010 tkinter list thread.

"My testing was inadequate. From bpo-42867, it appears that 0. the bug is limited to Windows; 1. opening a canned dialog or message box is part of getting the buggy behavior; 2. timing is involved; 3. the bug can be exhibited on Windows directly with wish/tk, so that it is a 3rd party tcl/tk issue and not a tkinter issue. Hence changing the issue resolution.

A workaround mentioned in both the referenced tkinter thread and bpo-42867 is to call after_idle() at some point."

Paine, thank you for verifying both the bug and workaround directly with tk. I am closing this also as 3rd party.

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 9, 2021

Thank you all for your time. I hope you don't feel it has been wasted since, at the very least, it confirms an issue in tkinter usage, albeit that the actual cause of the issue is TK itself.

@terryjreedy
Copy link
Member

No problem. While your failing example was 'too long', you *did* search and find the appropriate previous issue, with my inadequate response and incorrect resolution, even though closed. I appreciate having my understanding fixed.

IDLE uses several entry boxes in dialogs. I have partially replaced error message boxes with error messages in the dialog. For example, Open Module (Alt-M at least on Windows) opens a dialog to open a module. Enter a bad name and "Error: module not found" is printed in red below, leaving the cursor in the entry box so the user can correct it (or Cancel). As is turn out, this avoids any possibility of running into this bug. I might have run into it before I replaced most old dialog + error box uses, but I now know to watch out in those remaining.

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 10, 2021

Fair point about being "too" long. Having seen a "short" example that, unfortunately, didn't actually exhibit the problem, I thought that providing a "smallish" example that definitely did exhibit the issue was quicker than, potentially, spending more time than necessary cutting it down.

However, for the record, hope the below example is better. As before, changing:

self.createWidgets()

to:

self.after_idle(self.createWidgets())

avoids the issue.

-----
#!/usr/bin/python3

import tkinter as tk
from tkinter import messagebox

class Application(tk.Frame):

    def __init__(self, master):
        super().__init__(master)
        self.master = master
        self.pack()
        self.createWidgets()

    def createWidgets(self):
        tk.messagebox.askquestion("Use An Existing File", "Do you want to load and use an existing file?")
        tk.Entry(self.master, width = 20).pack()

if __name__ == "__main__":
    root = tk.Tk()
    app = Application(root)
    app.mainloop()

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 10, 2021

Apologies, I couldn't find an edit button! That last comment should've said:

"As before, changing:

self.createWidgets()

to:

self.after_idle(self.createWidgets)

avoids the issue."

@terryjreedy
Copy link
Member

With a truly minimal but reproducible example, I was able to understand and experiment. As is, widget creation is done without the event loop running. Displaying the message box shifts grabs the internal focus but also displays the unfocused tk window. When the message box is dismissed and mainloop() is called, the root window is displayed apparently with focus but it is unresponsive to mouse click on the entry box, hence the entry box never gets focus and never gets keypresses. This is a bug somewhere between tk and Windows window manager and focus management.

Adding after_idle delays the message box and entry creation until after the mainloop call, and clicking on the entry now works. In this case, the (unfocused) root window is *not* displayed.

If the entry box is the proper default focused widget when the message box is dismissed, an even better fix, to me, is to bind the entry box and explicitly give it the focus. According to the tk docs, focus_set should work. But it does not on Windows (and I suspect that this is related to clicking not working). focus_force does work.

Another solution is to add master.update() before createWidgets. Clicking on then works. Putting self.master.update() after the messagebox call does not work.

@jmccabe
Copy link
Mannequin Author

jmccabe mannequin commented Jan 11, 2021

Thank you for that information and analysis Terry. As you can see, at the end of the addNew() function in the original example, I'd added:

---

        if initialLeft is not None:
            rightBox.insert(tk.END, initialLeft)
        self.leftRightEntries.append((leftBox, rightBox))
        leftBox.focus_set()

and, as you've noticed, that makes no difference on Windows. I hadn't tried focus_force(), but will try to bear that in mind.

@ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.8 only security fixes 3.9 only security fixes 3.10 only security fixes topic-tkinter type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

2 participants