/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.uberfire.client.workbench;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
import org.jboss.errai.common.client.dom.DOMUtil;
import org.jboss.errai.common.client.dom.HTMLElement;
import org.jboss.errai.ioc.client.container.SyncBeanManager;
import org.uberfire.client.mvp.PerspectiveActivity;
import org.uberfire.client.mvp.PlaceManager;
import org.uberfire.client.mvp.UIPart;
import org.uberfire.client.workbench.events.ChangeTitleWidgetEvent;
import org.uberfire.client.workbench.events.DropPlaceEvent;
import org.uberfire.client.workbench.events.PanelFocusEvent;
import org.uberfire.client.workbench.events.PlaceGainFocusEvent;
import org.uberfire.client.workbench.events.PlaceHiddenEvent;
import org.uberfire.client.workbench.events.PlaceLostFocusEvent;
import org.uberfire.client.workbench.events.PlaceMaximizedEvent;
import org.uberfire.client.workbench.events.PlaceMinimizedEvent;
import org.uberfire.client.workbench.events.SelectPlaceEvent;
import org.uberfire.client.workbench.panels.DockingWorkbenchPanelPresenter;
import org.uberfire.client.workbench.panels.WorkbenchPanelPresenter;
import org.uberfire.client.workbench.part.WorkbenchPartPresenter;
import org.uberfire.debug.Debug;
import org.uberfire.mvp.PlaceRequest;
import org.uberfire.workbench.model.CompassPosition;
import org.uberfire.workbench.model.PanelDefinition;
import org.uberfire.workbench.model.PartDefinition;
import org.uberfire.workbench.model.Position;
import org.uberfire.workbench.model.impl.PanelDefinitionImpl;
import org.uberfire.workbench.model.menu.Menus;
import static org.uberfire.commons.validation.PortablePreconditions.checkNotNull;
import static org.uberfire.plugin.PluginUtil.ensureIterable;
/**
* Standard implementation of {@link PanelManager}.
*/
@ApplicationScoped
public class PanelManagerImpl implements PanelManager {
protected final Map<PartDefinition, WorkbenchPartPresenter> mapPartDefinitionToPresenter = new HashMap<PartDefinition, WorkbenchPartPresenter>();
protected final Map<PanelDefinition, WorkbenchPanelPresenter> mapPanelDefinitionToPresenter = new HashMap<PanelDefinition, WorkbenchPanelPresenter>();
/**
* Remembers which HasWidgets contains each existing custom panel. Items are removed from this map when the panels
* are closed/removed.
*/
protected final Map<PanelDefinition, HasWidgets> customPanels = new HashMap<PanelDefinition, HasWidgets>();
/**
* Remembers which HTMLElements contain each existing custom panel. Items are removed from this map when the panels
* are closed/removed.
*/
protected final Map<PanelDefinition, HTMLElement> customPanelsInsideHTMLElements = new HashMap<>();
protected Event<PlaceGainFocusEvent> placeGainFocusEvent;
protected Event<PlaceLostFocusEvent> placeLostFocusEvent;
protected Event<PanelFocusEvent> panelFocusEvent;
protected Event<SelectPlaceEvent> selectPlaceEvent;
protected Event<PlaceMaximizedEvent> placeMaximizedEvent;
protected Event<PlaceMinimizedEvent> placeMinimizedEvent;
protected Event<PlaceHiddenEvent> placeHiddenEvent;
protected SyncBeanManager iocManager;
protected Instance<PlaceManager> placeManager;
/**
* Description that the current root panel was created from. Presently, this is a mutable data structure and the
* whole UF framework tries to keep this in sync with the reality (syncing each change from DOM -> Widget ->
* UberView -> Presenter -> Definition). This may change in the future. See UF-117.
*/
protected PanelDefinition rootPanelDef = null;
protected PartDefinition activePart = null;
LayoutSelection layoutSelection;
private BeanFactory beanFactory;
/**
* Registration for the native preview handler that watches for ^M events and maximizes/restores the current panel.
*/
private HandlerRegistration globalHandlerRegistration;
/**
* The currently maximized panel. Set to null when a panel is not maximized.
*/
private WorkbenchPanelPresenter maximizedPanel = null;
@Inject
public PanelManagerImpl(
Event<PlaceGainFocusEvent> placeGainFocusEvent,
Event<PlaceLostFocusEvent> placeLostFocusEvent,
Event<PanelFocusEvent> panelFocusEvent,
Event<SelectPlaceEvent> selectPlaceEvent,
Event<PlaceMaximizedEvent> placeMaximizedEvent,
Event<PlaceMinimizedEvent> placeMinimizedEventEvent,
Event<PlaceHiddenEvent> placeHiddenEvent,
SyncBeanManager iocManager,
Instance<PlaceManager> placeManager,
LayoutSelection layoutSelection,
BeanFactory beanFactory) {
this.placeGainFocusEvent = placeGainFocusEvent;
this.placeLostFocusEvent = placeLostFocusEvent;
this.panelFocusEvent = panelFocusEvent;
this.selectPlaceEvent = selectPlaceEvent;
this.placeMaximizedEvent = placeMaximizedEvent;
this.placeMinimizedEvent = placeMinimizedEventEvent;
this.placeHiddenEvent = placeHiddenEvent;
this.iocManager = iocManager;
this.placeManager = placeManager;
this.layoutSelection = layoutSelection;
this.beanFactory = beanFactory;
}
@PostConstruct
private void setup() {
globalHandlerRegistration = com.google.gwt.user.client.Event.addNativePreviewHandler(new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event) {
if (event.getTypeInt() == com.google.gwt.user.client.Event.ONKEYPRESS &&
event.getNativeEvent().getCharCode() == 'm' &&
event.getNativeEvent().getCtrlKey()) {
if (maximizedPanel != null) {
maximizedPanel.unmaximize();
maximizedPanel = null;
} else if (activePart != null) {
WorkbenchPanelPresenter activePanelPresenter = mapPanelDefinitionToPresenter.get(activePart.getParentPanel());
activePanelPresenter.maximize();
maximizedPanel = activePanelPresenter;
}
}
}
});
}
@PreDestroy
private void teardown() {
globalHandlerRegistration.removeHandler();
}
protected BeanFactory getBeanFactory() {
return beanFactory;
}
@Override
public PanelDefinition getRoot() {
return this.rootPanelDef;
}
@Override
public void setRoot(PerspectiveActivity activity,
PanelDefinition root) {
checkNotNull("root",
root);
final WorkbenchPanelPresenter oldRootPanelPresenter = mapPanelDefinitionToPresenter.remove(rootPanelDef);
if (!mapPanelDefinitionToPresenter.isEmpty()) {
String message = "Can't replace current root panel because it is not empty. The following panels remain: " + mapPanelDefinitionToPresenter;
mapPanelDefinitionToPresenter.put(rootPanelDef,
oldRootPanelPresenter);
throw new IllegalStateException(message);
}
HasWidgets perspectiveContainer = layoutSelection.get().getPerspectiveContainer();
perspectiveContainer.clear();
getBeanFactory().destroy(oldRootPanelPresenter);
this.rootPanelDef = root;
WorkbenchPanelPresenter newPresenter = mapPanelDefinitionToPresenter.get(root);
if (newPresenter == null) {
newPresenter = getBeanFactory().newRootPanel(activity,
root);
mapPanelDefinitionToPresenter.put(root,
newPresenter);
}
perspectiveContainer.add(newPresenter.getPanelView().asWidget());
}
@Override
public void addWorkbenchPart(final PlaceRequest place,
final PartDefinition partDef,
final PanelDefinition panelDef,
final Menus menus,
final UIPart uiPart,
final String contextId,
final Integer preferredWidth,
final Integer preferredHeight) {
checkNotNull("panel",
panelDef);
final WorkbenchPanelPresenter panelPresenter = mapPanelDefinitionToPresenter.get(panelDef);
if (panelPresenter == null) {
throw new IllegalArgumentException("Target panel is not part of the layout");
}
WorkbenchPartPresenter partPresenter = mapPartDefinitionToPresenter.get(partDef);
if (partPresenter == null) {
partPresenter = getBeanFactory().newWorkbenchPart(menus,
uiPart.getTitle(),
uiPart.getTitleDecoration(),
partDef,
panelPresenter.getPartType());
partPresenter.setWrappedWidget(uiPart.getWidget());
partPresenter.setContextId(contextId);
mapPartDefinitionToPresenter.put(partDef,
partPresenter);
}
panelPresenter.addPart(partPresenter,
contextId);
if (panelPresenter.getParent() instanceof DockingWorkbenchPanelPresenter) {
DockingWorkbenchPanelPresenter parent = (DockingWorkbenchPanelPresenter) panelPresenter.getParent();
parent.setChildSize(panelPresenter,
preferredWidth,
preferredHeight);
}
//Select newly inserted part
selectPlaceEvent.fire(new SelectPlaceEvent(place));
}
@Override
public boolean removePartForPlace(PlaceRequest toRemove) {
final PartDefinition removedPart = getPartForPlace(toRemove);
if (removedPart != null) {
removePart(removedPart);
return true;
}
return false;
}
@Override
public PanelDefinition addWorkbenchPanel(final PanelDefinition targetPanel,
final Position position,
final Integer height,
final Integer width,
final Integer minHeight,
final Integer minWidth) {
final PanelDefinitionImpl childPanel = new PanelDefinitionImpl(PanelDefinition.PARENT_CHOOSES_TYPE);
childPanel.setHeight(height);
childPanel.setWidth(width);
childPanel.setMinHeight(minHeight);
childPanel.setMinWidth(minWidth);
return addWorkbenchPanel(targetPanel,
childPanel,
position);
}
@Override
public void removeWorkbenchPanel(final PanelDefinition toRemove) throws IllegalStateException {
if (toRemove.isRoot()) {
throw new IllegalArgumentException("The root panel cannot be removed. To replace it, call setRoot()");
}
if (!toRemove.getParts().isEmpty()) {
throw new IllegalStateException("Panel still contains parts: " + toRemove.getParts());
}
final WorkbenchPanelPresenter presenterToRemove = mapPanelDefinitionToPresenter.remove(toRemove);
if (presenterToRemove == null) {
throw new IllegalArgumentException("Couldn't find panel to remove: " + toRemove);
}
removeWorkbenchPanelFromParent(toRemove,
presenterToRemove);
// we do this check last because some panel types (eg. docking panels) can "rescue" orphaned child panels
// during the PanelPresenter.remove() call
if (!toRemove.getChildren().isEmpty()) {
throw new IllegalStateException("Panel still contains child panels: " + toRemove.getChildren());
}
getBeanFactory().destroy(presenterToRemove);
}
private void removeWorkbenchPanelFromParent(final PanelDefinition toRemove,
final WorkbenchPanelPresenter presenterToRemove) {
HasWidgets customContainer = customPanels.remove(toRemove);
if (customContainer != null) {
customContainer.remove(presenterToRemove.getPanelView().asWidget());
} else {
HTMLElement customHTMLElementContainer = customPanelsInsideHTMLElements.remove(toRemove);
if (customHTMLElementContainer != null) {
DOMUtil.removeFromParent(presenterToRemove.getPanelView().asWidget());
} else {
final PanelDefinition parentDef = toRemove.getParent();
final WorkbenchPanelPresenter parentPresenter = mapPanelDefinitionToPresenter.get(parentDef);
if (parentPresenter == null) {
throw new IllegalArgumentException("The given panel's parent could not be found");
}
parentPresenter.removePanel(presenterToRemove);
}
}
}
@Override
public void onPartFocus(final PartDefinition part) {
activePart = part;
panelFocusEvent.fire(new PanelFocusEvent(part.getParentPanel()));
placeGainFocusEvent.fire(new PlaceGainFocusEvent(part.getPlace()));
}
@Override
public void onPartMaximized(final PartDefinition part) {
placeMaximizedEvent.fire(new PlaceMaximizedEvent(part.getPlace()));
}
@Override
public void onPartMinimized(final PartDefinition part) {
placeMinimizedEvent.fire(new PlaceMinimizedEvent(part.getPlace()));
}
@Override
public void onPartHidden(final PartDefinition part) {
placeHiddenEvent.fire(new PlaceHiddenEvent(part.getPlace()));
}
@Override
public void onPartLostFocus() {
if (activePart == null) {
return;
}
placeLostFocusEvent.fire(new PlaceLostFocusEvent(activePart.getPlace()));
activePart = null;
}
@Override
public void onPanelFocus(final PanelDefinition panel) {
for (Map.Entry<PanelDefinition, WorkbenchPanelPresenter> e : mapPanelDefinitionToPresenter.entrySet()) {
e.getValue().setFocus(e.getKey().asString().equals(panel.asString()));
}
}
@Override
public void closePart(final PartDefinition part) {
placeManager.get().closePlace(part.getPlace());
}
void onSelectPlaceEvent(@Observes SelectPlaceEvent event) {
final PlaceRequest place = event.getPlace();
// TODO (hbraun): PanelDefinition is not distinct (missing hashcode)
for (Map.Entry<PanelDefinition, WorkbenchPanelPresenter> e : mapPanelDefinitionToPresenter.entrySet()) {
WorkbenchPanelPresenter panelPresenter = e.getValue();
for (PartDefinition part : ensureIterable(panelPresenter.getDefinition().getParts())) {
if (part.getPlace().asString().equals(place.asString())) {
panelPresenter.selectPart(part);
onPanelFocus(e.getKey());
}
}
}
}
@SuppressWarnings("unused")
private void onDropPlaceEvent(@Observes DropPlaceEvent event) {
final PartDefinition part = getPartForPlace(event.getPlace());
if (part != null) {
removePart(part);
}
}
@Override
public PanelDefinition getPanelForPlace(final PlaceRequest place) {
for (PartDefinition part : mapPartDefinitionToPresenter.keySet()) {
if (part.getPlace().equals(place)) {
return part.getParentPanel();
}
}
return null;
}
/**
* Returns the first live (associated with an active presenter) PartDefinition whose place matches the given one.
* @return the definition for the live part servicing the given place, or null if no such part can be found.
*/
protected PartDefinition getPartForPlace(final PlaceRequest place) {
for (PartDefinition part : mapPartDefinitionToPresenter.keySet()) {
if (part.getPlace().asString().equals(place.asString())) {
return part;
}
}
return null;
}
@SuppressWarnings("unused")
private void onChangeTitleWidgetEvent(@Observes ChangeTitleWidgetEvent event) {
final PlaceRequest place = event.getPlaceRequest();
final IsWidget titleDecoration = event.getTitleDecoration();
final String title = event.getTitle();
for (Map.Entry<PanelDefinition, WorkbenchPanelPresenter> e : mapPanelDefinitionToPresenter.entrySet()) {
final PanelDefinition panel = e.getKey();
final WorkbenchPanelPresenter presenter = e.getValue();
for (PartDefinition part : ensureIterable(panel.getParts())) {
if (place.equals(part.getPlace())) {
mapPartDefinitionToPresenter.get(part).setTitle(title);
presenter.changeTitle(part,
title,
titleDecoration);
break;
}
}
}
}
/**
* Destroys the presenter bean associated with the given part and removes the part's presenter from the panel
* presenter that contains it (which in turn removes the part definition from the panel definition and the part view
* from the panel view).
* @param part the definition of the workbench part (screen or editor) to remove from the layout.
*/
protected void removePart(final PartDefinition part) {
for (Map.Entry<PanelDefinition, WorkbenchPanelPresenter> e : mapPanelDefinitionToPresenter.entrySet()) {
final WorkbenchPanelPresenter panelPresenter = e.getValue();
if (panelPresenter.getDefinition().getParts().contains(part)) {
panelPresenter.removePart(part);
break;
}
}
WorkbenchPartPresenter deadPartPresenter = mapPartDefinitionToPresenter.remove(part);
getBeanFactory().destroy(deadPartPresenter);
}
@Override
public PanelDefinition addWorkbenchPanel(final PanelDefinition targetPanel,
final PanelDefinition childPanel,
final Position position) {
WorkbenchPanelPresenter targetPanelPresenter = mapPanelDefinitionToPresenter.get(targetPanel);
if (targetPanelPresenter == null) {
targetPanelPresenter = beanFactory.newWorkbenchPanel(targetPanel);
mapPanelDefinitionToPresenter.put(targetPanel,
targetPanelPresenter);
}
PanelDefinition newPanel;
// Position instance could come from a different script so we compare using position.getName
if (CompassPosition.ROOT.getName().equals(position.getName())) {
newPanel = rootPanelDef;
} else if (CompassPosition.SELF.getName().equals(position.getName())) {
newPanel = targetPanelPresenter.getDefinition();
} else {
String defaultChildType = targetPanelPresenter.getDefaultChildType();
if (defaultChildType == null) {
throw new IllegalArgumentException("Target panel (type " + targetPanelPresenter.getClass().getName() + ")"
+ " does not allow child panels");
}
if (childPanel.getPanelType().equals(PanelDefinition.PARENT_CHOOSES_TYPE)) {
childPanel.setPanelType(defaultChildType);
}
final WorkbenchPanelPresenter childPanelPresenter = beanFactory.newWorkbenchPanel(childPanel);
mapPanelDefinitionToPresenter.put(childPanel,
childPanelPresenter);
targetPanelPresenter.addPanel(childPanelPresenter,
position);
newPanel = childPanel;
}
onPanelFocus(newPanel);
return newPanel;
}
@Override
public PanelDefinition addCustomPanel(final HasWidgets container,
final String panelType) {
return addCustomPanelOnContainer(container,
panelType);
}
@Override
public PanelDefinition addCustomPanel(final HTMLElement container,
final String panelType) {
return addCustomPanelOnContainer(container,
panelType);
}
private PanelDefinition addCustomPanelOnContainer(final Object container,
final String panelType) {
PanelDefinition panelDef = new PanelDefinitionImpl(panelType);
final WorkbenchPanelPresenter panelPresenter = beanFactory.newWorkbenchPanel(panelDef);
Widget panelViewWidget = panelPresenter.getPanelView().asWidget();
panelViewWidget.addAttachHandler(new CustomPanelCleanupHandler(panelPresenter));
if (container instanceof HasWidgets) {
HasWidgets widgetContainer = (HasWidgets) container;
widgetContainer.add(panelViewWidget);
customPanels.put(panelDef,
widgetContainer);
} else {
HTMLElement htmlContainer = (HTMLElement) container;
appendWidgetToElement(htmlContainer,
panelViewWidget);
customPanelsInsideHTMLElements.put(panelDef,
htmlContainer);
}
mapPanelDefinitionToPresenter.put(panelDef,
panelPresenter);
onPanelFocus(panelDef);
return panelDef;
}
void appendWidgetToElement(final HTMLElement container,
final Widget panelViewWidget) {
DOMUtil.appendWidgetToElement(container,
panelViewWidget.asWidget());
}
/**
* Cleanup handler for custom panels that are removed from the DOM before they are removed via PlaceManager.
* @see PanelManagerImpl#addCustomPanel(HasWidgets, String)
*/
private final class CustomPanelCleanupHandler implements AttachEvent.Handler {
private final WorkbenchPanelPresenter panelPresenter;
private boolean detaching;
private CustomPanelCleanupHandler(WorkbenchPanelPresenter panelPresenter) {
this.panelPresenter = panelPresenter;
}
@Override
public void onAttachOrDetach(AttachEvent event) {
if (event.isAttached()) {
return;
}
if (!detaching && mapPanelDefinitionToPresenter.containsKey(panelPresenter.getDefinition())) {
System.out.println("Running cleanup for " + Debug.objectId(this));
detaching = true;
Scheduler.get().scheduleFinally(new ScheduledCommand() {
@Override
public void execute() {
try {
for (PartDefinition part : ensureIterable(panelPresenter.getDefinition().getParts())) {
placeManager.get().closePlace(part.getPlace());
}
// in many cases, the panel will have cleaned itself up when we closed its last part in the loop above.
// for other custom panel use cases, the panel may still be open. we can do the cleanup here.
if (mapPanelDefinitionToPresenter.containsKey(panelPresenter.getDefinition())) {
removeWorkbenchPanel(panelPresenter.getDefinition());
}
} finally {
detaching = false;
}
}
});
}
}
}
}