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

Blinking text cursor in bottom-left corner; mouse pointer always displayed as CURSOR_TEXT on Mac OS X. #1042

Open
ciraben opened this issue Jan 26, 2024 · 16 comments
Labels
bug Something isn't working help wanted Extra attention is needed mac Mac OS X specific

Comments

@ciraben
Copy link

ciraben commented Jan 26, 2024

Describe the bug

Whenever pyglet opens a window on my machine, a text cursor flashes in the bottom-left of the window, as though the entire window is a text input box. Additionally, my mouse pointer assumes the CURSOR_TEXT icon when in the window, as long as the window is in focus - with exception below.

If the mouse pointer is hovering over the window as it gains focus, the pointer assumes the icon selected by pyglet until mouse movement. Additionally, when moving the pointer into the window from the top, there's a small boundary (between the window rect and the window bar) within which the pointer assumes the pyglet-chosen icon.

In short, pyglet windows visibly act as textboxes on my (2 week old) machine.


This behavior first appears for me after commit 74b6bc05. pyglet 2.0.7 has no such behavior, while pyglet 2.0.8-2.0.10 does.

This seems like it may be related to this comment in cocoa/pyglet-view.py:

# Create an instance of PygletTextView to handle text events.
# We must do this because NSOpenGLView doesn't conform to the
# NSTextInputClient protocol by default, and the insertText: method will
# not do the right thing with respect to translating key sequences like
# "Option-e", "e" if the protocol isn't implemented.  So the easiest
# thing to do is to subclass NSTextView which *does* implement the
# protocol and let it handle text input.
self._textview = PygletTextView.alloc().initWithCocoaWindow_(window)
# Add text view to the responder chain.
self.addSubview_(self._textview)

FWIW, removing the self.addSubview_(self._textview) line resolves the visual bug for me but breaks text editing in any textboxes in the window. Same with setting acceptsFirstResponder again (reversing part of the commit). This too removes the visual bug, but prevents textboxes from being edited.

@PygletView.method('B')
    def acceptsFirstResponder (self):
        return True

All this to say, I suspect this bug is a side-effect of subclassing NSTextView. If we can figure out how to replicate this bug, it might warrant a successor to the 'easy' solution in the comments above.

System Information:

How To Reproduce

After speaking with Benjamin on Discord (thread here), I hear he's unable to replicate on his M1 macbook with Sonoma 14.2.1.

This is the code I used to test:

import pyglet

SCREEN_WIDTH = 350
SCREEN_HEIGHT = 350

def main():

    win = pyglet.window.Window(SCREEN_WIDTH, SCREEN_HEIGHT, 'test')
    cursor = win.get_system_mouse_cursor(win.CURSOR_CROSSHAIR)
    win.set_mouse_cursor(cursor)
    pyglet.app.run()

if __name__ == "__main__":
    main()

I also tested textbox functionality with examples/text_input.py

@ciraben ciraben added the bug Something isn't working label Jan 26, 2024
@benmoran56 benmoran56 added help wanted Extra attention is needed mac Mac OS X specific labels Feb 14, 2024
@benmoran56
Copy link
Member

If anyone has an M1/M2 Mac and can replicate this issue, please let us know.

@caffeinepills
Copy link
Collaborator

caffeinepills commented Mar 1, 2024

I couldn't replicate this either on older Intel hardware.

However, NSTextView does have deprecated functions we use according to the Mac notes, so it's possible there may be some weird behavior with it in newer Mac versions. NSTextInputClient is the replacement to it according to the docs, but I do see that issue in the comments regarding Option E. I'm not sure how relevant that that bug that's described in the comment text would still be (or when it was written). I looked at other windowing libraries and they all seem to implement NSTextInputClient, so that may be the route to go.

@caffeinepills
Copy link
Collaborator

caffeinepills commented Mar 5, 2024

After a few days of investigation, I've discovered that we can't remove the NSTextView and implement the NSTextIntputClient. Essentially ctypes does not support callbacks with Structures. It can do pointers to Structures, just not the Structures themselves

Structure returns are required for these instance methods:
https://developer.apple.com/documentation/appkit/nstextinputclient/1438242-selectedrange?language=objc
https://developer.apple.com/documentation/appkit/nstextinputclient/1438250-markedrange?language=objc
https://developer.apple.com/documentation/appkit/nstextinputclient/1438240-firstrectforcharacterrange?language=objc

I did find some information from a bug/request 15 years ago: python/cpython#49960

The only fix seems to be a ctypes patch from another project, but it involves overwriting internal ctypes calls, which are subject to change. Licensing aside, not sure that'd be the route to go.

At this point we can assume it probably won't be patched or implemented in the immediate future. So we will have to try to hang onto the NSTextView as long as we can.

@ciraben
The only other solution I can think of would be a work around. Maybe explicitly set the built in Window caret to be invisible and the NSTextView to not invoke the cursor change? Unfortunately, I can't really test this behavior myself, but I did make a test branch with a couple changes to see if it would do anything.

Could you try it out and let me know if this changes anything? Thanks

https://github.com/caffeinepills/pyglet/tree/macos_nstextview

@ciraben
Copy link
Author

ciraben commented Mar 5, 2024

Thanks for taking the time to look into this! I know it's not your fave topic and I appreciate the dedication.

Testing your workaround, it hides the text caret as expected. My mouse pointer still behaves as before, switching to the CURSOR_TEXT icon on movement in the window when the window has focus. I found this SO thread which was a bit over my head, but looks like it has some ideas for what might be going on.

I'm not personally in need of a workaround (2.0.7 works for my needs, and I don't have any active pyglet project ATM) - but maybe the above is useful to someone anyway!

FWIW, I'm getting a message that examples/text_input.py isn't migrated to pyglet 2.0 yet (relies on batch.add_indexed), so I tested your workaround with examples/timer.py and examples/image_display.py instead.

@caffeinepills
Copy link
Collaborator

Appreciate you checking on that. Bummer about the cursor at least.

There are still a few more tricks to try.

Could you try adding this. It's supposed to set the view as uneditable, hopefully that includes the invisible selection area:

  1. self.setEditable_(False) in this area?
    https://github.com/caffeinepills/pyglet/blob/65fda4fd0cf85a46f17d5b66755893b2103db8f4/pyglet/window/cocoa/pyglet_textview.py#L29

  2. If it is indeed the mouse motions, you can also try adding this:

    @PygletTextView.method('v@')
    def mouseMoved_(self, nsevent):
        send_super(self, 'mouseMoved:', nsevent)

Which should capture the mouse moved from the Text view and pass it to the regular view, hopefully that intercepts any cursor change stuff going on.

See if any of those change anything. I think the first option might work, but try the second to see if anything changes if not. I'd like to narrow down the issue whenever you have time.

@HerbeMalveillante
Copy link

If anyone has an M1/M2 Mac and can replicate this issue, please let us know.

I have the exact same issue on a 2020 M1 MacBook pro running macOS Sonoma 14.3.1, python 3.12.2 and pyglet 2.0.14

steps to reproduce :

  1. Create a new virtual environment
  2. Run python3.12 -m pip install --upgrade pip pyglet
  3. Enter the minimal boilerplate to get a window to show up :
import pyglet
window = pyglet.window.Window()
pyglet.app.run()

image

You can see a small blue line in the bottom-left corner of the screen. It's blinking at ~1Hz.
The mouse cursor is changed to the text cursor icon as well.

It's my first time ever trying Pyglet, so I think I might simply downgrade until this bug is fixed, but I can totally share more details about my configuration if needed.

image

@HerbeMalveillante
Copy link

I have some news ! I'm not sure exactly what happened, but I reinstalled Pyglet using pip in my environment, and I can't seem to reproduce the issue anymore !

It's really weird as the Pyglet version is the exact same, and nothing in my configuration has changed. Maybe Apple did an update of its own ?

Anyways, I'll let you know if I manage to reproduce this bug again. It's now time to learn how to use the library !

@benmoran56
Copy link
Member

Thanks for the feedback @HerbeMalveillante.
We did push one small bugfix in the latest point release. If you're curious to try, you might compare with previous releases to confirm if that was it.

@eruvanos
Copy link

eruvanos commented Mar 13, 2024

Updated to master, still the same :/
Maybe it is possible to report this behavior as a bug in >=14.3?

@eruvanos
Copy link

Adding this code to the PygletTextView_Implementation class fixes the blinking cursor.

    @PygletTextView.method("v")
    def drawRect_(self):
        pass

Following the documentation of AppKit
It looks like we could change the cursor behaviour. The cursor API seems to be legacy following the information from trackingAreas.

@eruvanos
Copy link

eruvanos commented Mar 30, 2024

Just updated to 14.4.1 and everything works again as expected

Can somebody else confirm?

@gcollura
Copy link

gcollura commented Apr 9, 2024

I'm still seeing the blinking cursor at the bottom right of the window. macOS 14.4.1 (23E224), pyglet ==2.0.15 and python3.12

@eruvanos
Copy link

eruvanos commented Apr 15, 2024

You are right, it was only one time.

Again same issue, whatever I do :/

@ArielAlon24
Copy link

I had the same issue - a blinking cursor in the bottom left corner on a macOS 14.2.1 and pyglet 2.0.15.
The fix for PygletTextView_Implementation provided here worked.

@ciraben
Copy link
Author

ciraben commented May 14, 2024

Quick update here! New OS version ✨
OS 14.4.1, pyglet 2.0.15, python 3.12.3

tl;dr - blinking text cursor resolved by suggested pyglet patch above. wrong mouse cursor partly resolved by subclassing Window and setting cursor type on each mouse motion event.

I tried out eruvanos' PygletTextView_Implementation idea. This removed the bottom-left blinking text cursor for me - yay!

However, my mouse pointer is still reset to the CURSOR_TEXT icon on mouse motion (while within the window rect, while the window is in focus). I got this result by testing with HerbeMalveillante's simple code example.

I also went back and re-tested with text_input.py and I get better results now. I'm not able to replicate what I described initially back in January regarding the mouse cursor. I can't recall whether I misreported, or whether I really did see solely CURSOR_TEXT when running text_input.py back then.

Anyway, I now see the correct mouse cursor (CURSOR_DEFAULT) displayed over the widget-less regions of the window. As I move the mouse cursor across these regions, the CURSOR_TEXT cursor occasionally flickers into view for a frame (~once/sec). Over the window bar, CURSOR_TEXT is still displayed there as well.

Suspecting a race condition, I tested the following code out (based on examples/text_input.py, L69-L70):

import pyglet
from time import sleep

lag = 0
# lag = 0.05

class CoolWindow(pyglet.window.Window):
  def on_mouse_motion(self, x, y, dx, dy):
    sleep(lag)
    self.set_mouse_cursor(None)

window = CoolWindow()
pyglet.app.run()

With lag = 0, I see the same flickery mouse behavior that I see with text_input.py. With lag = 0.05, the cursor is solidly CURSOR_TEXT until mouse motion stops. The CURSOR_DEFAULT to CURSOR_TEXT frame ratio increases predictably as lag approaches 0.

So I think we can safely conclude that the OS and pyglet are racing to set cursor type on mouse motion, with varying results. Maybe this is why people above were seeing different results from day to day; and why I can't replicate my January results.

I wonder whether this race condition arises in each mouse motion event, or only when many events are triggered in quick succession.

@ciraben
Copy link
Author

ciraben commented May 14, 2024

Following up on caffeinepills' original suggestions specifically -

Adding self.setEditable_(False) removes the bottom-left blinking text cursor.

Adding the suggested mouseMoved_ function has no visible effect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed mac Mac OS X specific
Projects
None yet
Development

No branches or pull requests

7 participants