Issue when using base.accept("window-event") and also setting window size outside of window-event

Here is the simplest code snippet to show the issue:

import panda3d.core

from direct.showbase.ShowBase import ShowBase

base = ShowBase()

def set_window_size(arg):
	print("ran")
	
	props1 = panda3d.core.WindowProperties()
	props1.set_size(320, 100) 
	base.win.request_properties(props1)

base.accept("window-event", set_window_size)

props2 = panda3d.core.WindowProperties()
props2.set_size(1200,100)
base.win.request_properties(props2)

base.run()

The issue is that base.win.request_properties(props2) in this case seems to be overriden by window-event. It seems like window-event is not just invoked when a user touches the window UI and resizes it, and is called twice in any case.

Now here’s a real life example why I want to both set resolution inside a window-event and also in code and outside of the window-event:
In the real program, my code checks for a configuration file for the game’s settings , to use the last window size from previous session (unless the file has been deleted or corrupted, in which case, the hardcoded PRC value remains). And the window-event function is just there to make sure the window size never goes below a specific values, so that my UI fits in the window.

To set the resolution to be the same one as in the last session, by using the values stored in the program’s file, you want to only do that on startup.

On the other hand, making sure the window is not minimized by the user to a size not acceptable for your UI, that you want to do with a window-event and continously as the program is used, not during startup.

Is this due to a bug, or this is expected? In any case, what can be done?

window-event is called whenever the operating system sends an event that some window property has changed, which may be a size, its origin, its position in the stacking order, or something else entirely.
You’re responsible for checking that the property that has changed is something you’re interested in handling. In fact, the window-event is the main way that you find out the size you actually got in response to your request.

Because keep in mind that request_properties is exactly that: a request to change properties. The window manager may honour, change or ignore the request at its pleasure. I am personally using a tiling window manager, which enforces its own sizing for windows, so all your code would do is create a feedback loop that would needlessly consume resources with useless requests. So I highly recommend against changing the window size as a response to a window event.

All this is just a long-winded way of saying that I don’t think there’s a way to get the behaviour you want. If you really wish to try and enforce a minimum or maximum size, you should open a feature request on GitHub requesting a min_size or max_size window property (but there would still be no hard guarantee that they would be honoured).

Just as an aside, don’t do base.accept("window-event"). Create your own DirectObject instead. Or if you do use base.accept, call to ShowBase’s own windowEvent handler in your own event handler. Otherwise you will override ShowBase’s own mechanisms for updating the aspect ratio of the lens and such properly.

Let me clarify, the issue is not that I can’t get a minimum window size to work. That part works fine, I’ve used that for quite some time and the program can render at 600fps.
The issue is the window-event clashes with my attempt to set the window size by code outside of the window-event, once during startup, to set the window size from last session.

Could you not just store the desired size and have it be set the first time your window-event handler runs, like this?

initial_size = (1200, 100)

def set_window_size(arg):
    print("ran")
    global initial_size
    
    props1 = panda3d.core.WindowProperties()
    if initial_size:
        props1.set_size(*initial_size)
        initial_size = None
    else:
        props1.set_size(320, 100)
    base.win.request_properties(props1)

If you run the code, you will see that the window-event is triggered twice, not once, by the engine, even if you comment out the request_properties outside of the event, and I’m not sure if two times is a fixed value or will depend on each user’s system.
This code:

import panda3d.core

from direct.showbase.ShowBase import ShowBase

base = ShowBase()

def set_window_size(arg):
	print("ran")
	
	props = panda3d.core.WindowProperties()
	props.set_size(320, 100) 
	base.win.request_properties(props)

base.accept("window-event", set_window_size)

base.run()

shows the window-event function called twice on my system:

Known pipe types:
  wglGraphicsPipe
(all display modules loaded.)
ran
ran

And I thought a counter and conditional inside the window-event function was a hacky approach, and I would be ignoring a potential bug with the engine.

That is, I think, expected: I suspect that the first call is produced during the setup of the window, and the second is produced by the response given to “request_properties”.

Why so, if I may ask?

Returning to your original code, if I add a conditional in the window-event to check that the window’s size is smaller than the minimum, then the code seems to work as expected: having “props2” apply a large window-size produces a large window, and having it apply a small window-size results in the minimum window-size.

The modified code:

import panda3d.core

from direct.showbase.ShowBase import ShowBase

base = ShowBase()

def set_window_size(arg):
	print("ran")
	
	if arg.getXSize() < 320 or arg.getYSize() < 100:
		props1 = panda3d.core.WindowProperties()
		props1.set_size(320, 100) 
		base.win.request_properties(props1)

base.accept("window-event", set_window_size)

props2 = panda3d.core.WindowProperties()
props2.set_size(1200,100) # Produces a large window
#props2.set_size(100,100) # Produces a window of minimum size
base.win.request_properties(props2)

base.run()

(Note, by the way, that at current the window-event interferes with the closing of the window on my system, presumably as the event isn’t passed on to the underlying system. This can, I think, be dealt with either by passing the event on (I’m not sure offhand of how that’s done), or by accepting it not from the core ShowBase object but instead from a separate DirectObject. The latter can be done something like this, I believe:

from direct.showbase.DirectObject import DirectObject

do = DirectObject()
do.accept("window-event", set_window_size)

)

Few reasons

  1. It seems like a band-aid solution, I know it works but I don’t know why I need this workaround and I don’t know how it solves the issue.
  2. I’m not certain it is a perfect solution, or will fail for some users: on my machine I need the counter to go to 2, but will it be the same on a Mac, different Windows version, Linux or future versions of Windows Mac and Linux, or will they require a higher count? I could increase the count to, say, 5, but that would again be a band-aid “fingers-crossed” solution.

Well ask yourself the question, why does my code where there is no conditional not work, and when you add a conditional in window-event, it works? Because when your window-event is ran the first time, the window resolution from the PRC file is used, which is higher than 320x100, so the window-event function window resizing is simply not ran. If it did run, it would interfere/overwrite with request_properties() outside of the window-event, regardless of whether base.accept(“window-event”) was before or after it in the code. That’s the issue I showed in my code.

I don’t know what you mean by both interferes as well as “window closing”. Please clarify, as it works on my two x64 Windows 10 systems.

I don’t know what you mean by “passing the event on”.

I mentioned this in another topic. The manual does not seem to clarify what DirectObject is even used for, how it is different from ShowBase and when you should use it, and when you should use it over ShowBase. In fact the only way it seems like newcomers to the engine can learn about DirectObject is the code snippets and sample programs, where sometimes ShowBase is used, sometimes DirectObject is used, sometimes the whole code is wrapped in a class and the class inherits from DirectObject. No clarification seems to exists about when to use each of these 3 approaches. So even in this solution, I don’t understand why it will work differently from base.request_properties().

Thanks

That’s fair. I believe that it solves the issue by only actually running the “request_properties” call within “set_window_size” if the window is actually too small–otherwise it runs it whenever any window-event is called, and resets the window-size. Since changing the window-size prompts a window-event, without the conditional changing the window-size prompts a reset of the window-size.

Hmm… With the conditional that I posted above, there should be no call for a counter–are you seeing one with the approach that I showed?

Otherwise, this is a fair worry, I do think!

I don’t think that this is true–at least on my machine. I do seem to see window-events as soon as the window is opened, even if I remove all code that changes the window-size.

Hence the issue that I outlined above: when the size-reset is run regardless of the state of the window, it keeps resetting the window-size to the minimum, even when the size is already above the minimum.

Ah, fair. What I’m saying is this:

If I run your code from a terminal (I’m doing this in Linux), and close the window via the “close” button in the title-bar, the window-closes–but in the terminal the associated process is still running. (Until I kill it.)

So, closing the window appears to work, but may be leaving an orphaned process behind.

In short, in an event-driven system it’s possible that multiple objects will want access to a given event. When a single object (in this case the ShowBase instance) handles events for multiple such objects, it passes the event to whichever is first in line–and then trusts that this object will re-emit the event to pass it on, if it’s still valid.

But if the event is still valid, and the object doesn’t pass it on, then other objects behind it in line may never receive the event. (To be clear, this is not an uncommon issue in such event-driven systems, I believe.)

In this case, there’s presumably some object within the engine that’s waiting for window-events, and that relies on them for various tasks, such as handling internal matters related to closing a window.

But your “accepting” of the window-event takes priority over that internal object, causing your method to take possession of the event before it reaches that engine-internal object. And because said method doesn’t re-send the event, the engine-internal object never gets it, and thus doesn’t do whatever it usually does when the window is closed.

I think that this sort of use of DirectObject is perhaps considered somewhat-advanced–not the sort of thing that one wants to put in front of new devs.

It is an issue that there are some samples that build their programs around DirectObject and some that build it around ShowBase, I do feel.

As to ShowBase, to the best of my knowledge it’s a specialised sub-class of DirectObject.

It doesn’t work differently. In fact, it works because it’s the same.

Simply put, what we’re doing here is setting up the event on an event-handling object other than the game’s central event-handling object (the latter being in this case ShowBase). Thus when the window-event is emitted, there are two separate “queues” for events, one in ShowBase, and one in the separate DirectObject instance, and the event is received by both. Thus it’s no longer a problem that the method that handles the event in your code doesn’t pass on that event.

The other solution would be, as mentioned, to have your event-method pass on the event. But I’m not sure that I’ve done that myself, and so don’t have an example of it to-hand–looking over my old code, it seems that I’ve generally used the “separate DirectObject” approach.

I’m getting a bit confused trying to follow the comments, sorry.
My point in the end of the day is that I still don’t know why window-event function which sets resolution causes the resolution setting code after it in the execution order to be skipped.
We can try to find ways to have it not behave like so, but if we are not addressing the core issue I fear we are leaving the floodgates open for any potential issue caused by this but not spotted right now.

I don’t think we also clarified the difference between DirectObject and ShowBase. You said they are similar, and ShowBase inherits from DirectObject. Okay, still not clear why both exist and when to use which one.

From what I understand, ShowBase is a singleton. So I would need another DIrectObject instance to be able to have another event handler anyway. But why would you want to set up an event with a different event setting object than the game’s main event setting object? Shouldn’t you have only one event manager/handler? isn’t it a good design to have one of each manager such as task manager, camera manager, event handler, collision handler, etc? Otherwise it sounds like you will either cause a crash (like two task managers trying to take control of the main loop) or will just make debugging harder (like which collision handler failed to function as expected or how same collision event got triggered twice per frame).

Okay, fair. I’ll try to keep this reply short and to-the-point.

[edit]
Looking at the resulting post, I may have failed. Well, I tried! :stuck_out_tongue:
[/edit]

Let me start by saying: What you’re seeing is pretty much the behaviour that I would expect, given the code in place. This doesn’t appear to be an engine bug.

What’s happening, I believe, is this:

  • You’re registering a method to be called when a window-event occurs.
    • Note that this is any window-event–it will be called when the window is moved, resized, and I would imagine when it’s focussed, unfocussed, created, or closed.
  • In this event you’re requesting a change to the size of the window to the minimum.

Thus, when you later attempt to change the size of the window, a window-event occurs as a result, which causes the code to encounter the request to change the size of the window to the minimum, and thus the latter overrides the former.

(I’m honestly not sure of why you’re not getting an infinite loop of window-events that change the window-size and thus spawn window-events. Perhaps there’s some guard against that, or perhaps I’m wrong about the setting of the window-size spawning a window-event, and it’s some other window-event that’s occurring naturally and thus hitting the code that changes the window-size.)

Whether to use one or the other as the core of a program–the base-class for the main “application” class–is a whole discussion in and of itself. To some degree I think that it’s a matter of style, of personal preference in implementation: ShowBase provides some conveniences, and imposes some restrictions (such as there being only one instance of ShowBase per application, I think that it is).

However, outside of that core application-class, only DirectObject should be used, I believe: there can be only one ShowBase, but there may be many DirectObjects.

Put another way, ShowBase is a special class that has, I think, two specific purposes: to be the basis for an application-class, and to expose some global variables. All other duties are performed by DirectObject. (And I think that the global variables are now deprecated.)

That’s… starting to get into complex program-design stuff, I fear, and may be a long discussion. Suffice it to say that there are good reasons to have multiple event-handlers, I do feel, and that it works perfectly well.

Not necessarily–it depends on the context in which they’re being used, I would say.

I do apologise that we haven’t answered your actual question yet. I just had a chance to boot up my Windows computer again and test out your code for myself. (I couldn’t test it on Linux because I use a tiling window manager there which doesn’t honour window size requests.)

It isn’t skipped, the problem is that the event queue still contains unprocessed events from the initial window-events from opening the window. (The reason events are queued up is that it would hurt performance to make calls to Python from the Draw thread, where these events are generated.) So I think what happens is this:

  1. The window opens, generating a window-event with size 800x600, the default size specified in Config.prc.
  2. Before the event is handled, you generate a request to resize it to 1200x100.
  3. Before that request has a chance to get handled, the event loop runs and fires off your window-event.
  4. The window-event requests a size of 320x100, overriding your previous request.

Adding this to your code somewhere between the base.accept() and base.win.request_properties(props2) calls seems sufficient to fix the problem:

base.eventMgr.doEvents()

Now I do see a window-event come in that performs a resize to 1200x100. Of course, that still gets immediately overridden to 320x100, because your event handler makes a new request.

But I suggest that if you wish to set the initial size of a window, you request it through the win-size configuration variable (which can be set programmatically) or by setting it in the WindowProperties that is (which requires passing windowType='none' to the ShowBase constructor and manually calling base.open_default_window(props=props).

All my previous recommendations about not making assumptions about the requests being honoured still apply. I plan to address the window-event usability defect in 1.11.

2 Likes