package com.vitco.layout;
import com.jidesoft.action.CommandMenuBar;
import com.jidesoft.action.DockableBar;
import com.jidesoft.action.DockableBarFactory;
import com.jidesoft.action.DockableBarManager;
import com.jidesoft.docking.*;
import com.jidesoft.docking.event.DockableFrameAdapter;
import com.jidesoft.docking.event.DockableFrameEvent;
import com.jidesoft.swing.LayoutPersistence;
import com.vitco.core.data.Data;
import com.vitco.layout.bars.BarLinkagePrototype;
import com.vitco.layout.content.console.ConsoleInterface;
import com.vitco.layout.content.shortcut.ShortcutManagerInterface;
import com.vitco.layout.frames.FrameLinkagePrototype;
import com.vitco.layout.frames.custom.CDockableFrame;
import com.vitco.manager.action.ActionManager;
import com.vitco.manager.action.ComplexActionManager;
import com.vitco.manager.action.types.StateActionPrototype;
import com.vitco.manager.error.ErrorHandlerInterface;
import com.vitco.manager.help.FrameHelpOverlay;
import com.vitco.manager.lang.LangSelectorInterface;
import com.vitco.manager.pref.PreferencesInterface;
import com.vitco.settings.VitcoSettings;
import com.vitco.util.misc.SaveResourceLoader;
import com.vitco.util.misc.UrlUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.swing.*;
import javax.swing.plaf.InsetsUIResource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
/*
* Manages the creation of the main window.
*
* Defers dealing with the content of frames and bars to the in config.xml
* configured classes.
*/
public class WindowManager extends ExtendedDockableBarDockableHolder implements WindowManagerInterface {
// maps the bars to the linkage class that deals with them
private Map<String, BarLinkagePrototype> barLinkageMap;
// set the map
@Override
public final void setBarLinkageMap(Map<String, BarLinkagePrototype> map) {
this.barLinkageMap = map;
}
// var & setter
private ErrorHandlerInterface errorHandler;
@Override
public final void setErrorHandler(ErrorHandlerInterface errorHandler) {
this.errorHandler = errorHandler;
}
// var & setter (can not be interface!!)
protected Data data;
@Override
public final void setData(Data data) {
this.data = data;
}
// var & setter
private PreferencesInterface preferences;
@Override
public final void setPreferences(PreferencesInterface preferences) {
this.preferences = preferences;
}
// maps the frames to the linkage class that deals with them
private Map<String, FrameLinkagePrototype> frameLinkageMap;
// set the map
@Override
public final void setFrameLinkageMap(Map<String, FrameLinkagePrototype> map) {
this.frameLinkageMap = map;
}
// to hook the shortcut manager to the frames
private ShortcutManagerInterface shortcutManager;
@Override
public final void setShortcutManager(ShortcutManagerInterface shortcutManager) {
this.shortcutManager = shortcutManager;
}
private ActionManager actionManager;
// set the action handler
@Override
public final void setActionManager(ActionManager actionManager) {
this.actionManager = actionManager;
}
private ComplexActionManager complexActionManager;
// set the complex action handler
@Override
public final void setComplexActionManager(ComplexActionManager complexActionManager) {
this.complexActionManager = complexActionManager;
}
// var & setter
protected LangSelectorInterface langSelector;
@Override
public final void setLangSelector(LangSelectorInterface langSelector) {
this.langSelector = langSelector;
}
// var & setter
protected ConsoleInterface console;
@Autowired(required=true)
public final void setConsole(ConsoleInterface console) {
this.console = console;
}
// reference to this instance
final JFrame thisFrame = this;
// counts how often the program was started
private int start_count = 0;
// prepare all frames
@Override
public final CDockableFrame prepareFrame(final String key) {
CDockableFrame frame = null;
if (frameLinkageMap.containsKey(key)) {
frame = frameLinkageMap.get(key).buildFrame(key, thisFrame);
// add help overlay (this is only used if this frame is floated!)
JRootPane rootPane = frame.getRootPane();
final FrameHelpOverlay overlay = new FrameHelpOverlay(rootPane, actionManager, complexActionManager, langSelector);
// add help button
final DockableFrame finalFrame = frame;
AbstractAction action = new AbstractAction("help", new SaveResourceLoader("resource/img/icons/frame_help_button_icon.png").asIconImage()) {
@Override
public void actionPerformed(final ActionEvent e) {
if (finalFrame.isFloated()) {
// for this sub-frame (if floated only)
overlay.setActive(!overlay.isActive());
} else {
// show help for entire window (if docked)
actionManager.performWhenActionIsReady("show_help_overlay", new Runnable() {
@Override
public void run() {
actionManager.getAction("show_help_overlay").actionPerformed(
new ActionEvent(e.getSource(), e.getID(), e.paramString())
);
}
});
}
}
};
frame.setHelpAction(action);
action.putValue(AbstractAction.SHORT_DESCRIPTION, "Help"); // tooltip
frame.addAdditionalButtonActions(action);
// register the shortcuts for this frame
shortcutManager.registerFrame(frame);
} else {
System.err.println("Error: No linkage class defined for frame \"" + key + "\"");
}
return frame;
}
// prepare all bars
@Override
public final CommandMenuBar prepareBar(String key) {
CommandMenuBar bar = null;
if (barLinkageMap.containsKey(key)) {
bar = barLinkageMap.get(key).buildBar(key, thisFrame);
} else {
System.err.println("Error: No linkage class defined for bar \"" + key + "\"");
}
return bar;
}
// constructor
public WindowManager() throws HeadlessException {
super(VitcoSettings.TITLE_STRING);
// save the state on exit of the program
// this needs to be done BEFORE the window is closing
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(final WindowEvent e) {
// execute closing action
actionManager.performWhenActionIsReady("close_program_action", new Runnable() {
@Override
public void run() {
actionManager.getAction("close_program_action").actionPerformed(
new ActionEvent(e.getSource(), e.getID(), e.paramString())
);
}
});
}
});
}
// handle cursor (global)
@Override
public final void setCustomCursor(Cursor cursor) {
for (String frameName : getDockingManager().getAllFrames()) {
getDockingManager().getFrame(frameName).setCursor(cursor);
}
for (DockableBar dockableBar : getDockableBarManager().getAllDockableBars()) {
dockableBar.setCursor(cursor);
}
}
@PreDestroy
@Override
public final void finish() {
// store the boundary of the program (current window position)
preferences.storeObject("program_boundary_rect", this.getBounds());
// store the startcount + 1
preferences.storeInteger("program_start_count", start_count+1);
}
// handle borderless logic (make floated frames borderless)
private void handleBorderLess(final DockingManager dockingManager) {
// list of managed floating containers
final HashSet<DialogFloatingContainer> containers = new HashSet<DialogFloatingContainer>();
// the last mouse position (used to determine which container(s) need borders)
final Point lastMousePos = new Point(0,0);
// check if ctrl is currently pressed
final boolean[] ctrlDown = new boolean[] {false};
// the container that currently has a border
final DialogFloatingContainer[] activeContainer = {null};
// wrapper action to refresh the container borders
final AbstractAction refreshBorderAction = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
// loop over all managed containers
for (DialogFloatingContainer container : containers) {
// check if we need to show/hide the border and title
boolean showBorder = ctrlDown[0] && container.getBounds().contains(lastMousePos);
// nested check
for (Component child : container.getContentPane().getComponents()) {
if (child instanceof ContainerContainer) {
Component[] comps = ((ContainerContainer) child).getComponents();
if (comps.length > 0) {
if (comps[0] instanceof FrameContainer) {
for (Component comp : ((FrameContainer) comps[0]).getComponents()) {
if (comp instanceof DockableFrame) {
// show/hide title bar
((DockableFrame) comp).setShowTitleBar(showBorder);
// show/hide resize border (different border depending if active or inactive)
container.setBorder(showBorder
? VitcoSettings.FLOATING_FRAME_BORDER
: BorderFactory.createEmptyBorder());
}
}
}
}
}
}
}
}
};
// display the titlebar of frames when frame is docked and hide it when its floated
// also handle border color change when active frame changes
dockingManager.addDockableFrameListener(new DockableFrameAdapter() {
@Override
public void dockableFrameDocked(DockableFrameEvent dockableFrameEvent) {
dockableFrameEvent.getDockableFrame().setShowTitleBar(true);
}
@Override
public void dockableFrameFloating(DockableFrameEvent dockableFrameEvent) {
dockableFrameEvent.getDockableFrame().setShowTitleBar(false);
}
@Override
public void dockableFrameActivated(DockableFrameEvent dockableFrameEvent) {
// call this to update the border color
if (ctrlDown[0]) {
refreshBorderAction.actionPerformed(null);
// null the active container
activeContainer[0] = null;
}
}
});
// listen to mouse events
Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent event) {
if (event.getID() == MouseEvent.MOUSE_ENTERED) {
// remember the location
lastMousePos.setLocation(MouseInfo.getPointerInfo().getLocation());
if (ctrlDown[0]) {
// check which container is active
for (DialogFloatingContainer container : containers) {
if (container.getBounds().contains(lastMousePos)) {
if (activeContainer[0] != container) {
// a new container is now active, we need to update
activeContainer[0] = container;
refreshBorderAction.actionPerformed(null);
}
}
}
}
}
}
}, AWTEvent.MOUSE_EVENT_MASK);
// handle keyboard events (global)
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(new KeyEventDispatcher() {
@Override
public boolean dispatchKeyEvent(final KeyEvent e) {
// listen to ctrl events
if (ctrlDown[0] != e.isControlDown()) {
ctrlDown[0] = !ctrlDown[0];
// update border state
refreshBorderAction.actionPerformed(null);
activeContainer[0] = null;
}
return false;
}
});
// remove border from all FloatingContainer and store reference for dynamically changing them
dockingManager.setFloatingContainerCustomizer(new DockingManager.FloatingContainerCustomizer() {
@Override
public void customize(FloatingContainer fc) {
final DialogFloatingContainer dialogFloatingContainer = ((DialogFloatingContainer) fc);
// always make sure this has no border
dialogFloatingContainer.setBorder(BorderFactory.createEmptyBorder());
// register container
containers.add(dialogFloatingContainer);
}
});
// toggle "highlight active frame" setting
actionManager.registerAction("toggle_active_frame_highlighted", new StateActionPrototype() {
@Override
public void action(ActionEvent actionEvent) {
CDockableFrame.setActiveFrameHighlighted(!CDockableFrame.isActiveFrameHighlighted());
preferences.storeBoolean("use_highlight_active_frame", CDockableFrame.isActiveFrameHighlighted());
for (String frame : dockingManager.getAllFrames()) {
// state "null" is ignored here
dockingManager.getFrame(frame).setBorder(null);
}
}
@Override
public boolean getStatus() {
return CDockableFrame.isActiveFrameHighlighted();
}
});
// toggle "floatable frames"
actionManager.registerAction("toggle_floatable_frames", new StateActionPrototype() {
@Override
public void action(ActionEvent actionEvent) {
CDockableFrame.setFramesFloatable(!CDockableFrame.isFramesFloatable());
preferences.storeBoolean("enable_frames_floatable", CDockableFrame.isFramesFloatable());
for (String frame : dockingManager.getAllFrames()) {
// state "true" is ignored here
dockingManager.getFrame(frame).setFloatable(true);
}
}
@Override
public boolean getStatus() {
return CDockableFrame.isFramesFloatable();
}
});
// open link to wiki
actionManager.registerAction("open_website", new AbstractAction() {
private String wikiUrl = "https://simlu.github.io/voxelshop/";
@Override
public void actionPerformed(ActionEvent e) {
UrlUtil.openURL(console, wikiUrl);
}
});
// remove extra spacing of frames
UIManager.getDefaults().put("FrameContainer.contentBorderInsets", new InsetsUIResource(0, 0, 0, 0));
// listen to main frame resize events and make sure the floated frames do not
// disappear outside the window area (prevent frames from disappearing)
thisFrame.addComponentListener(new ComponentAdapter() {
Integer xOld = thisFrame.getX();
Integer yOld = thisFrame.getY();
// validate the floated frame position to be contained inside main jframe
private void validateFrame(DialogFloatingContainer container) {
container.setLocation(
Math.max(thisFrame.getX() + 20, Math.min(thisFrame.getX() + thisFrame.getWidth() - container.getWidth() - 20, container.getLocation().x)),
Math.max(thisFrame.getY() + 20, Math.min(thisFrame.getY() + thisFrame.getHeight() - container.getHeight() - 20, container.getLocation().y))
);
}
@Override
public void componentResized(ComponentEvent e) {
// make sure all floating containers are "on main JFrame"
for (DialogFloatingContainer container : containers) {
validateFrame(container);
}
}
@Override
public void componentMoved(ComponentEvent e) {
// move all DialogFloatingContainer with this main JFrame
int x = thisFrame.getX();
int y = thisFrame.getY();
for (DialogFloatingContainer container : containers) {
Point oldPosition = container.getLocation();
container.setLocation(oldPosition.x + (x - xOld), oldPosition.y + (y - yOld));
}
xOld = x;
yOld = y;
}
});
}
// set the divider size for any ContainerContainerDivider contained in hierarchy
private static void setDividerSizeDeep(Container component, int value) {
ArrayList<Component> components = new ArrayList<Component>();
components.add(component);
while (!components.isEmpty()) {
Component com = components.remove(0);
if (com instanceof ContainerContainerDivider) {
((ContainerContainerDivider)com).setDividerSize(value);
}
if (com instanceof Container) {
Collections.addAll(components, ((Container) com).getComponents());
}
}
}
/* Focus on MainView unless the FrameHelpOverlay is shown */
private static void tryFocusOnMainView(final FrameHelpOverlay overlay, final DockingManager dockingManager) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (!overlay.isActive()) {
dockingManager.getFrame("mainView").requestFocus();
}
}
});
}
@PostConstruct
@Override
public final void init() {
if (preferences.contains("use_highlight_active_frame")) {
// load active frame highlighted setting
CDockableFrame.setActiveFrameHighlighted(preferences.loadBoolean("use_highlight_active_frame"));
}
if (preferences.contains("enable_frames_floatable")) {
// load floatable frames setting
CDockableFrame.setFramesFloatable(preferences.loadBoolean("enable_frames_floatable"));
}
if (preferences.contains("program_boundary_rect")) {
// load the boundary of the program (current window position)
this.setBounds((Rectangle)preferences.loadObject("program_boundary_rect"));
}
// default close action
this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
// set the icon
this.setIconImage(
new SaveResourceLoader("resource/img/icons/application/paintbucket.png").asImage()
);
final DockingManager dockingManager = getDockingManager();
try {
DockableBarManager dockableBarManager = getDockableBarManager();
LayoutPersistence layoutPersistence = getLayoutPersistence();
// handles all the logic to make floating frames borderless
handleBorderLess(dockingManager);
// init loading
////////////////
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(
new SaveResourceLoader("resource/layout/TopLayout.ilayout").asInputStream()
);
// prepare
dockableBarManager.beginLoadLayoutData();
dockingManager.beginLoadLayoutData();
// add menu bars
dockableBarManager.setDockableBarFactory(new DockableBarFactory() {
public DockableBar create(String key) {
return prepareBar(key);
}
});
// add dock-able frames
dockingManager.setDockableFrameFactory(new DockableFrameFactory() {
public DockableFrame create(String key) {
return prepareFrame(key);
}
});
// finish adding
dockableBarManager.loadInitialLayout(document);
dockingManager.loadInitialLayout(document);
////////////////////
// register the shortcut action names
shortcutManager.registerGlobalShortcutActions();
// load the global hotkeys
shortcutManager.registerShortcuts(thisFrame, dockingManager);
// try to load the saved layout
layoutPersistence.beginLoadLayoutData();
byte[] layoutData = (byte[]) preferences.loadObject("custom_raw_layout_data");
layoutPersistence.setUsePref(false);
if(layoutData != null) {
layoutPersistence.setLayoutRawData(layoutData);
} else {
layoutPersistence.loadLayoutData();
}
this.toFront();
// allow frames to fill empty space
dockingManager.getWorkspace().setAcceptDockableFrame(true);
dockingManager.setEasyTabDock(true);
//dockingManager.setUseGlassPaneEnabled(false);
// set the grid snap size, e.g. when dragging
dockingManager.setSnapGridSize(5);
// set the draggable size between frames
setDividerSizeDeep(dockingManager.getDockedFrameContainer(), 2);
} 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
}
// register help overlay for entire window
JRootPane rootPane = thisFrame.getRootPane();
final FrameHelpOverlay overlay = new FrameHelpOverlay(rootPane, actionManager, complexActionManager, langSelector);
overlay.addFocusListener(new FocusAdapter() {
@Override
public void focusLost(FocusEvent e) {
// focus on main view again when the focus is lost, ensures that frame specific shortcuts work initially
tryFocusOnMainView(overlay, getDockingManager());
}
});
actionManager.registerAction("show_help_overlay", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
// toggle visibility of the glass "help" pane
overlay.setActive(!overlay.isActive());
}
});
// display help overlay on first three start
if (preferences.contains("program_start_count")) {
start_count = preferences.loadInteger("program_start_count");
}
if (start_count < 3) {
overlay.setActive(true);
}
// focus main frame, ensures that frame specific shortcuts work initially
tryFocusOnMainView(overlay, dockingManager);
actionManager.registerAction("swap_mainView_with_xyView", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleFrameSwap("mainView", "xyView");
}
});
actionManager.registerAction("swap_mainView_with_xzView", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleFrameSwap("mainView", "xzView");
}
});
actionManager.registerAction("swap_mainView_with_yzView", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleFrameSwap("mainView", "yzView");
}
});
actionManager.registerAction("swap_xyView_with_mainView", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleFrameSwap("xyView", "mainView");
}
});
actionManager.registerAction("swap_xzView_with_mainView", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleFrameSwap("xzView", "mainView");
}
});
actionManager.registerAction("swap_yzView_with_mainView", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
handleFrameSwap("yzView", "mainView");
}
});
}
// handle swap of frames (second frame is activated)
private void handleFrameSwap(String frame1, String frame2) {
DockingManager dm = getDockingManager();
dm.addFrame(new DockableFrame("__dummy"));
dm.moveFrame("__dummy", frame1);
dm.moveFrame(frame1, frame2);
dm.moveFrame(frame2, "__dummy");
dm.removeFrame("__dummy");
dm.activateFrame(frame2);
}
}