Friday, September 4, 2020

GWindowStyle Flag Issue

Background
While working on the WINDOWEDLOCKED flag, a feature to lock a window's size, I found an issue on Linux when creating a GWindow using either the WINDOWBORDERLESS or FULLSCREENBORDERLESS flags. When calling Create() with one of these flags, the program generated a "Bad Match" error and crashed. At the time, I added the issue to GitLab and finished up with the feature I was working on originally. Last week, I came back to the issue and found that the problem was more widespread than just the two flags.


A problem on all platforms

GWindowTest.hpp was missing a Unit Test that called Create() with each existing style flag, so I made one. After running this test on each platform, I discovered the results of using some of the flags varied. While the program didn't crash on Windows, using either of the fullscreen flags did not produce a fullscreen window. Similar problems were on the Mac. After creating a spreadsheet to track all the flags with issues, I considered there might be even more problems when using the second function that uses style flags, ReconfigureWindow(). The table below shows the results of calling Create() with each flag and if it is visually correct on each platform.


Style flag issues when calling Create()


More problems

ReconfigureWindow() can be called anytime after Create() to change the style of a GWindow. I saw a potential issue with using different combinations of calling Create() and then ReconfigureWindow(). My theory was that creating a window with one flag may affect how a window is reconfigured with another flag. To ensure all windows restyle correctly, I made another Unit Test for all the possible combinations of calling Create() and then ReconfigureWindow(). There are six flags tested against each other, 36 tests total. Running the test on each platform revealed more issues. Each table below is for one of the three different platforms Gateware runs on. The left column is the flag used when calling Create(). The top row is the flag used to call ReconfigureWindow() after calling Create(). The tables below show which combination of flags produce visually correct results on each platform.


Windows working flags calling Create() then ReconfigWindow()

Linux working flags calling Create() then ReconfigWindow()

Mac working flags calling Create() then ReconfigWindow()


Windows and Linux

Fixing the issues on Windows and Linux didn't take much time. I already had familiarity with those GWindow implementations from working on the WINDOWEDLOCKED and SetIcon() features. The tests on these operating systems are running on recent computers. As a result, the turn around time of making changes and testing happens quickly. The Mac was a more involved process. 


Mac

On the Mac, the style flag issues with calling Create() had straightforward solutions. However, fixing the problems calling ReconfigureWindow() on the Mac was not as easy as the other platforms. The Mac I am using is from 2012, running the latest version of macOS. As a result, it is a significantly slow computer to develop on. Building and running Gateware takes anywhere from 1 to 10 minutes. On this platform, the biggest issue I faced was with all the animations whenever a window changes its size or position. I have found that making changes to a mid animation window produces strange results, especially to or from fullscreen. 


For example, a fullscreen window cannot immediately minimize. The window must be brought out of fullscreen before it can minimize. If the window is transitioning out of fullscreen when minimize is called, something strange happens; the window will appear in the dock as if it had minimized; however, the animation of the window transitioning from fullscreen will continue to a windowed state that is visible on the desktop. The minimized version of the window will come out of the dock and merge with the desktop window.


There is a way to stop animations. However, this only causes more strange behavior. On the Mac, fullscreen windows exist in their own space beside the desktop. With animations turned off, bringing the window out of fullscreen and minimizing it will work as expected, but the space it existed in will remain. This behavior is likely because when animations are off, Mac developers are expected to handle specific actions that are normally performed for you. I haven't found a way to delete empty spaces, and it appears macOS developers have no way to do so at this time. Running through all of the Unit Tests results in multiple unoccupied spaces that aren't deleted until the program stops.


The solution I came up with to minimize a fullscreen window is to leave animations on, wait for the window to exit fullscreen completely, and then minimize the window from there. This solution works nicely. Unfortunately, I have to return SUCCESS from ReconfigureWindow() even though the window hasn't been told to minimize yet. However, I don't see any other way to go about it.


Most of the flag issues on the Mac were simple fixes that could be addressed with general solutions. The rest required more specific solutions to handle each particular case. The last eight flag situations I worked on took twice the amount of time to work on than the other 41. As ReconfigureWindow() began styling windows more visually correct, I started getting errors from other Unit Tests, adding to the existing list of issues.


Even more problems on the Mac

Calling Maximize() immediately followed by Minimize() causes the program to crash with an EXC_BAD_ACCESS error. I believe this might be a threading issue. The error doesn't always happen, which is a trait of threading issues. I think this issue is caused when trying to change a windows state while it is in transition to another. I've found that this can be avoided by waiting for the window to maximize fully before calling Minimize()


Another new Mac issue is the debug message "not in fullscreen state." This is being reported whenever I try to toggle out fullscreen while the window is transitioning to fullscreen. I haven't found a way to stop the transition or force it to go back. Overriding animationResizeTime() - a method of NSWindow - so that it returns 0 helps to some degree. Although I haven't noticed any differences in the window's transition animation timing, I receive less debug messages. After testing it further, this might be causing windows to stick around long after their Unit Test is over, so I won't be using this.


One last Mac issue is with IsFullscreen(). It reports when a window is fullscreen. However, it can report a window as fullscreen even though it is still transitioning to fullscreen. This misreporting is potentially an issue if a program needs to be sure the window is fullscreen before moving on. This is something that I discovered while looking into the problems I mentioned above. The current IsFullScreen() implementation checks if the window has a fullscreen flag set, which is done when toggleFullScreen() is initially called. I tried modifying this to also compare the window size to the size of the screen, but this is not reliable either. The window can be the size of the screen and still be in its transition animation. The best option might be returning a boolean value that changes based on fullscreen events. This solution would change the function's behavior from returning true when the window is on its way to fullscreen, to returning true when it is fullscreen. However, this would require further testing because the Mac implementation of GWindow will behave differently than how it has been. I also don't know if any other Gateware libraries use IsFullscreen(). If so, they might be affected as well by the changes in behavior I described.

The Mac solution

Waiting for each transition and style change to complete fully avoids any issues. I accomplished this by transitioning the window to a neutral state before moving on to its final state. This only needs to be done when the window is fullscreen or minimized. When that is the case, the window is transitioned into a windowed state (the neutral state) first and then brought into its final state. This behavior is not how the Windows and Linux implementations of GWindow work, but with the Mac this is the cleanest way to it. Especially with the Unit Tests that open, close, and resize windows at blazing fast speeds. 



Conclusion

I may have said it before, but Unit Tests are very helpful in uncovering issues with implementation. Catching these problems now means developers who use Gateware won't experience them later. Although I struggled more than I would have liked to on the Mac issues, I gained valuable insight into how NSWindow works and how to work with it.

No comments:

Post a Comment