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

Dialog parent should not be null #4739

Merged
merged 1 commit into from
Oct 18, 2022
Merged

Dialog parent should not be null #4739

merged 1 commit into from
Oct 18, 2022

Conversation

errael
Copy link
Contributor

@errael errael commented Oct 6, 2022

This PR affects multi screen systems when the NetBeans main window is not the default window.

There are several scattered uses of JFileChooser and JOptionPane which use null for parent, so these dialogs go to the default screen (screen 0). If a dialog parent is not specified, generally it's preferred to go to the same screen as the MainWindow. This PR does that. The PR was done with 3 jackpot scripts, with some manual hints to add a core windows dependency when needed.

It's initially a draft PR. Look at the description for each of the 3 commits to see the jackpot scripts. I'll squish the commits later; now there's an opportunity to see the changes for each declarative refactoring script.

Note, usually the window that brings up a dialog should be the parent. But if that wasn't done, this PR at least brings up the dialog on the window that probably has the user's attention.

@mbien mbien added Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) Platform [ci] enable platform tests (platform/*) UI User Interface labels Oct 6, 2022
@mbien
Copy link
Member

mbien commented Oct 6, 2022

wondering what happens if a dialog opens a dialog. Would this risk to open the new dialog behind the other since the parent is the main window? I think with null parent it would open on top of everything what is already opened.

@mbien mbien removed the Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) label Oct 6, 2022
@errael
Copy link
Contributor Author

errael commented Oct 6, 2022

wondering what happens if a dialog opens a dialog

I have an example of that in a plugin, in something in Options I do

JOptionPane.showMessageDialog(
        ViManager.getFactory().getMainWindow(),

Does the right thing. In particular, JOptionPane creates a modal dialog, end of story.

@neilcsmith-net
Copy link
Member

Don't we have a lot of things that bypass NetBeans' APIs! 😬 I wonder if it's worth the effort of rewriting them? Other than that,DialogDisplayerImpl and FileChooserBuilder have slightly different approaches, which makes me wonder if use of KeyboardFocusManager might be better than getMainWindow()? eg. https://github.com/apache/netbeans/blob/master/platform/openide.filesystems.nb/src/org/openide/filesystems/FileChooserBuilder.java#L296

@errael
Copy link
Contributor Author

errael commented Oct 6, 2022

bypass NetBeans' APIs! grimacing I wonder if it's worth the effort of rewriting them?

The current situation is a hassle, it happens enough to be anoying, but not enough to be the first thing I check (that's slowly changing).

I thought about saying something like "this could be viewed as an interim step, fixing bugs, until some kind of rewrite". The last sentence of the initial comment does suggest that things weren't done right, and implies that it should be handled better. I saw FileChooserBuilder, that method findDialogParent could be put somewhere public, maybe DialogDisplayer, and I could rerun jackpot with that. But then there's a testing problem, I don't know how to force most of those dialogs. But I believe that would be better than the current state, and maybe not as reliable as this current PR. I'm suspect about findDialogParent picking the last frame in the list.

I don't trust KeyboardFocusManager.getActiveWindow. I think it returns null at inconvenient times, see #4714 (comment) third paragraph; I've been thinking about a patch to DialogDisplayerImpl that handles a null return from KFM.getActiveWindow and makes a choice. I can't imagine where else random/occasional incorrect placement of the dialogs could be coming from.

@matthiasblaesing
Copy link
Contributor

I noticed, that at least in some cases the class, that opens the dialog is itself an instance of java.awt.Component. In that case it could be passed to JFileChooser#show*Dialog and Swing would be responsible to find the right JFrame which should be the parent of the dialog. That relieves the need to fiddle with the main window or the keyboard focus or whatever and makes the code easier to follow. This should apply to TemplatesPanel.java, LoadGeneratorCustomizer.java, LocationCustomizer.java and SnapshotCustomizer.java.

@errael
Copy link
Contributor Author

errael commented Oct 6, 2022

the class that opens the dialog is itself an instance of java.awt.Component. In that case it could be passed to JFileChooser#show*Dialog

Yes, that would be best. This PR is about automatically fixing obvious bugs.

@errael
Copy link
Contributor Author

errael commented Oct 6, 2022

I copied FileChooserBuilder.findDialogParent() into a standalone app. In practice it has much better characteristics than simply using the main window (at least in my tests using JOptionPane(findDialogParent(),...) from a dialog).

I'm wondering if, for the last parent == null using MainWindow would be better than the last entry in the Frame.getFrames() array.

I started to put findDialgParent as a static method into DialogDisplayer; then ApiChange came to mind. Before I make any changes, I'd like some comments on where to put it. Where to put it is more of an issue, effort wise, that what's in it.

@mbien
Copy link
Member

mbien commented Oct 7, 2022

the class that opens the dialog is itself an instance of java.awt.Component. In that case it could be passed to JFileChooser#show*Dialog

Yes, that would be best. This PR is about automatically fixing obvious bugs.

I mean this inspection affects 30 files or so. I think a second manual pass would be realistic to fix those occurrences which apply.

There is also the inClass(String... classes) condition which could help here if you want to automate that too (I haven't used that one yet myself, it doesn't work for interfaces that I know), however I believe doing this manually is going to be faster (since we already know the occurences) than scanning all projects again (which can take a bit).

If you decide to do that manually, maybe also consider reducing the whitespace changes in the imports. Since they add empty lines in between every top level package change - this only makes sense if the imports are actually sorted ;)

@errael
Copy link
Contributor Author

errael commented Oct 7, 2022

I'm just taking another look at DialogDisplayerImpl. I may have unfoundedly disparaged KeyboardFocusManager returns; I had thought that perhaps it was being called when the focus owner was not in the "same context as the calling thread" (not sure that's possible as a desktop app).

But now I notice the use of noParent in DialogDisplayerImpl which may get set if runDelayed is involved and gives a null parent.

Null is just not the right choice in a multiscreen environment. I'm wondering about, in showDialog, using MainWindow instead of null parent. Another possibility would be to use the "preferred screen" from #4714. I lean towards MainWindow. Which handles both noParent and KeyboardFocusManager. Perhaps something a bit more complicated, determine the screen, eg MainWindow, but leave the parent null and move the dialog to the center of the chosen screen, (if there are cases where you don't want the dialog to have a parent?). Comments?

@errael
Copy link
Contributor Author

errael commented Oct 7, 2022

manual pass

Using findDialogParent, which is needed anyway since not all the classes in question meet the criteria, there's good odds that the Component in question is the focusOwner so things should work out. If it's not the focus owner, I guess it would still be safe to use it as the owner; the dialog in question is placed over that component; but I think focus owner would be better. Hmm, might need to check isVisible()?

I can probably be talked into it; but I'm uncomfortable making manual changes without testing and I'm not sure I'll be able to navigate to all the dialogs.

@errael
Copy link
Contributor Author

errael commented Oct 7, 2022

maybe also consider reducing the whitespace changes in the imports

Yeah, I need to run it again anyway, I can turn off Separate groups in editor > formatting.

@errael errael changed the title Dialog parent main window instead of null Dialog parent should not be null Oct 7, 2022
@errael errael marked this pull request as ready for review October 9, 2022 00:52
@errael
Copy link
Contributor Author

errael commented Oct 9, 2022

@neilcsmith-net, put findDialogParent in DialogDisplayer as static method. Instance method didn't make sense, it's not part of a NetBeans dialog.

Upgraded PR from draft. ApiChanges updated.

@lkishalmi lkishalmi added this to the NB16 milestone Oct 10, 2022
@neilcsmith-net
Copy link
Member

@errael having been looking at changes in #4714 I wonder if that method should just go in Utilities?

@errael
Copy link
Contributor Author

errael commented Oct 11, 2022

wonder if that method should just go in Utilities?

I'm OK with that. Wanted to pick something to get the ball rolling... When I was looking for a Util class to put it in, I think I missed this one. Was looking for something UI related.

System went into the shop yesterday, limited mail access. Not going to setup for dev on another system. Hope to get it back this weekend, but at least before the freeze.

@errael
Copy link
Contributor Author

errael commented Oct 11, 2022

looking at changes in #4714 I wonder if that method

@neilcsmith-net, Oh, wait. I may have misunderstood. Is your comment about getPreferredScreen() or findDialogParent()?

@neilcsmith-net
Copy link
Member

neilcsmith-net commented Oct 11, 2022

@errael findDialogParent(). Well, both I guess. Just looking at that other PR made me think it might be the best place for this method too.

@neilcsmith-net
Copy link
Member

@errael incidentally, I think moving it into Utilities could enable FileChooserBuilder to use it too? I think the module dependency is already there.

@errael
Copy link
Contributor Author

errael commented Oct 12, 2022

Took me a while to realize that #4714 was the same Utilities.java that findDialogParent() is moving to. (I miss my system)

FileChooserBuilder to use it too? I think the module dependency is already there.

Right, in this version of the PR I had to add a dependendency on Dialogs API to get FileChooserBuilder to use the new location findDialogParent.

Copy link
Member

@neilcsmith-net neilcsmith-net left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, should have been specific I meant org.openide.util.Utilities!

Yes, I think it's better there and should remove the need for some additional dependencies.

In a multi-screen setup, using null parent often puts the dialog
on the wrong screen.

Utilities.findDialogParent() copied from FileChooserBuilder.
Manually change FileChoolerBuidler to use Utilities.
Rest of the changes done with jackpot script:

<?
import org.openide.util.Utilities;
?>

"chooser dialog null":
$jfc.showOpenDialog(null) :: $jfc instanceof javax.swing.JFileChooser
=> $jfc.showOpenDialog(Utilities.findDialogParent())
;;

"chooser dialog null":
$jfc.showSaveDialog(null) :: $jfc instanceof javax.swing.JFileChooser
=> $jfc.showSaveDialog(Utilities.findDialogParent())
;;

"JOptionPane-2a null parent":
    $jop.showConfirmDialog($parent, $rest$)
::  isNullLiteral($parent)
    && $jop instanceof javax.swing.JOptionPane
=>  $jop.showConfirmDialog(Utilities.findDialogParent(), $rest$)
;;

"JOptionPane-2b null parent":
    $jop.showMessageDialog($parent, $rest$)
::  isNullLiteral($parent)
    && $jop instanceof javax.swing.JOptionPane
=>  $jop.showMessageDialog(Utilities.findDialogParent(), $rest$)
;;

"JOptionPane-2c null parent":
    $jop.showOptionDialog($parent, $rest$)
::  isNullLiteral($parent)
    && $jop instanceof javax.swing.JOptionPane
=>  $jop.showOptionDialog(Utilities.findDialogParent(), $rest$)
;;
@errael
Copy link
Contributor Author

errael commented Oct 16, 2022

Wow, there are 49, as of a recent count, Utilities.java in the tree.

It's looking like the system won't be back from the shop till later in the week, so setup old win7 system... Tested Utilities.findDialogParent() from a plugin. Changed FileChooserBuilder manually. The rest of the changes are generated by Jackpot.

Updated apichanges.xml, don't do that much so another set of eyes...

javafx/maven.htmlui was only module that didn't already have the dependency.

@errael
Copy link
Contributor Author

errael commented Oct 16, 2022

Too bad using add module dependency doesn't preserve the file line endings.

Copy link
Member

@mbien mbien left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code looks good to me. I do also like that the logic is now at a central place and everything uses this method.

However, I couldn't really see any difference. I tried to reproduce it with the git clone action for example. And both, NB 15 and this PR behaved correctly, It always opened on the "correct" screen (the screen i moved the main window to). But maybe I was looking at the wrong dialog.

Some other windows like main options are stubborn and open on screen 1 no matter where i move the main window.

If I close an editor tab via the drop down documents list while its modified. the discard dialog is still displaced in the corner of my screen.

But this can be all fixed in followup PRs if necessary, now that we have this method public.

@errael
Copy link
Contributor Author

errael commented Oct 17, 2022

looking at the wrong dialog

Yeah, this only applies to a small number of dialogs. Swing dialogs that were using null parent. NB API dialogs are not affected.

other windows like main options are stubborn and open on screen 1

Yeah, many have a memory.

I may instrument all the places a dialog is displayed looking for ones that use a null parent. Those are obviously in correct.

@errael
Copy link
Contributor Author

errael commented Oct 17, 2022

@mbien, addressing some of the annoying behavior you describe would require a rethink of part of the dialog displaying and/or some fixup maybe.

  • should memorized location be changed/set relative to the main window?
  • Are there dialogs that do not have a memory and should, like that discard dialog?

Might be worth an issue/discussion about problematic behavior that should be addressed. I suspect null parent doesn't play a role in much of it.

Copy link
Contributor

@matthiasblaesing matthiasblaesing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks sane to me. I left a minor inline comment and I still wonder if using the components themselves where possible would make the code simpler, but ok.

}
if (parent == null) {
Frame[] f = Frame.getFrames();
parent = f.length == 0 ? null : f[f.length - 1];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation of Frame.getFrames reads a bit strange. For NetBean I suggest to fall back to WindowManager.getMainWindow and if that indeed is not present, the return null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation of Frame.getFrames reads a bit strange. For NetBean I suggest to fall back to WindowManager.getMainWindow and if that indeed is not present, the return null.

Strange indeed. getMainWindow() does seem a better fallback. (and acc'd to doc creates a frame if none exists.)

I'll make that change, please review carefully since I won't be able to test it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is code that already exists in the platform, in FileChooserBuilder. I'm not sure using getMainWindow can happen without causing dependency issues?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm manually looking for main window specifically to avoid adding the dependency. Similar to whats seen around

for( Frame f : Frame.getFrames() ) {
if( "NbMainWindow".equals(f.getName())) { //NOI18N
return f.getGraphicsConfiguration();
}
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'll wait for further comment. I've got a change ready that find the main window (without a dependency).

I will not push it, unless/until there's a consensus.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, it looks like

        // KFM is out of it, check out known frames
        if (parent == null) {
            Frame[] frames = Frame.getFrames();
            for( Frame f : frames ) {
                if( "NbMainWindow".equals(f.getName())) { //NOI18N
                    parent = f;
                    break;
                }
            }
            if(parent == null && frames.length != 0)
                parent = frames[frames.length - 1];
        }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am strongly inclined to leave the code as a verbatim move of the code from FileChooserBuilder for now - that is used a lot across the IDE already. Let's use as is for the additional cases unless there's a proven issue to address.

While the documentation Frame::getFrames is a bit odd, I'm curious how often that fallback is ever hit here? Have you tried a breakpoint? If it's rare/never then changing it is pointless. If it's commonly hit, then special casing NbMainWindow may be a change in logic that actually ends up being less correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no consensus to change. As is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merging now. Let's test and revisit this if necessary during RC phase.

Be good to have a list of a few dialogs for users to test the corrected behaviour with.

@errael
Copy link
Contributor Author

errael commented Oct 17, 2022

Other than that,DialogDisplayerImpl and FileChooserBuilder have slightly different approaches

@neilcsmith-net, taking a look at DialogDisplayerImpl, in one case it use KeyboardFocusManager.getActiveWindow() which is similar; but it also checks several more cases. And I can't figure out when/how AWTQuery is used.

I'm thinking now that for local choices, JOptionPane is actually a better choice (particularly with findDialogParent()). In my tests, when I click on a button, the JOptionPane dialog shows up right under the mouse cursor; very nice.

@neilcsmith-net
Copy link
Member

@errael yes, I agree, and there's probably more scope for consolidating this behaviour .. in the NB17 timeframe!

@neilcsmith-net neilcsmith-net merged commit b0366bf into apache:master Oct 18, 2022
@errael errael deleted the DialogParentMainWindowInsteadOfNull branch October 18, 2022 17:37
@errael
Copy link
Contributor Author

errael commented Oct 19, 2022

a list of a few dialogs for users to test the corrected behaviour with.

@neilcsmith-net, here's some

Note that previously these would have shown up in the middle of default screen (not necessarily the one with the main window). Now they should appear over the button that was activated.

  • Tools > Templates > Add
  • Profile > InsertProfilingPoint > Next > Browse
  • Options > Editor > Spellchecker > Add > Browse
  • get to a file that has attachSources button, click it > AddZip/Folder
    for example, navigate to a library, click on a class file

For changes in files:

  • platform/templates/src/org/netbeans/modules/templates/ui/TemplatesPanel.java
  • profiler/profiler.ppoints/src/org/netbeans/modules/profiler/ppoints/ui/LocationCustomizer.java
  • ide/spellchecker/src/org/netbeans/modules/spellchecker/options/DictionaryInstallerPanel.java
  • java/java.j2seplatform/src/org/netbeans/modules/java/j2seplatform/queries/SourceJavadocAttacherUtil.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Platform [ci] enable platform tests (platform/*) UI User Interface
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants