Search code examples
netbeans-8netbeans-platform

How to hide parent menu automatically if all menu items are hidden via DynamicMenuContent


I have implemented menu hiding based on user authentication and roles using DynamicMenuContent

/**
 * Presenter that displays menu item if, and only if the user has the specified role.
 */
public class RoleMenuPresenter
        extends JMenuItem
        implements DynamicMenuContent {
    private static final JComponent[] EMPTY = new JComponent[0];
    private final String roleName;
    private final AuthenticationDelegate delegate = (AuthenticationDelegate) SpringSingleton
            .getBean("authenticationDelegate");
    private final JComponent[] components = {this};
    private final JComponent[] emptyComponents = EMPTY;

    //~ Constructors ===================================================================================================
    /**
     * Constructs this presenter.
     *
     * @param a action to execute
     * @param roleName the rolename
     */
    public RoleMenuPresenter(Action a, String roleName) {
        super(a);
        this.roleName = roleName;
    }
    //~ Methods ========================================================================================================

    @Override
    public JComponent[] getMenuPresenters() {

        if (delegate.getStatusBean().hasRole(roleName)) {
            return components;
        }

        return emptyComponents;
    }

    @Override
    public JComponent[] synchMenuPresenters(JComponent[] jComponents) {
        return getMenuPresenters();
    }

    public String getRoleName() {
        return roleName;
    }

    public interface Provider
            extends Presenter.Menu {
        @Override
        RoleMenuPresenter getMenuPresenter();
    }
}

Secured Action implements RoleMenuPresenter.Provider. Menu items are hidden as expected. There is a problem however, if all items of a menu are hidden, the menu still is displayed. Example:

  • File
    • Print
    • Exit
  • Transaction
    • A
    • B
    • C

If A, B, and C are hidden, Transaction is still visible. My question is how to automatically hide Transaction? Thank you.


Solution

  • I finally managed to make it work. MenuFolderUtil is used to calculate empty folder paths. ReheatMenus is used to warm up the menus. DynamicLayerContent is file system provider to provide in memory *_hidden folders. MenuVisibilityUtil is the main entry.

    MenuFolderUtil

    final class MenuFolderUtil {
    
        static Set<String> findEmptyFolder() {
            Set<String> folderPaths = new LinkedHashSet<>();
            FileObject file = FileUtil.getConfigFile("Menu");
            Enumeration<? extends FileObject> folderEnumerations = file.getFolders(
                    true);
            while (folderEnumerations.hasMoreElements()) {
                FileObject element = folderEnumerations.nextElement();
                folderPaths.add(element.getPath());
            }
    
            Enumeration<? extends FileObject> childrenEnum = file.getChildren(true);
            while (childrenEnum.hasMoreElements()) {
                FileObject element = childrenEnum.nextElement();
    
                if (element.isFolder()) {
                    continue;
                }
                FileObject parent = element.getParent();
                String parentPath = parent.getPath();
                if (!folderPaths.contains(parentPath)) {
                    continue;
                }
    
                Action action = FileUtil.getConfigObject(
                        element.getPath(),
                        Action.class);
                if (action instanceof Presenter.Menu) {
                    JMenuItem presenter = ((Presenter.Menu) action).getMenuPresenter();
                    if (presenter instanceof DynamicMenuContent) {
                        treatDynamicMenuContent(
                                (DynamicMenuContent) presenter,
                                folderPaths, buildParentPaths(element));
                    } else {
                        folderPaths.removeAll(buildParentPaths(element));
                    }
                } else if (action instanceof DynamicMenuContent) {
                    treatDynamicMenuContent(
                            (DynamicMenuContent) action,
                            folderPaths,
                            buildParentPaths(element));
                } else if (action != null) {
                    folderPaths.removeAll(buildParentPaths(element));
                }
            }
            return folderPaths;
        }
    
        private static void treatDynamicMenuContent(DynamicMenuContent presenter,
                Set<String> folderPaths, Set<String> c) {
            JComponent[] presenters = presenter.getMenuPresenters();
            if (presenters != null && presenters.length > 0) {
                folderPaths.removeAll(c);
            }
        }
    
        private static Set<String> buildParentPaths(FileObject fo) {
            Set<String> paths = new LinkedHashSet<>();
            for (FileObject parent = fo.getParent();
                    parent != null; parent = parent.getParent()) {
                paths.add(parent.getPath());
            }
            return paths;
        }
    }
    

    ReheatMenus

    /**
     * Adapted from {@code org.netbeans.core.ui.warmup.MenuWarmUpTask}.
     */
    class ReheatMenus
            implements Runnable {
    
        static void launch() {
            EventQueue.invokeLater(new ReheatMenus());
        }
    
        private ReheatMenus() {}
    
        private static final RequestProcessor RP = new RequestProcessor(ReheatMenus.class);
    
        private MenuBar bar;
        private Component[] comps;
    
        @Override
        public void run() {
            if (EventQueue.isDispatchThread()) {
                if (bar == null) {
                    Frame main = WindowManager.getDefault().getMainWindow();
                    assert main != null;
                    if (main instanceof JFrame) {
                        JMenuBar menuBar = ((JFrame) main).getJMenuBar();
                        if (menuBar instanceof MenuBar) {
                            bar = (MenuBar) menuBar;
                            RP.post(this);
                        }
                    }
                } else {
                    comps = bar.getComponents();
                    RP.post(this);
                }
            } else if (comps != null) {
                walkMenu(comps);
            } else {
                bar.waitFinished();
                EventQueue.invokeLater(this);
            }
        }
    
        private void walkMenu(Component[] items) {
            for (Component item : items) {
                if (!(item instanceof JMenu)) {
                    continue;
                }
                try {
                    Class<?> cls = item.getClass();
                    //noinspection JavaReflectionMemberAccess
                    Method m = cls.getDeclaredMethod("doInitialize");
                    m.setAccessible(true);
                    m.invoke(item);
                    walkMenu(((JMenu) item).getMenuComponents());
                } catch (Exception x) {
                    // most likely a NoSuchMethodException on a dynamic submenu
                }
            }
        }
    
    }
    

    DynamicLayerContent

    /**
     * File system which add system hidden folders.
     *
     * @author Thomas Edwin Santosa
     */
    @ServiceProvider(service = FileSystem.class)
    public class DynamicLayerContent
            extends MultiFileSystem {
        private static DynamicLayerContent INSTANCE;
    
        public DynamicLayerContent() {
            // will be created on startup, exactly once
            INSTANCE = this;
            setPropagateMasks(true); // permit *_hidden masks to be used
        }
    
        static void clear() {
            INSTANCE.setDelegates();
        }
    
        static void setHidden(Set<String> paths)
                throws IOException {
            FileSystem fileSystem = FileUtil.createMemoryFileSystem();
            for (String path : paths) {
                String pathHidden = path + "_hidden";
                String[] pathSplits = pathHidden.split("/");
                FileObject fo = fileSystem.getRoot();
                for (String folder : pathSplits) {
                    fo = FileUtil.createFolder(fo, folder);
                }
            }
            INSTANCE.setDelegates(fileSystem);
        }
    }
    

    MenuVisibilityUtil

    /**
     * Utility for menu security.
     *
     * @author Thomas Edwin Santosa
     */
    final class MenuVisibilityUtil {
        private static final Logger log = LoggerFactory.getLogger(MenuVisibilityUtil.class);
    
        private MenuVisibilityUtil() {
        }
    
        static void refresh() {
            Runnable run = new Runnable() {
                public void run() {
                    Set<String> hiddenPaths = new HashSet<>();
                    Frame main = WindowManager.getDefault().getMainWindow();
                    assert main != null;
                    if (main instanceof JFrame) {
                        JFrame frame = (JFrame) main;
                        JMenuBar menuBar = frame.getJMenuBar();
                        frame.setJMenuBar(null);
                        DynamicLayerContent.clear();
                        try {
                            DynamicLayerContent.setHidden(MenuFolderUtil.findEmptyFolder());
                        } catch (IOException e) {
                            log.error("cannot delete menu {}", hiddenPaths, e);
                        }
                        frame.setJMenuBar(menuBar);
                        ReheatMenus.launch();
                    }
    
                }
            };
            if (EventQueue.isDispatchThread()) {
                run.run();
            } else {
                EventQueue.invokeLater(run);
            }
        }
    
    }
    

    To use this, create an Installer in your plugin and call MenuVisibilityUtil.refresh(); You may want to call MenuVisibilityUtil.refresh() again if there is any user switch.