funfried/nb-editor-close-left-right

Move Close Tabs menu item placement

Closed this issue · 10 comments

Terrific little plugin! Makes Netbeans document management feel like a web browser's tab management.

It's certainly useful and does the trick as-is, but is it possible to move the "Close Right" and "Close Left" menu items up in the Close Tabs section?

image

Cheers

Hi. Thank you for reply. I have tried to do this from the beginning but without success. I will investigate it further when there will be some spare time.

I know this issue is pretty old, but as I have taken over this plugin I also tried to solve this, without any success as well. I only found out, that this is impossible by implementation as the WindowManagerImpl calls ActionUtils.createDefaultPopupActions and there the actions are instantiated and then added to a List of Actions and before the List is returned, there is a call for other actions from the Lookup and then the actions like "Close Right" and "Close Left" are loaded and added to that List. So there is no chance to get in between those actions before the current position.
The only possibility I see would be to provide a wrapper to the WindowManager and reorder the actions List, but manipulating the WindowManager in the IDE is not so easy and also not really a good idea.

I'll close this issue as there seems to be no solution to it.

I think it should be possible in theory by implementing org.netbeans.core.windows.actions.ActionsFactory ServiceProvider that would reorder actions but as far as I can tell org.netbeans.core.windows.actions.ActionsFactory is not a public api.
I was able to add dependency

<dependency>
	<groupId>org.netbeans.modules</groupId>
	<artifactId>org-netbeans-core-windows</artifactId>
	<version>${netbeans.version}</version>
</dependency>

and

package de.funfried.netbeans.plugins.editor.closeleftright;

import org.netbeans.core.windows.actions.ActionsFactory;
import org.openide.util.lookup.ServiceProvider;
import org.openide.windows.Mode;
import org.openide.windows.TopComponent;

import javax.swing.*;

@ServiceProvider(service = ActionsFactory.class)
public class ReorderActionsFactory extends ActionsFactory {

    public ReorderActionsFactory(){
        System.out.println("ReorderActionsFactory");
    }

    @Override
    public Action[] createPopupActions(TopComponent topComponent, Action[] actions) {
        System.out.println("ReorderActionsFactory: createPopupActions topComponent");
        return actions;
    }

    @Override
    public Action[] createPopupActions(Mode mode, Action[] actions) {
        System.out.println("ReorderActionsFactory: createPopupActions mode");
        return actions;
    }
}

but plugin loading fails with

	de.funfried.netbeans.plugins.nb.editor.close.left.right [1.0.0 1.0.0 202205131217]
INFO [org.openide.util.lookup.MetaInfServicesLookup]
java.lang.ClassNotFoundException: org.netbeans.core.windows.actions.ActionsFactory
	at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:352)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at org.netbeans.ProxyClassLoader.doFindClass(ProxyClassLoader.java:209)
Caused: java.lang.ClassNotFoundException: org.netbeans.core.windows.actions.ActionsFactory starting from ModuleCL@872da54[de.funfried.netbeans.plugins.nb.editor.close.left.right] with possible defining loaders [ModuleCL@77fd52ab[org.netbeans.core.windows]] and declared parents [ModuleCL@77fd52ab[org.netbeans.core.windows], ModuleCL@20ec0da6[org.openide.windows], ModuleCL@5631dad5[org.openide.text], org.netbeans.JarClassLoader@10429009]
	at org.netbeans.ProxyClassLoader.doFindClass(ProxyClassLoader.java:211)
	at org.netbeans.ProxyClassLoader.loadClass(ProxyClassLoader.java:125)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
Caused: java.lang.NoClassDefFoundError: org/netbeans/core/windows/actions/ActionsFactory
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at org.netbeans.JarClassLoader.doLoadClass(JarClassLoader.java:287)
	at org.netbeans.ProxyClassLoader.selfLoadClass(ProxyClassLoader.java:246)
Caused: java.lang.NoClassDefFoundError: org/netbeans/core/windows/actions/ActionsFactory while loading de.funfried.netbeans.plugins.editor.closeleftright.ReorderActionsFactory; see http://wiki.netbeans.org/DevFaqTroubleshootClassNotFound
	at org.netbeans.ProxyClassLoader.selfLoadClass(ProxyClassLoader.java:250)
	at org.netbeans.ProxyClassLoader.doFindClass(ProxyClassLoader.java:174)
	at org.netbeans.ProxyClassLoader.loadClass(ProxyClassLoader.java:125)
	at org.netbeans.ModuleManager$SystemClassLoader.loadClass(ModuleManager.java:769)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at org.openide.util.lookup.MetaInfServicesLookup.search(MetaInfServicesLookup.java:306)
Caused: java.lang.ClassNotFoundException: org/netbeans/core/windows/actions/ActionsFactory while loading de.funfried.netbeans.plugins.editor.closeleftright.ReorderActionsFactory; see http://wiki.netbeans.org/DevFaqTroubleshootClassNotFound
[catch] at org.openide.util.lookup.MetaInfServicesLookup.search(MetaInfServicesLookup.java:311)
	at org.openide.util.lookup.MetaInfServicesLookup.beforeLookup(MetaInfServicesLookup.java:131)
	at org.openide.util.lookup.MetaInfServicesLookup.beforeLookupResult(MetaInfServicesLookup.java:110)
	at org.openide.util.lookup.AbstractLookup.lookup(AbstractLookup.java:458)
	at org.openide.util.lookup.ProxyLookup$R.initResults(ProxyLookup.java:449)
	at org.openide.util.lookup.ProxyLookup$R.myBeforeLookup(ProxyLookup.java:736)
	at org.openide.util.lookup.ProxyLookup$R.computeResult(ProxyLookup.java:612)
	at org.openide.util.lookup.ProxyLookup$R.allInstances(ProxyLookup.java:572)
	at org.openide.util.lookup.ProxyLookup$R.allInstances(ProxyLookup.java:568)
	at org.openide.util.Lookup.lookupAll(Lookup.java:281)
	at org.netbeans.core.windows.actions.ActionUtils.createDefaultPopupActions(ActionUtils.java:243)
	at org.netbeans.core.windows.WindowManagerImpl.topComponentDefaultActions(WindowManagerImpl.java:1439)
	at org.openide.windows.TopComponent.getActions(TopComponent.java:610)
	at org.netbeans.core.multiview.MultiViewCloneableTopComponent.getActions(MultiViewCloneableTopComponent.java:153)
	at org.netbeans.core.windows.view.ui.TabbedHandler.handlePopupMenuShowing(TabbedHandler.java:423)
	at org.netbeans.core.windows.view.ui.TabbedHandler.actionPerformed(TabbedHandler.java:320)
	at org.netbeans.swing.tabcontrol.TabbedContainer.postActionEvent(TabbedContainer.java:705)
	at org.netbeans.swing.tabcontrol.TabbedContainerUI.shouldPerformAction(TabbedContainerUI.java:140)
	at org.netbeans.swing.tabcontrol.plaf.DefaultTabbedContainerUI.access$2700(DefaultTabbedContainerUI.java:87)
	at org.netbeans.swing.tabcontrol.plaf.DefaultTabbedContainerUI$DisplayerActionListener.actionPerformed(DefaultTabbedContainerUI.java:1261)
	at org.netbeans.swing.tabcontrol.TabDisplayer.postActionEvent(TabDisplayer.java:589)
	at org.netbeans.swing.tabcontrol.TabDisplayerUI.shouldPerformAction(TabDisplayerUI.java:168)
	at org.netbeans.swing.tabcontrol.plaf.BasicTabDisplayerUI.access$1700(BasicTabDisplayerUI.java:96)
	at org.netbeans.swing.tabcontrol.plaf.BasicTabDisplayerUI$BasicDisplayerMouseListener.performCommand(BasicTabDisplayerUI.java:760)
	at org.netbeans.swing.tabcontrol.plaf.BasicTabDisplayerUI$BasicDisplayerMouseListener.potentialCommand(BasicTabDisplayerUI.java:736)
	at org.netbeans.swing.tabcontrol.plaf.BasicTabDisplayerUI$BasicDisplayerMouseListener.mousePressed(BasicTabDisplayerUI.java:720)
	at java.awt.AWTEventMulticaster.mousePressed(AWTEventMulticaster.java:279)
	at java.awt.AWTEventMulticaster.mousePressed(AWTEventMulticaster.java:279)
	at java.awt.Component.processMouseEvent(Component.java:6536)
	at javax.swing.JComponent.processMouseEvent(JComponent.java:3324)
	at java.awt.Component.processEvent(Component.java:6304)
	at java.awt.Container.processEvent(Container.java:2239)
	at java.awt.Component.dispatchEventImpl(Component.java:4889)
	at java.awt.Container.dispatchEventImpl(Container.java:2297)
	at java.awt.Component.dispatchEvent(Component.java:4711)
	at java.awt.LightweightDispatcher.retargetMouseEvent(Container.java:4904)
	at java.awt.LightweightDispatcher.processMouseEvent(Container.java:4532)
	at java.awt.LightweightDispatcher.dispatchEvent(Container.java:4476)
	at java.awt.Container.dispatchEventImpl(Container.java:2283)
	at java.awt.Window.dispatchEventImpl(Window.java:2746)
	at java.awt.Component.dispatchEvent(Component.java:4711)
	at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:760)
	at java.awt.EventQueue.access$500(EventQueue.java:97)
	at java.awt.EventQueue$3.run(EventQueue.java:709)
	at java.awt.EventQueue$3.run(EventQueue.java:703)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74)
	at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:84)
	at java.awt.EventQueue$4.run(EventQueue.java:733)
	at java.awt.EventQueue$4.run(EventQueue.java:731)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:74)
	at java.awt.EventQueue.dispatchEvent(EventQueue.java:730)
	at org.netbeans.core.TimableEventQueue.dispatchEvent(TimableEventQueue.java:136)
	at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:205)
	at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
	at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
	at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
	at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

WindowManagerImpl calls ActionUtils.createDefaultPopupActions and there the actions are instantiated and then added to a List of Actions and before the List is returned, there is a call for other actions from the Lookup and then the actions like "Close Right" and "Close Left" are loaded and added to that List.

Indeed, the org.netbeans.core.windows.actions.ActionsFactory only observes a list of actions before other actions from Lookup are loaded. I guess it could be possible to instantiate and insert close left/right actions here instead of adding them via @ActionRegistration but as mentioned before ActionsFactory is not a public API and to use it plugin would need to pin the implementation version like so

diff --git a/pom.xml b/pom.xml
index 062794b..232408c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -117,6 +117,13 @@
                        <artifactId>org-openide-windows</artifactId>
                        <version>${netbeans.version}</version>
                </dependency>
+
+               <dependency>
+                       <groupId>org.netbeans.modules</groupId>
+                       <artifactId>org-netbeans-core-windows</artifactId>
+                       <version>${netbeans.version}</version>
+               </dependency>
+
        </dependencies>
 
        <build>
@@ -126,6 +133,16 @@
                                        <groupId>org.apache.netbeans.utilities</groupId>
                                        <artifactId>nbm-maven-plugin</artifactId>
                                        <version>4.7</version>
+                                       <configuration>
+                                               <moduleDependencies>
+                                                       <dependency>
+                                                               <id>org.netbeans.modules:org-netbeans-core-windows</id>
+                                                               <type>impl</type>
+                                                               <explicitValue>org.netbeans.core.windows/2 = 13-00d6d969bf4d9b14e7406c9ee9cc13a61dc39655</explicitValue>
+                                                       </dependency>
+                                               </moduleDependencies>
+                                       </configuration>
+
                                </plugin>
                                <plugin>
                                        <groupId>org.apache.maven.plugins</groupId>

and the close left/right implementation would most likely be different.

@AlexanderYastrebov Thanks for the hints, I'll definitely will look deeper into this, could be a solution indeed. But I guess I could only support one NetBeans IDE version like NetBeans IDE 13 (or 12 or 11, but not e.g. NetBeans IDE 11+) because of the direct explicit dependency.

I was able to implement it, thanks to Yenta which can befriend a restricted module with your own plugin. Unfortunately, Yenta is only available as a NetBeans plugin (nbm) in Maven central and not as a jar (yet), so if I add Yenta as a dependency it is not packed inside the plugin as e.g. commons-lang, NetBeans will simple assume it will be available in the user's IDE like e.g. the org-openide-windows nbm, but that's not the case, so a user would need to install it manually and actually it should be installed beforehand, otherwise the nb-editor-close-left-right plugin will be installed deactivated and the user would need to manually enable it after he/she installed the Yenta plugin. That would not be the case if I would have it as a jar or the plugin is available in the NetBeans plugin portal, both options are checked right now by the developer of Yenta and as soon as one of the two options is available, I will switch to Yenta as a dependency, since then I guess I have to keep the one source file inside the nb-editor-close-left-right plugin.

Here's the Yenta issue: jglick/yenta#2

@funfried Nice!
I think it is fine to vendor-in Yenta.java like you did and have no dependency on it.

I would not pull commons-lang just for one function though:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Scratch {

    public static void main(String[] args) {
        String[] actions = new String[]{"Close", "Close All", "Close Other", "---", "Maximize", "Float"};

        int insertAt = actions.length > 3 ? 3 : actions.length;

        List<String> actionList = Arrays.asList(actions);
        List<String> result = new ArrayList<>();
        result.addAll(actionList.subList(0, insertAt));
        result.add("Close Left");
        result.add("Close Right");
        result.addAll(actionList.subList(insertAt, actionList.size()));
        actions = result.toArray(new String[0]);

        System.out.println(String.join("\n", actions));
    }
}

Good point (commons-lang), but I don't think it should be a problem (it has a size of 574K) and your code snippet would work, but as the ActionFactory is taken into account for just any context menu in the whole IDE, it would be shown everywhere and no matter if a TopComponent can be closed or not, that's the reason why I check for the other close other actions, so I won't add close left and right and something cannot be closed (not sure when that's the case, but at least TopComponents can be closable a not closable)

Up to you, I think there are two createPopupActions in ActionsFactory but only one (receiving the Mode) is called for editor panes. I think it may also be possible to check the that mode==editor and insert actions only for it https://netbeans.apache.org/wiki/DevFaqWindowsMode.asciidoc

Should be fixed in latest SNAPSHOT release and will be published in a final released soon