package com.vitco.manager.menu;
import com.jidesoft.action.CommandBarSeparator;
import com.jidesoft.action.CommandMenuBar;
import com.jidesoft.docking.DockableFrame;
import com.jidesoft.swing.JideButton;
import com.jidesoft.swing.JideMenu;
import com.jidesoft.swing.JideSplitButton;
import com.jidesoft.swing.JideToggleButton;
import com.vitco.layout.content.shortcut.ShortcutChangeListener;
import com.vitco.layout.content.shortcut.ShortcutManagerInterface;
import com.vitco.manager.action.ActionManager;
import com.vitco.manager.action.ChangeListener;
import com.vitco.manager.action.ComplexActionManager;
import com.vitco.manager.action.types.StateActionPrototype;
import com.vitco.manager.action.types.SwitchActionPrototype;
import com.vitco.manager.async.AsyncAction;
import com.vitco.manager.async.AsyncActionManager;
import com.vitco.manager.error.ErrorHandlerInterface;
import com.vitco.manager.lang.LangSelectorInterface;
import com.vitco.settings.VitcoSettings;
import com.vitco.util.misc.SaveResourceLoader;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
/**
* Generates menus from xml files and links actions to them (e.g. main menu, tool menu)
*/
public class MenuGenerator implements MenuGeneratorInterface {
// var & setter
private LangSelectorInterface langSel;
@Override
public final void setLangSelector(LangSelectorInterface langSel) {
this.langSel = langSel;
}
// var & setter
private ShortcutManagerInterface shortcutManager;
@Override
public final void setShortcutManager(ShortcutManagerInterface shortcutManager) {
this.shortcutManager = shortcutManager;
}
// var & setter
private ErrorHandlerInterface errorHandler;
@Override
public final void setErrorHandler(ErrorHandlerInterface errorHandler) {
this.errorHandler = errorHandler;
}
// var & setter
private ActionManager actionManager;
@Override
public final void setActionManager(ActionManager actionManager) {
this.actionManager = actionManager;
}
// var & setter
private ComplexActionManager complexActionManager;
@Override
public final void setComplexActionManager(ComplexActionManager complexActionManager) {
this.complexActionManager = complexActionManager;
}
// var & setter
private AsyncActionManager asyncActionManager;
@Autowired
public final void setAsyncActionManager(AsyncActionManager asyncActionManager) {
this.asyncActionManager = asyncActionManager;
}
@Override
public void buildMenuFromXML(JComponent jComponent, String xmlFile) {
// the menu needs to be focusable. Otherwise some parent will be focused with a delay (when
// the menu closes). This can cause components that gained focus while the menu
// was opened to lose focus again.
jComponent.setFocusable(true);
// disable the chevron by default
if (jComponent instanceof CommandMenuBar) {
((CommandMenuBar)jComponent).setChevronAlwaysVisible(false);
}
try {
// load the xml document
DocumentBuilderFactory factory = DocumentBuilderFactory
.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new SaveResourceLoader(xmlFile).asInputStream());
buildRecursive(doc.getFirstChild(), jComponent);
} catch (ParserConfigurationException e) {
errorHandler.handle(e); // should not happen
} catch (SAXException e) {
errorHandler.handle(e); // should not happen
} catch (IOException e) {
errorHandler.handle(e); // should not happen
}
}
private void buildRecursive(Node node, JComponent component) {
// build the menu
String name = node.getNodeName();
if (node.hasChildNodes()) {
if (name.equals("menu")) {
Element e = (Element) node;
JideMenu mnu = new JideMenu(langSel.getString(e.getAttribute("caption")));
mnu.setName(e.getAttribute("caption")); // set identifier
NodeList list = node.getChildNodes();
component.add(mnu);
int len = list.getLength();
for (int i = 0; i < len; i++) {
buildRecursive(list.item(i), mnu);
}
} else if (name.equals("head")) {
NodeList list = node.getChildNodes();
int len = list.getLength();
for (int i = 0; i < len; i++) {
buildRecursive(list.item(i), component);
}
}
} else {
if (name.equals("item")) {
Element e = (Element) node;
addItem(component, e,
e.hasAttribute("checkable") && e.getAttribute("checkable").equals("true"),
e.hasAttribute("grayable") && e.getAttribute("grayable").equals("true"),
e.hasAttribute("hideable") && e.getAttribute("hideable").equals("true")
);
} else if (name.equals("separator")) {
if (component instanceof JideMenu) {
JPanel jPanel = new JPanel();
jPanel.setBackground(VitcoSettings.MAIN_MENU_BACKGROUND);
jPanel.setBorder(BorderFactory.createEmptyBorder(2, 0, 2, 0));
jPanel.setLayout(new BorderLayout());
JSeparator jSeparator = new JSeparator();
jSeparator.setBackground(VitcoSettings.MAIN_MENU_SEPARATOR_COLOR);
jSeparator.setForeground(VitcoSettings.MAIN_MENU_SEPARATOR_COLOR);
jSeparator.setPreferredSize(new Dimension(0, 1));
jPanel.add(jSeparator, BorderLayout.CENTER);
component.add(jPanel);
} else {
component.add(new CommandBarSeparator());
}
} else if (name.equals("icon-item")) {
Element e = (Element) node;
addIconItem(component, e,
e.hasAttribute("checkable") && e.getAttribute("checkable").equals("true"),
e.hasAttribute("grayable") && e.getAttribute("grayable").equals("true"),
e.hasAttribute("hideable") && e.getAttribute("hideable").equals("true")
);
} else if (name.equals("split-item")) {
Element e = (Element) node;
addSplitItem(component, e,
e.hasAttribute("checkable") && e.getAttribute("checkable").equals("true"),
e.hasAttribute("grayable") && e.getAttribute("grayable").equals("true"),
e.hasAttribute("hideable") && e.getAttribute("hideable").equals("true")
);
}
}
}
private void handleMenuShortcut(final JMenuItem item, final Element e) {
// shortcut change events
KeyStroke accelerator = shortcutManager.getShortcutByAction(null, e.getAttribute("action"));
if (accelerator != null) {
item.setAccelerator(accelerator);
}
shortcutManager.addShortcutChangeListener(new ShortcutChangeListener() {
@Override
public void onChange() {
KeyStroke accelerator = shortcutManager.getShortcutByAction(null, e.getAttribute("action"));
item.setAccelerator(accelerator);
}
});
}
/* Obtain the Dockable frame for a component or null if not available */
private String getFrameForComponent(Container component) {
while (component != null && !(component instanceof DockableFrame)) {
component = component.getParent();
}
if (component != null) {
return component.getName();
} else {
return null;
}
}
/* Update Tooltip for a Button and action */
private void updateButtonToolTip(String action, JComponent button, String baseToolTop) {
// check frame shortcut
String frame = getFrameForComponent(button);
if (frame != null) {
KeyStroke keyStroke = shortcutManager.getShortcutByAction(frame, action);
if (keyStroke != null) {
button.setToolTipText(baseToolTop + " [" + shortcutManager.asString(keyStroke) + "]");
return;
}
}
// check global shortcut
KeyStroke keyStroke = shortcutManager.getShortcutByAction(null, action);
if (keyStroke != null) {
button.setToolTipText(baseToolTop + " (" + shortcutManager.asString(keyStroke) + ")");
return;
}
// no shortcut
button.setToolTipText(baseToolTop);
}
private void handleButtonShortcutAndTooltip(final JComponent button, final Element e) {
final String baseTooltip = langSel.getString(e.getAttribute("tool-tip"));
// listen to button ancestor changes
button.addPropertyChangeListener("ancestor",new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
updateButtonToolTip(e.getAttribute("action"), button, baseTooltip);
}
});
// listen to shortcut changes
shortcutManager.addShortcutChangeListener(new ShortcutChangeListener() {
@Override
public void onChange() {
updateButtonToolTip(e.getAttribute("action"), button, baseTooltip);
}
});
}
// =========================
// adds a default menu item
private void addItem(JComponent component, final Element e,
final boolean checkable, final boolean grayable, final boolean hideable) {
final JMenuItem item = checkable ? new JCheckBoxMenuItem() : new JMenuItem();
// to perform validity check we need to register this name
actionManager.registerActionIsUsed(e.getAttribute("action"));
// lazy action linking (the action might not be ready!)
actionManager.performWhenActionIsReady(e.getAttribute("action"), new Runnable() {
@Override
public void run() {
item.addActionListener(actionManager.getAction(e.getAttribute("action")));
}
});
handleMenuShortcut(item, e);
item.setText(langSel.getString(e.getAttribute("caption")));
if (checkable || grayable || hideable) {
// look up current check status
item.addPropertyChangeListener("ancestor",new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getNewValue() != null) {
// do this async to prevent deadlock (as property change does lock AWT)
asyncActionManager.addAsyncAction(new AsyncAction() {
@Override
public void performAction() {
boolean invert = e.hasAttribute("invert") && e.getAttribute("invert").equals("true");
StateActionPrototype action = ((StateActionPrototype) actionManager.getAction(e.getAttribute("action")));
if (checkable) {
item.setSelected(
// triggered when the menu item is show
// this makes sure the "checked" is always current
invert ? !action.isChecked() : action.isChecked()
);
}
if (grayable) {
item.setEnabled(
// triggered when the menu item is shown
// this makes sure the "checked" is always current
invert ? !action.isEnabled() : action.isEnabled()
);
}
if (hideable) {
item.setVisible(
// triggered when the menu item is show
// this makes sure the "checked" is always current
invert ? !action.isVisible() : action.isVisible()
);
}
}
});
}
}
});
}
component.add(item);
}
// handles correct states of gray, checked, hide button
private void handleAbstractButton(final AbstractButton abstractButton, final Element e, final boolean checkable, final boolean grayable, final boolean hideable) {
abstractButton.setFocusable(false);
handleButtonShortcutAndTooltip(abstractButton, e);
if (grayable) {
// check if there is a custom gray icon defined
if (e.hasAttribute("src-gray")) {
abstractButton.setDisabledIcon(new SaveResourceLoader(e.getAttribute("src-gray")).asIconImage());
}
}
if (checkable || grayable || hideable) {
actionManager.performWhenActionIsReady(e.getAttribute("action"), new Runnable() {
@Override
public void run() {
final StateActionPrototype stateActionPrototype =
((StateActionPrototype) actionManager.getAction(e.getAttribute("action")));
final boolean invert = e.hasAttribute("invert") && e.getAttribute("invert").equals("true");
stateActionPrototype.addChangeListener(new ChangeListener() {
@Override
public void actionFired(boolean b) {
if (checkable) {
if (abstractButton instanceof JideSplitButton) {
// only select the "upper" button -> otherwise the popup part would not be click-able anymore
((JideSplitButton)abstractButton).setButtonSelected(invert ? !stateActionPrototype.isChecked() : stateActionPrototype.isChecked());
} else {
abstractButton.setSelected(invert ? !stateActionPrototype.isChecked() : stateActionPrototype.isChecked());
}
}
if (grayable) {
abstractButton.setEnabled(invert ? !stateActionPrototype.isEnabled() : stateActionPrototype.isEnabled());
}
if (hideable) {
abstractButton.setVisible(invert ? !stateActionPrototype.isVisible() : stateActionPrototype.isVisible());
}
}
});
}
});
}
}
// =========================
// adds an item that has an icon and a tooltip
private void addIconItem(JComponent component, final Element e,
final boolean checkable, final boolean grayable, final boolean hideable) {
final JideButton jideButton = checkable
? new JideToggleButton(new SaveResourceLoader(e.getAttribute("src")).asIconImage())
: new JideButton(new SaveResourceLoader(e.getAttribute("src")).asIconImage());
// to perform validity check we need to register this name
actionManager.registerActionIsUsed(e.getAttribute("action"));
// lazy action linking (the action might not be ready!)
actionManager.performWhenActionIsReady(e.getAttribute("action"), new Runnable() {
@Override
public void run() {
final AbstractAction action = actionManager.getAction(e.getAttribute("action"));
if (action instanceof SwitchActionPrototype) {
jideButton.getModel().addChangeListener(new javax.swing.event.ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
if (jideButton.getModel().isPressed()) {
((SwitchActionPrototype) action).switchOn();
} else {
((SwitchActionPrototype) action).switchOff();
}
}
});
} else {
jideButton.addActionListener(actionManager.getAction(e.getAttribute("action")));
}
}
});
if (e.hasAttribute("register-button-as-complex-action")) {
complexActionManager.registerAction(e.getAttribute("register-button-as-complex-action"), jideButton);
}
handleAbstractButton(jideButton, e, checkable, grayable, hideable);
component.add(jideButton);
}
// =========================
// adds a split item (submenu)
private void addSplitItem(JComponent component, final Element e,
final boolean checkable, final boolean grayable, final boolean hideable) {
final JideSplitButton splitButton = new JideSplitButton(new SaveResourceLoader(e.getAttribute("src")).asIconImage());
// check if we have an action
if (e.hasAttribute("action")) {
// to perform validity check we need to register this name
actionManager.registerActionIsUsed(e.getAttribute("action"));
// lazy action linking (the action might not be ready!)
actionManager.performWhenActionIsReady(e.getAttribute("action"), new Runnable() {
@Override
public void run() {
splitButton.addActionListener(actionManager.getAction(e.getAttribute("action")));
}
});
// disable action if wanted (e.g. if only grayout action)
if (e.hasAttribute("disable-action") && e.getAttribute("disable-action").equals("true")) {
splitButton.setAlwaysDropdown(true);
}
} else {
// the whole button is now used to open the dropdown menu
splitButton.setAlwaysDropdown(true);
}
// disable the border of the shown dialog
splitButton.getPopupMenu().setBorder(BorderFactory.createEmptyBorder());
// to perform validity check we need to register this name
complexActionManager.registerActionIsUsed(e.getAttribute("complex-action"));
// lazy action linking (the action might not be ready!)
complexActionManager.performWhenActionIsReady(e.getAttribute("complex-action"), new Runnable() {
@Override
public void run() {
splitButton.add(complexActionManager.getAction(e.getAttribute("complex-action")));
}
});
if (e.hasAttribute("register-button-as-complex-action")) {
complexActionManager.registerAction(e.getAttribute("register-button-as-complex-action"), splitButton);
}
handleAbstractButton(splitButton, e, checkable, grayable, hideable);
component.add(splitButton);
}
}