blob: f035d0033307041c0e19814ed280e2fc02dc6404 [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.rt.client.ui.desktop;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.scout.rt.client.ModelContextProxy;
import org.eclipse.scout.rt.client.ModelContextProxy.ModelContext;
import org.eclipse.scout.rt.client.context.ClientRunContext;
import org.eclipse.scout.rt.client.context.ClientRunContexts;
import org.eclipse.scout.rt.client.deeplink.DeepLinkException;
import org.eclipse.scout.rt.client.deeplink.IDeepLinks;
import org.eclipse.scout.rt.client.deeplink.OutlineDeepLinkHandler;
import org.eclipse.scout.rt.client.extension.ui.desktop.DefaultDesktopEventHistory;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopBeforeClosingChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopClosingChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopDefaultViewChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopFormAboutToShowChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopGuiAttachedChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopGuiDetachedChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopInitChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopLogoActionChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopOpenedChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopOutlineChangedChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopPageDetailFormChangedChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopPageDetailTableChangedChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopPageSearchFormChangedChain;
import org.eclipse.scout.rt.client.extension.ui.desktop.DesktopChains.DesktopTablePageLoadedChain;
import org.eclipse.scout.rt.client.job.ModelJobs;
import org.eclipse.scout.rt.client.services.common.bookmark.internal.BookmarkUtility;
import org.eclipse.scout.rt.client.services.common.icon.IIconProviderService;
import org.eclipse.scout.rt.client.session.ClientSessionProvider;
import org.eclipse.scout.rt.client.transformation.IDeviceTransformationService;
import org.eclipse.scout.rt.client.ui.AbstractWidget;
import org.eclipse.scout.rt.client.ui.Coordinates;
import org.eclipse.scout.rt.client.ui.IDisplayParent;
import org.eclipse.scout.rt.client.ui.IEventHistory;
import org.eclipse.scout.rt.client.ui.IWidget;
import org.eclipse.scout.rt.client.ui.action.ActionFinder;
import org.eclipse.scout.rt.client.ui.action.IAction;
import org.eclipse.scout.rt.client.ui.action.keystroke.IKeyStroke;
import org.eclipse.scout.rt.client.ui.action.menu.IMenu;
import org.eclipse.scout.rt.client.ui.action.menu.MenuUtility;
import org.eclipse.scout.rt.client.ui.action.menu.root.IContextMenu;
import org.eclipse.scout.rt.client.ui.action.view.IViewButton;
import org.eclipse.scout.rt.client.ui.basic.filechooser.IFileChooser;
import org.eclipse.scout.rt.client.ui.basic.table.ITable;
import org.eclipse.scout.rt.client.ui.basic.tree.ITreeNode;
import org.eclipse.scout.rt.client.ui.desktop.bench.layout.BenchLayoutData;
import org.eclipse.scout.rt.client.ui.desktop.datachange.DataChangeEvent;
import org.eclipse.scout.rt.client.ui.desktop.datachange.IDataChangeManager;
import org.eclipse.scout.rt.client.ui.desktop.notification.IDesktopNotification;
import org.eclipse.scout.rt.client.ui.desktop.notification.NativeNotificationDefaults;
import org.eclipse.scout.rt.client.ui.desktop.outline.AbstractOutlineViewButton;
import org.eclipse.scout.rt.client.ui.desktop.outline.IOutline;
import org.eclipse.scout.rt.client.ui.desktop.outline.pages.IPage;
import org.eclipse.scout.rt.client.ui.desktop.outline.pages.IPageWithTable;
import org.eclipse.scout.rt.client.ui.form.FormUtility;
import org.eclipse.scout.rt.client.ui.form.IForm;
import org.eclipse.scout.rt.client.ui.form.IFormHandler;
import org.eclipse.scout.rt.client.ui.form.IFormMenu;
import org.eclipse.scout.rt.client.ui.form.fields.button.IButton;
import org.eclipse.scout.rt.client.ui.messagebox.IMessageBox;
import org.eclipse.scout.rt.client.ui.messagebox.MessageBoxes;
import org.eclipse.scout.rt.client.ui.popup.PopupManager;
import org.eclipse.scout.rt.platform.BEANS;
import org.eclipse.scout.rt.platform.Order;
import org.eclipse.scout.rt.platform.OrderedComparator;
import org.eclipse.scout.rt.platform.annotations.ConfigOperation;
import org.eclipse.scout.rt.platform.annotations.ConfigProperty;
import org.eclipse.scout.rt.platform.classid.ClassId;
import org.eclipse.scout.rt.platform.context.PropertyMap;
import org.eclipse.scout.rt.platform.exception.ExceptionHandler;
import org.eclipse.scout.rt.platform.exception.PlatformError;
import org.eclipse.scout.rt.platform.exception.ProcessingException;
import org.eclipse.scout.rt.platform.exception.VetoException;
import org.eclipse.scout.rt.platform.holders.Holder;
import org.eclipse.scout.rt.platform.holders.IHolder;
import org.eclipse.scout.rt.platform.job.JobState;
import org.eclipse.scout.rt.platform.job.Jobs;
import org.eclipse.scout.rt.platform.reflect.ConfigurationUtility;
import org.eclipse.scout.rt.platform.resource.BinaryResource;
import org.eclipse.scout.rt.platform.text.TEXTS;
import org.eclipse.scout.rt.platform.util.Assertions;
import org.eclipse.scout.rt.platform.util.ChangeStatus;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
import org.eclipse.scout.rt.platform.util.ObjectUtility;
import org.eclipse.scout.rt.platform.util.StringUtility;
import org.eclipse.scout.rt.platform.util.TypeCastUtility;
import org.eclipse.scout.rt.platform.util.collection.OrderedCollection;
import org.eclipse.scout.rt.platform.util.concurrent.IRunnable;
import org.eclipse.scout.rt.platform.util.concurrent.ThreadInterruptedError;
import org.eclipse.scout.rt.shared.ISession;
import org.eclipse.scout.rt.shared.deeplink.DeepLinkUrlParameter;
import org.eclipse.scout.rt.shared.extension.AbstractExtension;
import org.eclipse.scout.rt.shared.extension.ContributionComposite;
import org.eclipse.scout.rt.shared.extension.ExtensionUtility;
import org.eclipse.scout.rt.shared.extension.IContributionOwner;
import org.eclipse.scout.rt.shared.extension.IExtensibleObject;
import org.eclipse.scout.rt.shared.extension.IExtension;
import org.eclipse.scout.rt.shared.extension.ObjectExtensions;
import org.eclipse.scout.rt.shared.job.filter.future.SessionFutureFilter;
import org.eclipse.scout.rt.shared.services.common.bookmark.Bookmark;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The desktop model (may) consist of
* <ul>
* <li>set of available outlines
* <li>active outline
* <li>active table view
* <li>active detail form
* <li>active search form
* <li>form stack (swing: dialogs on desktop as {@code JInternalFrame}s)
* <li>dialog stack of modal and non-modal dialogs (swing: dialogs as {@code JDialog}, {@code JFrame})
* <li>active message box stack
* <li>menubar menus
* <li>toolbar and viewbar actions
* </ul>
* The Eclipse Scout SDK creates a subclass of this class that can be used as initial desktop.
*/
@ClassId("a6761d3f-7627-4e10-8440-f5c4865ca73c")
public abstract class AbstractDesktop extends AbstractWidget implements IDesktop, IContributionOwner, IExtensibleObject {
private static final Logger LOG = LoggerFactory.getLogger(AbstractDesktop.class);
private IEventHistory<DesktopEvent> m_eventHistory;
private final IDesktopExtension m_localDesktopExtension;
private List<IDesktopExtension> m_desktopExtensions;
private final DesktopListeners m_listeners;
private int m_dataChanging;
private final IDesktopUIFacade m_uiFacade;
private List<IOutline> m_availableOutlines;
private IOutline m_outline;
private boolean m_outlineChanging = false;
private P_ActiveOutlineListener m_activeOutlineListener;
private ITable m_pageDetailTable;
private IForm m_pageDetailForm;
private IForm m_pageSearchForm;
private final FormStore m_formStore;
private final Map<String /*viewId*/, IForm> m_selectedViewTabs;
private final MessageBoxStore m_messageBoxStore;
private final FileChooserStore m_fileChooserStore;
private final Set<IDesktopNotification> m_notifications = new LinkedHashSet<>(); // LinkedHashSet to preserve insertion order
private List<IMenu> m_menus;
private List<IViewButton> m_viewButtons;
private boolean m_autoPrefixWildcardForTextSearch;
private boolean m_isForcedClosing = false;
private final List<Object> m_addOns;
private IContributionOwner m_contributionHolder;
private final ObjectExtensions<AbstractDesktop, org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> m_objectExtensions;
private final List<ClientCallback<Coordinates>> m_pendingPositionResponses = Collections.synchronizedList(new ArrayList<>());
private int m_attachedCount;
private int m_attachedGuiCount;
private boolean m_activatingDefaultView;
private final IDataChangeManager m_dataChangeListeners;
private final IDataChangeManager m_dataChangeDesktopInForegroundListeners;
/**
* do not instantiate a new desktop<br>
* get it via {@code ClientScoutSession.getSession().getModelManager()}
*/
public AbstractDesktop() {
this(true);
}
public AbstractDesktop(boolean callInitializer) {
super(false);
m_localDesktopExtension = new P_LocalDesktopExtension();
m_listeners = new DesktopListeners();
m_formStore = BEANS.get(FormStore.class);
m_selectedViewTabs = new HashMap<>();
m_messageBoxStore = BEANS.get(MessageBoxStore.class);
m_fileChooserStore = BEANS.get(FileChooserStore.class);
m_uiFacade = BEANS.get(ModelContextProxy.class).newProxy(new P_UIFacade(), ModelContext.copyCurrent().withDesktop(this));
m_addOns = new ArrayList<>();
m_objectExtensions = new ObjectExtensions<>(this, true);
m_dataChangeListeners = BEANS.get(IDataChangeManager.class);
m_dataChangeDesktopInForegroundListeners = BEANS.get(IDataChangeManager.class);
if (callInitializer) {
callInitializer();
}
}
@Override
protected void initConfigInternal() {
initStartupRequestParams();
runWithDesktop(this::interceptInitConfig);
}
private void interceptInitConfig() {
m_objectExtensions.initConfig(createLocalExtension(), this::initConfig);
}
protected IEventHistory<DesktopEvent> createEventHistory() {
return new DefaultDesktopEventHistory(5000L);
}
@Override
public IEventHistory<DesktopEvent> getEventHistory() {
return m_eventHistory;
}
@Override
public final List<Object> getAllContributions() {
return m_contributionHolder.getAllContributions();
}
@Override
public final <T> List<T> getContributionsByClass(Class<T> type) {
return m_contributionHolder.getContributionsByClass(type);
}
@Override
public final <T> T getContribution(Class<T> contribution) {
return m_contributionHolder.getContribution(contribution);
}
@Override
public final <T> T optContribution(Class<T> contribution) {
return m_contributionHolder.optContribution(contribution);
}
@Override
public List<? extends IWidget> getChildren() {
return CollectionUtility.flatten(super.getChildren(), getAvailableOutlines(), getActions() /* contains keystrokes, menus, view-buttons*/,
m_notifications, m_messageBoxStore.values(), m_selectedViewTabs.values(), m_formStore.values());
}
/*
* Configuration
*/
/**
* Configures the title of this desktop. The title is typically used as title for the main application window.
* <p>
* Subclasses can override this method. Default is {@code null}.
*
* @return the title of this desktop
*/
@ConfigProperty(ConfigProperty.TEXT)
@Order(10)
protected String getConfiguredTitle() {
return null;
}
/**
* Configures the outlines associated with this desktop. If multiple outlines are configured, there is typically a
* need to provide some means of switching between different outlines, such as a {@link AbstractOutlineViewButton}.
* <p>
* Note that {@linkplain IDesktopExtension desktop extensions} might contribute additional outlines to this desktop.
* <p>
* Subclasses can override this method. Default is {@code null}.
*
* @return an array of outline type tokens
* @see IOutline
*/
@ConfigProperty(ConfigProperty.OUTLINES)
@Order(20)
protected List<Class<? extends IOutline>> getConfiguredOutlines() {
return null;
}
/**
* @return <code>true</code> if UI key strokes to select view tabs are enabled, <code>false</code> otherwise. Default
* value is <code>true</code>.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(30)
protected boolean getConfiguredSelectViewTabsKeyStrokesEnabled() {
return true;
}
/**
* @return optional modifier to use for UI key strokes to select view tabs (only relevant when
* {@link #isSelectViewTabsKeyStrokesEnabled()} is <code>true</code>). Default value is
* {@link IKeyStroke#CONTROL}.
*/
@ConfigProperty(ConfigProperty.STRING)
@Order(40)
protected String getConfiguredSelectViewTabsKeyStrokeModifier() {
return IKeyStroke.CONTROL;
}
/**
* Configures the desktop style which defines the basic layout of the application in the UI.
* <p>
* Subclasses can override this method. Default is {@link IDesktop#DISPLAY_STYLE_DEFAULT}
*/
@ConfigProperty(ConfigProperty.STRING)
@Order(50)
protected String getConfiguredDisplayStyle() {
return DISPLAY_STYLE_DEFAULT;
}
/**
* Configures the logo id.
* <p>
* If specified, the logo will be displayed on the top right corner of the desktop.
* <p>
* Subclasses can override this method. Default is {@code null}.
*
* @since 6.0
* @see IIconProviderService
*/
protected String getConfiguredLogoId() {
return null;
}
/**
* Configures whether the position of the splitter between the navigation and bench should be stored in the session
* storage, so that the position may be restored after a page reload. If set to false, the default position is used.
*
* @return {@code true} if the splitter position should be cached, false if not
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(60)
protected boolean getConfiguredCacheSplitterPosition() {
return true;
}
/**
* @return the value to be assigned initially to the <i>navigationVisible</i> property when <code>displayStyle</code>
* = {@link IDesktop#DISPLAY_STYLE_DEFAULT}. This value is ignored for all other display styles.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(70)
protected boolean getConfiguredNavigationVisible() {
return true;
}
/**
* @return the value to be assigned initially to the <i>navigationHandleVisible</i> property when
* <code>displayStyle</code> = {@link IDesktop#DISPLAY_STYLE_DEFAULT}. This value is ignored for all other
* display styles.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(75)
protected boolean getConfiguredNavigationHandleVisible() {
return true;
}
/**
* @return the value to be assigned initially to the <i>headerVisible</i> property when <code>displayStyle</code> =
* {@link IDesktop#DISPLAY_STYLE_DEFAULT}. This value is ignored for all other display styles.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(80)
protected boolean getConfiguredHeaderVisible() {
return true;
}
/**
* @return the value to be assigned initially to the <i>benchVisible</i> property when <code>displayStyle</code> =
* {@link IDesktop#DISPLAY_STYLE_DEFAULT}. This value is ignored for all other display styles.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(90)
protected boolean getConfiguredBenchVisible() {
return true;
}
/**
* Configures if the logo action is enabled.
* <p>
* Subclasses can override this method. Default is {@code false}
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(100)
protected boolean getConfiguredLogoActionEnabled() {
return false;
}
/**
* Configures the default settings for native notifications. A specific notification can override these settings if
* desired.
* <p>
* The default object is initialized with the desktop's title and logo id. If these values change the defaults won't
* be adjusted automatically. If you want them to be aligned you need to update the native notification defaults
* whenever the title or logo changes. <br>
* Note: SVG icons may not work with every browser, you may have to use a bitmap icon.
* </p>
*
* @see IDesktopNotification
*/
@ConfigProperty(ConfigProperty.OBJECT)
@Order(110)
protected NativeNotificationDefaults getConfiguredNativeNotificationDefaults() {
return new NativeNotificationDefaults()
.withTitle(getTitle())
.withIconId(getLogoId())
.withVisibility(IDesktopNotification.NATIVE_NOTIFICATION_VISIBILITY_NONE);
}
private List<Class<? extends IAction>> getConfiguredActions() {
Class[] dca = ConfigurationUtility.getDeclaredPublicClasses(getClass());
List<Class<IAction>> fca = ConfigurationUtility.filterClasses(dca, IAction.class);
return ConfigurationUtility.removeReplacedClasses(fca);
}
/**
* @return the value to be assigned initially to the <i>dense</i> property. This value is ignored for
* <code>displayStyle</code> = {@link IDesktop#DISPLAY_STYLE_COMPACT}.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(100)
protected boolean getConfiguredDense() {
return false;
}
/**
* Called while this desktop is initialized.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(10)
protected void execInit() {
}
/**
* Called after this desktop was opened and displayed on the GUI.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(20)
protected void execOpened() {
}
/**
* Called just after the core desktop receives the request to close the desktop.
* <p>
* Subclasses can override this method to execute some custom code before the desktop gets into its closing state. The
* default behavior is to do nothing. By throwing an explicit {@link VetoException} the closing process will be
* stopped.
*/
@ConfigOperation
@Order(30)
protected void execBeforeClosing() {
}
/**
* Called before this desktop is being closed.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(40)
protected void execClosing() {
}
/**
* Called after a UI has been attached to this desktop. The desktop is opened at this point. This method may be called
* more than once. It is called immediately after the desktop has been opened and also in case of a reload, when a new
* UiSession has been created.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(50)
protected void execGuiAttached() {
}
/**
* Called after a UI has been detached from this desktop. This desktop must not necessarily be open.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(60)
protected void execGuiDetached() {
}
/**
* Called whenever a new outline has been activated on this desktop.
* <p>
* Subclasses can override this method. The default does nothing.
*
* @param oldOutline
* old outline that was active before
* @param newOutline
* new outline that is active after the change
*/
@ConfigOperation
@Order(70)
protected void execOutlineChanged(IOutline oldOutline, IOutline newOutline) {
}
/**
* Method invoked right before the given form is shown and therefore added to the desktop. That is before any UI is
* informed about the new form.<br/>
* Overwrite this method to modify the given form, or to replace it with another form instance. The default
* implementation simply returns the given form.
*
* @param form
* the form which is about to show.
* @return the form to show, or <code>null</code> to not show the form.
*/
@ConfigOperation
@Order(80)
protected IForm execFormAboutToShow(IForm form) {
return form;
}
/**
* Called whenever a new page has been activated (selected) on this desktop.
* <p>
* Subclasses can override this method.<br/>
* This default implementation does nothing.
*
* @param oldForm
* is the search form of the old (not selected anymore) page or {@code null}
* @param newForm
* is the search form of the new (selected) page or {@code null}
*/
@Order(90)
@ConfigOperation
protected void execPageSearchFormChanged(IForm oldForm, IForm newForm) {
}
/**
* Called whenever a new page has been activated (selected) on this desktop.
* <p>
* Subclasses can override this method.<br/>
* This default implementation does nothing.
*
* @param oldForm
* is the detail form of the old (not selected anymore) page or {@code null}
* @param newForm
* is the detail form of the new (selected) page or {@code null}
*/
@Order(100)
@ConfigOperation
protected void execPageDetailFormChanged(IForm oldForm, IForm newForm) {
}
/**
* Called whenever a new page has been activated (selected) on this desktop.
* <p>
* Subclasses can override this method.<br/>
* This default implementation keeps track of the current outline table form and updates it accordingly (including
* visibility). See also {@link #getPageDetailForm()} .
*
* @param oldTable
* is the table of the old (not selected anymore) table page or {@code null}
* @param newTable
* is the table of the new (selected) table page or {@code null}
*/
@Order(110)
@ConfigOperation
protected void execPageDetailTableChanged(ITable oldTable, ITable newTable) {
}
/**
* Called after a table page was loaded or reloaded.
* <p>
* Subclasses can override this method.<br/>
* The default implementation does nothing.
*
* @param tablePage
* the table page that has been (re)loaded
*/
@Order(120)
@ConfigOperation
protected void execTablePageLoaded(IPageWithTable<?> tablePage) {
}
/**
* Called by activateDefaultView when Scout application was not started with a deep-link or when deep-link could not
* be executed. The default implementation does nothing.
*/
@Order(130)
@ConfigOperation
protected void execDefaultView() {
}
@Order(140)
@ConfigOperation
protected void execLogoAction() {
}
public List<IDesktopExtension> getDesktopExtensions() {
return CollectionUtility.arrayList(m_desktopExtensions);
}
/**
* @return the special extension that contributes the contents of this desktop itself
*/
protected IDesktopExtension getLocalDesktopExtension() {
return m_localDesktopExtension;
}
protected org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop> createLocalExtension() {
return new LocalDesktopExtension<>(this);
}
@Override
public final List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> getAllExtensions() {
return m_objectExtensions.getAllExtensions();
}
@Override
public <T extends IExtension<?>> T getExtension(Class<T> c) {
return m_objectExtensions.getExtension(c);
}
@Override
protected void initConfig() {
super.initConfig();
m_eventHistory = createEventHistory();
// add convenience observer for event history
addDesktopListener(e -> {
IEventHistory<DesktopEvent> h = getEventHistory();
if (h != null) {
h.notifyEvent(e);
}
});
ClientSessionProvider.currentSession().getMemoryPolicy().registerDesktop(this);
BEANS.get(IDeviceTransformationService.class).install(this);
initDesktopExtensions();
setTitle(getConfiguredTitle());
setCssClass(getConfiguredCssClass());
setSelectViewTabsKeyStrokesEnabled(getConfiguredSelectViewTabsKeyStrokesEnabled());
setSelectViewTabsKeyStrokeModifier(getConfiguredSelectViewTabsKeyStrokeModifier());
setLogoId(getConfiguredLogoId());
setDisplayStyle(getConfiguredDisplayStyle());
initDisplayStyle(getDisplayStyle());
setCacheSplitterPosition(getConfiguredCacheSplitterPosition());
setLogoActionEnabled(getConfiguredLogoActionEnabled());
setNativeNotificationDefaults(getConfiguredNativeNotificationDefaults());
List<IDesktopExtension> extensions = getDesktopExtensions();
m_contributionHolder = new ContributionComposite(this);
// outlines
OrderedCollection<IOutline> outlines = new OrderedCollection<>();
for (IDesktopExtension ext : extensions) {
try {
ext.contributeOutlines(outlines);
}
catch (Exception t) {
LOG.error("contributing outlines by {}", ext, t);
}
}
List<IOutline> contributedOutlines = m_contributionHolder.getContributionsByClass(IOutline.class);
outlines.addAllOrdered(contributedOutlines);
// move outlines
ExtensionUtility.moveModelObjects(outlines);
setAvailableOutlines(outlines.getOrderedList());
// actions (keyStroke, menu, viewButton, toolButton)
List<IAction> actionList = new ArrayList<>();
for (IDesktopExtension ext : extensions) {
ext.contributeActions(actionList);
}
List<IAction> contributedActions = m_contributionHolder.getContributionsByClass(IAction.class);
actionList.addAll(contributedActions);
// build complete menu and viewButton lists
// only top level menus
OrderedComparator orderedComparator = new OrderedComparator();
List<IMenu> menuList = new ActionFinder().findActions(actionList, IMenu.class, false);
ExtensionUtility.moveModelObjects(menuList);
menuList.sort(orderedComparator);
m_menus = menuList;
List<IViewButton> viewButtonList = new ActionFinder().findActions(actionList, IViewButton.class, false);
ExtensionUtility.moveModelObjects(viewButtonList);
viewButtonList.sort(orderedComparator);
m_viewButtons = viewButtonList;
// add dynamic keyStrokes
List<IKeyStroke> ksList = new ActionFinder().findActions(actionList, IKeyStroke.class, true);
addKeyStrokes(ksList.toArray(new IKeyStroke[0]));
addPropertyChangeListener(new P_LocalPropertyChangeListener());
addAddOn(BEANS.get(PopupManager.class));
}
protected final void interceptInit() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopInitChain chain = new DesktopInitChain(extensions);
chain.execInit();
}
private void initDesktopExtensions() {
m_desktopExtensions = new ArrayList<>();
m_desktopExtensions.add(getLocalDesktopExtension());
injectDesktopExtensions(m_desktopExtensions);
}
/**
* Override to provide a set of extensions (modules) that contribute their content to this desktop.
* <p>
* The default list contains only the {@link #getLocalDesktopExtension()}
* </p>
* <p>
* The extension that are held by this desktop must call {@link IDesktopExtension#setCoreDesktop(IDesktop)} passing
* <code>this</code> as argument, before using the extension. That way the extension can use and access this desktop's
* methods.
* </p>
*
* @param desktopExtensions
* a live list can be modified.
*/
protected void injectDesktopExtensions(List<IDesktopExtension> desktopExtensions) {
List<IDesktopExtension> extensions = BEANS.all(IDesktopExtension.class);
for (IDesktopExtension e : extensions) {
e.setCoreDesktop(this);
}
desktopExtensions.addAll(extensions);
}
@Override
protected void initInternal() {
runWithDesktop(this::interceptInitInternal);
}
protected void interceptInitInternal() {
super.initInternal();
// extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
ContributionCommand cc = ext.initDelegate();
if (cc == ContributionCommand.Stop) {
break;
}
}
}
/**
* Applies values to the following properties, based on the given displayStyle: <i>navigationVisible</i>,
* <i>navigationHandleVisible</i>, <i>headerVisible</i>, <i>benchVisible</i>, <i>dense</i>.
*/
protected void initDisplayStyle(String displayStyle) {
if (DISPLAY_STYLE_BENCH.equals(displayStyle)) {
setNavigationVisible(false);
setNavigationHandleVisible(false);
setHeaderVisible(false);
setBenchVisible(true);
setDense(getConfiguredDense());
}
else if (DISPLAY_STYLE_COMPACT.equals(displayStyle)) {
setNavigationVisible(true);
setNavigationHandleVisible(false);
setHeaderVisible(false);
setBenchVisible(false);
setDense(false);
}
else {
// Default
setNavigationVisible(getConfiguredNavigationVisible());
setNavigationHandleVisible(getConfiguredNavigationHandleVisible());
setHeaderVisible(getConfiguredHeaderVisible());
setBenchVisible(getConfiguredBenchVisible());
setDense(getConfiguredDense());
}
}
@Override
public boolean isShowing(IForm form) {
if (form == null) {
return false;
}
// wrapped form
if (form.getOuterForm() != null) {
return form.getOuterForm().isShowing();
}
// dialog or view
if (m_formStore.contains(form)) {
return true;
}
// active detail or search Form
if (form == m_pageDetailForm || form == m_pageSearchForm) {
return true;
}
// active menu Form
for (IMenu menu : getMenus()) {
if (menu instanceof IFormMenu && menu.isSelected() && ((IFormMenu) menu).getForm() == form) {
return true;
}
}
return false;
}
@Override
public <FORM extends IForm> FORM findForm(Class<FORM> formType) {
if (formType == null) {
return null;
}
for (final IForm candidate : m_formStore.values()) {
if (formType.isAssignableFrom(candidate.getClass())) {
@SuppressWarnings("unchecked")
FORM form = (FORM) candidate;
return form;
}
}
return null;
}
@Override
public <FORM extends IForm> List<FORM> findForms(final Class<FORM> formType) {
if (formType == null) {
return CollectionUtility.emptyArrayList();
}
final List<FORM> forms = new ArrayList<>();
for (final IForm candidate : m_formStore.values()) {
if (formType.isAssignableFrom(candidate.getClass())) {
@SuppressWarnings("unchecked")
FORM form = (FORM) candidate;
forms.add(form);
}
}
return forms;
}
@Override
@SuppressWarnings("unchecked")
public <T extends IOutline> T findOutline(Class<T> outlineType) {
for (IOutline o : getAvailableOutlines()) {
if (outlineType.isAssignableFrom(o.getClass())) {
return (T) o;
}
}
return null;
}
@Override
public <T extends IAction> T findAction(Class<T> actionType) {
return new ActionFinder().findAction(getActions(), actionType);
}
@Override
public <T extends IViewButton> T findViewButton(Class<T> viewButtonType) {
return findAction(viewButtonType);
}
@Override
public String getDisplayStyle() {
return propertySupport.getPropertyString(PROP_DISPLAY_STYLE);
}
@Override
public void setDisplayStyle(String displayStyle) {
propertySupport.setProperty(PROP_DISPLAY_STYLE, displayStyle);
}
@Override
public String getLogoId() {
return propertySupport.getPropertyString(PROP_LOGO_ID);
}
@Override
public void setLogoId(String id) {
propertySupport.setPropertyString(PROP_LOGO_ID, id);
}
@Override
public IForm getActiveForm() {
return (IForm) propertySupport.getProperty(PROP_ACTIVE_FORM);
}
protected void setActiveForm(IForm form) {
if (form != null) {
form.getUIFacade().fireFormActivatedFromUI();
if (form.getDisplayHint() == IForm.DISPLAY_HINT_VIEW) {
String displayViewId = FormUtility.normalizeDisplayViewId(form.getDisplayViewId());
m_selectedViewTabs.put(displayViewId, form);
}
}
propertySupport.setProperty(PROP_ACTIVE_FORM, form);
}
@Override
public IWidget getFocusedElement() {
return (IWidget) propertySupport.getProperty(PROP_FOCUSED_ELEMENT);
}
protected void setFocusedElement(IWidget focusedElement) {
if (ObjectUtility.equals(focusedElement, getFocusedElement())) {
return;
}
if (getFocusedElement() instanceof AbstractWidget) {
((AbstractWidget) getFocusedElement()).notifyFocusOut();
}
propertySupport.setProperty(PROP_FOCUSED_ELEMENT, focusedElement);
if (focusedElement instanceof AbstractWidget) {
((AbstractWidget) focusedElement).notifyFocusIn();
}
}
@Override
public void setTrackFocus(boolean trackFocus) {
int trackFocusCount = getTrackFocus();
if (trackFocus) {
trackFocusCount++;
}
else {
trackFocusCount = Math.max(trackFocusCount - 1, 0);
}
propertySupport.setPropertyInt(PROP_TRACK_FOCUS, trackFocusCount);
}
protected int getTrackFocus() {
return propertySupport.getPropertyInt(PROP_TRACK_FOCUS);
}
@Override
public boolean isTrackFocus() {
return getTrackFocus() > 0;
}
@Override
public List<IForm> getForms(IDisplayParent displayParent) {
return m_formStore.getByDisplayParent(displayParent);
}
@Override
public List<IForm> getViews() {
return m_formStore.getViews();
}
@Override
public List<IForm> getViews(IDisplayParent displayParent) {
return m_formStore.getViewsByDisplayParent(displayParent);
}
@Override
public Collection<IForm> getSelectedViews(IDisplayParent displayParent) {
List<IForm> selectedViewTabs = new ArrayList<>();
for (IForm f : m_selectedViewTabs.values()) {
if (f.getDisplayParent() == displayParent) {
selectedViewTabs.add(f);
}
}
return selectedViewTabs;
}
@Override
public <F extends IForm, H extends IFormHandler> List<F> findAllOpenViews(Class<? extends F> formClass, Class<? extends H> handlerClass, Object exclusiveKey) {
List<F> forms = new ArrayList<>();
if (exclusiveKey != null) {
for (IForm view : getViews()) {
if (getPageDetailForm() == view || getPageSearchForm() == view) {
continue;
}
Object candidateKey = view.computeExclusiveKey();
if (candidateKey == null) {
continue;
}
else {
LOG.debug("form: {} vs {}", candidateKey, exclusiveKey);
if (exclusiveKey.equals(candidateKey)
&& view.getClass() == formClass
&& view.getHandler() != null
&& view.getHandler().getClass() == handlerClass
&& view.getHandler().isOpenExclusive()) {
forms.add(formClass.cast(view));
}
}
}
}
return forms;
}
@Override
public <F extends IForm, H extends IFormHandler> F findOpenView(Class<? extends F> formClass, Class<? extends H> handlerClass, Object exclusiveKey) {
return CollectionUtility.firstElement(findAllOpenViews(formClass, handlerClass, exclusiveKey));
}
@Override
public List<IForm> getDialogs() {
return m_formStore.getDialogs();
}
@Override
public List<IForm> getDialogs(final IDisplayParent displayParent, final boolean includeChildDialogs) {
final List<IForm> dialogs = new ArrayList<>();
for (final IForm dialog : m_formStore.getDialogsByDisplayParent(displayParent)) {
// Add the dialog's child dialogs first.
if (includeChildDialogs) {
dialogs.addAll(getDialogs(dialog, true));
}
// Add the dialog.
dialogs.add(dialog);
}
return dialogs;
}
public List<IMessageBox> getMessageBoxStack() {
return getMessageBoxes();
}
@Override
public List<IMessageBox> getMessageBoxes() {
return m_messageBoxStore.values();
}
@Override
public List<IDesktopNotification> getNotifications() {
return new ArrayList<>(m_notifications); // ordered as inserted because LinkedHashSet is used
}
@Override
public List<IMessageBox> getMessageBoxes(IDisplayParent displayParent) {
return m_messageBoxStore.getByDisplayParent(displayParent);
}
@Override
public List<IForm> getSimilarForms(final IForm form) {
if (form == null) {
return CollectionUtility.emptyArrayList();
}
final Object formKey;
try {
formKey = form.computeExclusiveKey();
}
catch (final RuntimeException | PlatformError e) {
BEANS.get(ExceptionHandler.class).handle(e);
return CollectionUtility.emptyArrayList();
}
if (formKey == null) {
return CollectionUtility.emptyArrayList();
}
final IForm currentDetailForm = getPageDetailForm();
final IForm currentSearchForm = getPageSearchForm();
final List<IForm> similarForms = new ArrayList<>();
for (final IForm candidateView : m_formStore.getFormsByExclusiveKey(formKey)) {
if (candidateView == currentDetailForm) {
continue;
}
if (candidateView == currentSearchForm) {
continue;
}
if (candidateView.getClass().equals(form.getClass())) {
similarForms.add(candidateView);
}
}
return similarForms;
}
@Override
public void ensureViewStackVisible() {
for (IForm view : m_formStore.getViews()) {
activateForm(view);
}
}
@Override
public void activateForm(IForm form) {
if (form == null) {
setActiveForm(form);
return;
}
if (!m_formStore.contains(form)) {
return; // only dialogs or views can be activated.
}
IDisplayParent displayParent = form.getDisplayParent();
if (displayParent instanceof IForm) {
activateForm((IForm) displayParent);
}
else if (displayParent instanceof IOutline && !displayParent.equals(getOutline())) {
activateOutline(((IOutline) displayParent));
}
setActiveForm(form);
fireFormActivate(form);
}
@Override
public void activateOutline(IOutline outline) {
if (m_outlineChanging) {
return;
}
final IOutline newOutline = resolveOutline(outline);
if (m_outline == newOutline) {
if (m_outline != null && getActiveForm() != null) {
m_outline.createDisplayParentRunContext().run(this::fireOutlineContentActivate);
}
return;
}
try {
m_outlineChanging = true;
setOutlineInternal(newOutline);
}
finally {
m_outlineChanging = false;
}
}
protected void setOutlineInternal(IOutline newOutline) {
final IOutline oldOutline = m_outline;
if (oldOutline != null) {
if (m_activeOutlineListener != null) {
oldOutline.removePropertyChangeListener(m_activeOutlineListener);
m_activeOutlineListener = null;
}
m_outline.deactivate();
}
// set new outline to set facts
m_outline = newOutline;
// deactivate old page
if (oldOutline != null) {
oldOutline.clearContextPage();
}
final ClientRunContext ctx;
if (m_outline == null) {
ctx = ClientRunContexts.copyCurrent().withOutline(null, true);
}
else {
ctx = m_outline.createDisplayParentRunContext();
}
ctx.run(() -> {
if (m_outline != null) {
m_activeOutlineListener = new P_ActiveOutlineListener();
m_outline.addPropertyChangeListener(m_activeOutlineListener);
setBrowserHistoryEntry(BEANS.get(OutlineDeepLinkHandler.class).createBrowserHistoryEntry(m_outline, m_activatingDefaultView));
}
// <bsh 2010-10-15>
// Those three "setXyz(null)" statements used to be called unconditionally. Now, they
// are only called when the new outline is null. When the new outline is _not_ null, we
// will override the "null" anyway (see below).
// This change is needed for the "on/off semantics" of the tool tab buttons to work correctly.
if (m_outline == null) {
setPageDetailForm(null);
setPageDetailTable(null);
setPageSearchForm(null, true);
}
// </bsh>
updateActiveFormOnOutlineChanged();
fireOutlineChanged(oldOutline, m_outline);
onOutlineChangedInternal();
fireOutlineContentActivate();
});
}
protected void updateActiveFormOnOutlineChanged() {
// If active form is not set or parent of active form is desktop
// we must do nothing
IForm activeForm = getActiveForm();
if (activeForm == null || activeForm.getDisplayParent() == this) {
return;
}
// Does the active form belong to the current outline?
// if not, we must set the active form to null otherwise we do nothing
List<IForm> formsByOutline = m_formStore.getByDisplayParent(m_outline);
if (!formsByOutline.contains(activeForm)) {
setActiveForm(null);
}
}
/**
* Called after the outline has been changed
*/
protected void onOutlineChangedInternal() {
if (m_outline == null) {
return;
}
// reload selected page in case it is marked dirty
if (m_outline.getActivePage() != null) {
try {
m_outline.getActivePage().ensureChildrenLoaded();
}
catch (RuntimeException | PlatformError e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
if (m_outline.getRootNode() != null) {
// XXX CGU Wieso macht man das hier und nicht in activate?
m_outline.setNodeExpanded(m_outline.getRootNode(), true);
}
setPageDetailForm(m_outline.getDetailForm());
setPageDetailTable(m_outline.getDetailTable());
setPageSearchForm(m_outline.getSearchForm(), true);
m_outline.makeActivePageToContextPage();
m_outline.activate();
}
@Override
public void showForm(IForm form) {
Assertions.assertFalse(ClientSessionProvider.currentSession().isStopping(), "Session is stopping");
// Let the desktop extensions to intercept the given form.
final IHolder<IForm> formHolder = new Holder<>(form);
for (IDesktopExtension extension : getDesktopExtensions()) {
try {
if (extension.formAboutToShowDelegate(formHolder) == ContributionCommand.Stop) {
break;
}
}
catch (Exception e) {
formHolder.setValue(form);
BEANS.get(ExceptionHandler.class).handle(e);
}
}
form = formHolder.getValue();
// Only show the form if not null nor already showing.
if (form == null || m_formStore.contains(form)) {
return;
}
// Validate the Form's modality configuration.
Assertions.assertNotNull(form.getDisplayParent(), "Property 'displayParent' must not be null");
boolean view = (form.getDisplayHint() == IForm.DISPLAY_HINT_VIEW);
// Ensure not to show a modal Form if the application is in 'application-modal' state. Otherwise, no user interaction would be possible.
if (view && form.isModal()) {
boolean applicationLocked = m_formStore.containsApplicationModalDialogs() || m_messageBoxStore.containsApplicationModalMessageBoxes() || m_fileChooserStore.containsApplicationModalFileChoosers();
if (applicationLocked) {
Assertions.fail(generateApplicationModalErrorMessage());
}
}
m_formStore.add(form);
activateForm(form);
fireFormShow(form);
}
protected String generateApplicationModalErrorMessage() {
String message = "Modal view cannot be shown because application is in 'desktop-modal' state; otherwise, no user interaction would be possible.";
String applicationModalForms = m_formStore.getApplicationModalDialogs()
.stream()
.map(f -> f.getClass().getSimpleName() + "; " + f.getTitle())
.collect(Collectors.joining(", "));
String applicationModalMessageBoxes = m_messageBoxStore.getApplicationModalMessageBoxes()
.stream()
.map(f -> f.getClass().getSimpleName() + "; " + f.getHeader())
.collect(Collectors.joining(", "));
String applicationModalFileChoosers = m_fileChooserStore.getApplicationModalFileChoosers()
.stream()
.map(f -> f.getClass().getSimpleName())
.collect(Collectors.joining(", "));
return message + "\n" + "Forms: [" + applicationModalForms + "]; MessageBoxes: [" + applicationModalMessageBoxes + "]; FileChoosers: [" + applicationModalFileChoosers + "]";
}
@Override
public void hideForm(IForm form) {
if (form == null || !m_formStore.contains(form)) {
return;
}
// Unset the currently active Form if being the given Form.
// The new active Form will be set by the UI or manually by IDesktop.activateForm(IForm).
if (getActiveForm() == form) {
setActiveForm(null);
}
if (form.getDisplayHint() == IForm.DISPLAY_HINT_VIEW) {
String displayViewId = FormUtility.normalizeDisplayViewId(form.getDisplayViewId());
if (m_selectedViewTabs.get(displayViewId) == form) {
m_selectedViewTabs.remove(displayViewId);
}
}
m_formStore.remove(form);
fireFormHide(form);
}
@Override
public void showMessageBox(IMessageBox messageBox) {
Assertions.assertFalse(ClientSessionProvider.currentSession().isStopping(), "Session is stopping");
if (messageBox == null || m_messageBoxStore.contains(messageBox)) {
return;
}
// Ensure 'displayParent' to be set.
Assertions.assertNotNull(messageBox.getDisplayParent(), "Property 'displayParent' must not be null");
m_messageBoxStore.add(messageBox);
fireMessageBoxShow(messageBox);
}
@Override
public void hideMessageBox(IMessageBox messageBox) {
if (messageBox == null || !m_messageBoxStore.contains(messageBox)) {
return;
}
m_messageBoxStore.remove(messageBox);
fireMessageBoxHide(messageBox);
}
@Override
public boolean isShowing(IMessageBox messageBox) {
return m_messageBoxStore.contains(messageBox);
}
@Override
public List<IOutline> getAvailableOutlines() {
return CollectionUtility.arrayList(m_availableOutlines);
}
@Override
public void setAvailableOutlines(List<? extends IOutline> availableOutlines) {
activateOutline(null);
if (m_availableOutlines != null) {
for (IOutline o : m_availableOutlines) {
ClientSessionProvider.currentSession().getMemoryPolicy().deregisterOutline(o);
}
}
m_availableOutlines = CollectionUtility.arrayList(availableOutlines);
for (IOutline o : m_availableOutlines) {
ClientSessionProvider.currentSession().getMemoryPolicy().registerOutline(o);
}
}
@Override
public IOutline getOutline() {
return m_outline;
}
@Override
public boolean isOutlineChanging() {
return m_outlineChanging;
}
@Override
public void activateFirstPage() {
if (m_outline.isRootNodeVisible()) {
m_outline.selectNode(m_outline.getRootNode(), false);
}
else {
List<ITreeNode> children = m_outline.getRootNode().getChildNodes();
if (CollectionUtility.hasElements(children)) {
for (ITreeNode node : children) {
if (node.isVisible() && node.isEnabled()) {
m_outline.selectNode(node, false);
break;
}
}
}
}
}
private IOutline resolveOutline(IOutline outline) {
for (IOutline o : getAvailableOutlines()) {
if (o == outline) {
return o;
}
}
return null;
}
@Override
public void setOutline(Class<? extends IOutline> outlineType) {
if (outlineType == null) {
return;
}
for (IOutline o : getAvailableOutlines()) {
if (outlineType.isInstance(o)) {
activateOutline(o);
return;
}
}
}
@Override
public Set<IKeyStroke> getKeyStrokes() {
return CollectionUtility.hashSet(getKeyStrokesInternal());
}
/**
* @return the internal key stroke list. May be {@code null}.
*/
protected Set<IKeyStroke> getKeyStrokesInternal() {
return propertySupport.getPropertySet(PROP_KEY_STROKES);
}
@Override
public void setKeyStrokes(Collection<? extends IKeyStroke> ks) {
propertySupport.setPropertySet(PROP_KEY_STROKES, CollectionUtility.<IKeyStroke> hashSetWithoutNullElements(ks));
}
@Override
public void addKeyStrokes(IKeyStroke... keyStrokes) {
if (keyStrokes != null && keyStrokes.length > 0) {
Map<String, IKeyStroke> map = new HashMap<>();
Set<IKeyStroke> currentKeyStrokes = getKeyStrokesInternal();
if (CollectionUtility.hasElements(currentKeyStrokes)) {
for (IKeyStroke ks : currentKeyStrokes) {
map.put(ks.getKeyStroke(), ks);
}
}
for (IKeyStroke ks : keyStrokes) {
map.put(ks.getKeyStroke(), ks);
}
setKeyStrokes(map.values());
}
}
@Override
public void removeKeyStrokes(IKeyStroke... keyStrokes) {
if (keyStrokes != null && keyStrokes.length > 0) {
Map<String, IKeyStroke> map = new HashMap<>();
Set<IKeyStroke> currentKeyStrokes = getKeyStrokesInternal();
if (CollectionUtility.hasElements(currentKeyStrokes)) {
for (IKeyStroke ks : currentKeyStrokes) {
map.put(ks.getKeyStroke(), ks);
}
}
for (IKeyStroke ks : keyStrokes) {
map.remove(ks.getKeyStroke());
}
setKeyStrokes(map.values());
}
}
@Override
public <T extends IMenu> T getMenuByClass(Class<T> menuType) {
// ActionFinder performs instance-of checks. Hence the menu replacement mapping is not required
return MenuUtility.getMenuByClass(this, menuType);
}
@Override
public IContextMenu getContextMenu() {
return null;
}
@Override
public List<IMenu> getMenus() {
return CollectionUtility.arrayList(m_menus);
}
@Override
public List<IAction> getActions() {
Set<IKeyStroke> keyStrokes = getKeyStrokesInternal();
List<IAction> result = new ArrayList<>(CollectionUtility.size(keyStrokes) + CollectionUtility.size(m_menus) + CollectionUtility.size(m_viewButtons));
CollectionUtility.appendAllList(result, keyStrokes);
CollectionUtility.appendAllList(result, m_menus);
CollectionUtility.appendAllList(result, m_viewButtons);
return result;
}
@Override
public <T extends IViewButton> T getViewButton(Class<? extends T> searchType) {
// ActionFinder performs instance-of checks. Hence the viewbutton replacement mapping is not required
return new ActionFinder().findAction(getViewButtons(), searchType);
}
@Override
public List<IViewButton> getViewButtons() {
return CollectionUtility.arrayList(m_viewButtons);
}
@Override
public IForm getPageDetailForm() {
return m_pageDetailForm;
}
@Override
public void setPageDetailForm(IForm f) {
if (m_pageDetailForm != f) {
IForm oldForm = m_pageDetailForm;
m_pageDetailForm = f;
//extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.pageDetailFormChangedDelegate(oldForm, m_pageDetailForm);
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (Exception e) {
LOG.error("extension {}", ext, e);
}
}
}
}
@Override
public IForm getPageSearchForm() {
return m_pageSearchForm;
}
@Override
public void setPageSearchForm(IForm f) {
setPageSearchForm(f, false);
}
public void setPageSearchForm(IForm f, boolean force) {
if (force || m_pageSearchForm != f) {
IForm oldForm = m_pageSearchForm;
m_pageSearchForm = f;
//extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.pageSearchFormChangedDelegate(oldForm, m_pageSearchForm);
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (Exception t) {
LOG.error("extension {}", ext, t);
}
}
}
}
@Override
public ITable getPageDetailTable() {
return m_pageDetailTable;
}
@Override
public void setPageDetailTable(ITable t) {
if (m_pageDetailTable != t) {
ITable oldTable = m_pageDetailTable;
m_pageDetailTable = t;
//extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.pageDetailTableChangedDelegate(oldTable, m_pageDetailTable);
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (Exception x) {
LOG.error("extension {}", ext, x);
}
}
}
}
@Override
public String getTitle() {
return propertySupport.getPropertyString(PROP_TITLE);
}
@Override
public void setTitle(String s) {
propertySupport.setPropertyString(PROP_TITLE, s);
}
@Override
public boolean isSelectViewTabsKeyStrokesEnabled() {
return propertySupport.getPropertyBool(PROP_SELECT_VIEW_TABS_KEY_STROKES_ENABLED);
}
@Override
public void setSelectViewTabsKeyStrokesEnabled(boolean selectViewTabsKeyStrokesEnabled) {
propertySupport.setPropertyBool(PROP_SELECT_VIEW_TABS_KEY_STROKES_ENABLED, selectViewTabsKeyStrokesEnabled);
}
@Override
public String getSelectViewTabsKeyStrokeModifier() {
return propertySupport.getPropertyString(PROP_SELECT_VIEW_TABS_KEY_STROKE_MODIFIER);
}
@Override
public void setSelectViewTabsKeyStrokeModifier(String selectViewTabsKeyStrokeModifier) {
propertySupport.setPropertyString(PROP_SELECT_VIEW_TABS_KEY_STROKE_MODIFIER, selectViewTabsKeyStrokeModifier);
}
/**
* @return request or URL parameters available at the time the Scout session/desktop has been attached. This map will
* be available after the desktop has been created but before the openFromUI and attachedFromUI are called.
*/
@Override
public PropertyMap getStartupRequestParams() {
return (PropertyMap) propertySupport.getProperty(PROP_STARTUP_REQUEST_PARAMS);
}
@Override
public <VALUE> VALUE getStartupRequestParam(String propertyName) {
return getStartupRequestParams().get(propertyName);
}
@Override
public String getStartupUrl() {
return getStartupRequestParam(STARTUP_REQUEST_PARAM_URL);
}
/**
* While the Desktop is initialized, the UiSession class makes sure the PropertyMap contains the startup request
* parameters. The desktop keeps a copy of these parameters which does not change until the desktop is re-attached.
*/
protected void initStartupRequestParams() {
PropertyMap copy = new PropertyMap(PropertyMap.CURRENT.get());
propertySupport.setProperty(PROP_STARTUP_REQUEST_PARAMS, copy);
}
@Override
public void addNotification(final IDesktopNotification notification) {
if (notification != null && m_notifications.add(notification)) {
fireNotification(DesktopEvent.TYPE_NOTIFICATION_ADDED, notification);
if (notification.getDuration() > 0) {
ModelJobs.schedule(() -> removeNotification(notification), ModelJobs.newInput(ClientRunContexts.copyCurrent())
.withExecutionTrigger(Jobs.newExecutionTrigger()
.withStartIn(notification.getDuration(), TimeUnit.MILLISECONDS)));
}
}
}
@Override
public void removeNotification(IDesktopNotification notification) {
removeNotificationInternal(notification, true);
}
protected void removeNotificationInternal(IDesktopNotification notification, boolean notifyUI) {
if (m_notifications.remove(notification) && notifyUI) {
fireNotification(DesktopEvent.TYPE_NOTIFICATION_REMOVED, notification);
}
}
@Override
public BrowserHistoryEntry getBrowserHistoryEntry() {
return (BrowserHistoryEntry) propertySupport.getProperty(PROP_BROWSER_HISTORY_ENTRY);
}
@Override
public void setBrowserHistoryEntry(BrowserHistoryEntry browserHistory) {
propertySupport.setProperty(PROP_BROWSER_HISTORY_ENTRY, browserHistory);
}
@Override
public boolean isCacheSplitterPosition() {
return propertySupport.getPropertyBool(PROP_CACHE_SPLITTER_POSITION);
}
@Override
public void setCacheSplitterPosition(boolean b) {
propertySupport.setPropertyBool(PROP_CACHE_SPLITTER_POSITION, b);
}
@Override
public List<IFileChooser> getFileChoosers() {
return m_fileChooserStore.values();
}
@Override
public List<IFileChooser> getFileChoosers(IDisplayParent displayParent) {
return m_fileChooserStore.getByDisplayParent(displayParent);
}
@Override
public void showFileChooser(IFileChooser fileChooser) {
if (fileChooser == null || m_fileChooserStore.contains(fileChooser)) {
return;
}
// Ensure 'displayParent' to be set.
Assertions.assertNotNull(fileChooser.getDisplayParent(), "Property 'displayParent' must not be null");
m_fileChooserStore.add(fileChooser);
fireFileChooserShow(fileChooser);
}
@Override
public void hideFileChooser(IFileChooser fileChooser) {
if (fileChooser == null || !m_fileChooserStore.contains(fileChooser)) {
return;
}
m_fileChooserStore.remove(fileChooser);
fireFileChooserHide(fileChooser);
}
@Override
public boolean isShowing(IFileChooser fileChooser) {
return m_fileChooserStore.contains(fileChooser);
}
@Override
public void openUri(String url, IOpenUriAction openUriAction) {
Assertions.assertNotNull(url);
Assertions.assertNotNull(openUriAction);
fireOpenUri(url, openUriAction);
}
@Override
public void openUri(BinaryResource res) {
Assertions.assertNotNull(res);
openUri(res, BEANS.get(OpenUriHelper.class).getOpenUriActionForResource(res));
}
@Override
public void openUri(BinaryResource res, IOpenUriAction openUriAction) {
Assertions.assertNotNull(res);
Assertions.assertNotNull(openUriAction);
fireOpenUri(res, openUriAction);
}
@Override
public boolean isAutoPrefixWildcardForTextSearch() {
return m_autoPrefixWildcardForTextSearch;
}
@Override
public void setAutoPrefixWildcardForTextSearch(boolean b) {
m_autoPrefixWildcardForTextSearch = b;
}
@Override
public boolean isOpened() {
return propertySupport.getPropertyBool(PROP_OPENED);
}
private void setOpenedInternal(boolean b) {
propertySupport.setPropertyBool(PROP_OPENED, b);
}
@Override
public boolean isGuiAvailable() {
return propertySupport.getPropertyBool(PROP_GUI_AVAILABLE);
}
@Override
public void reloadGui() {
fireReloadGUI();
}
@Override
public boolean isGeolocationServiceAvailable() {
return propertySupport.getPropertyBool(PROP_GEOLOCATION_SERVICE_AVAILABLE);
}
@Override
public void setGeolocationServiceAvailable(boolean available) {
propertySupport.setPropertyBool(PROP_GEOLOCATION_SERVICE_AVAILABLE, available);
}
@Override
public Future<Coordinates> requestGeolocation() {
synchronized (m_pendingPositionResponses) {
if (m_pendingPositionResponses.isEmpty()) {
fireRequestGeolocation();
}
ClientCallback<Coordinates> responseFuture = new ClientCallback<Coordinates>() {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
removePendingResponse();
return super.cancel(mayInterruptIfRunning);
}
@Override
protected void timedOut() {
removePendingResponse();
}
private void removePendingResponse() {
synchronized (m_pendingPositionResponses) {
m_pendingPositionResponses.remove(this);
}
}
};
m_pendingPositionResponses.add(responseFuture);
return responseFuture;
}
}
@Override
public DesktopListeners desktopListeners() {
return m_listeners;
}
@Override
public IDataChangeManager dataChangeListeners() {
return m_dataChangeListeners;
}
@Override
public IDataChangeManager dataChangeDesktopInForegroundListeners() {
return m_dataChangeDesktopInForegroundListeners;
}
@Override
public boolean isDataChanging() {
return m_dataChanging > 0;
}
@Override
public void setDataChanging(boolean b) {
if (b) {
m_dataChanging++;
}
else if (m_dataChanging > 0) {
m_dataChanging--;
}
m_dataChangeListeners.setBuffering(isDataChanging());
m_dataChangeDesktopInForegroundListeners.setBuffering(isDataChanging() || isInBackground());
}
@Override
public void dataChanged(Object... eventTypes) {
for (Object eventType : eventTypes) {
//this code is only used to manage the ambiguity of untyped Object... varargs
if (eventType instanceof DataChangeEvent) {
LOG.warn("Calling IDesktop.dataChanged with an argument of type '{}'. Call IDesktop.fireDataChangeEvent instead!", eventType.getClass());
fireDataChangeEvent((DataChangeEvent) eventType);
}
else {
fireDataChangeEvent(new DataChangeEvent(eventType, ChangeStatus.UPDATED));
}
}
}
@Override
public void fireDataChangeEvent(DataChangeEvent event) {
m_dataChangeListeners.fireEvent(event);
m_dataChangeDesktopInForegroundListeners.fireEvent(event);
}
private void fireDesktopClosed() {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_DESKTOP_CLOSED);
fireDesktopEvent(e);
}
private void fireOutlineChanged(IOutline oldOutline, IOutline newOutline) {
// fire
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_OUTLINE_CHANGED, newOutline);
fireDesktopEvent(e);
if (oldOutline != newOutline) {
//extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.outlineChangedDelegate(oldOutline, newOutline);
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (RuntimeException | PlatformError ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}
}
}
private void fireOutlineContentActivate() {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_OUTLINE_CONTENT_ACTIVATE);
fireDesktopEvent(e);
}
private void fireFormShow(IForm form) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_FORM_SHOW, form);
fireDesktopEvent(e);
}
private void fireFormActivate(IForm form) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_FORM_ACTIVATE, form);
fireDesktopEvent(e);
}
private void fireFormHide(IForm form) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_FORM_HIDE, form);
fireDesktopEvent(e);
}
private void fireMessageBoxShow(IMessageBox messageBox) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_MESSAGE_BOX_SHOW, messageBox);
fireDesktopEvent(e);
}
private void fireMessageBoxHide(IMessageBox messageBox) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_MESSAGE_BOX_HIDE, messageBox);
fireDesktopEvent(e);
}
private void fireFileChooserShow(IFileChooser fileChooser) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_FILE_CHOOSER_SHOW, fileChooser);
fireDesktopEvent(e);
}
private void fireFileChooserHide(IFileChooser fileChooser) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_FILE_CHOOSER_HIDE, fileChooser);
fireDesktopEvent(e);
}
private void fireOpenUri(String uri, IOpenUriAction openUriAction) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_OPEN_URI, uri, openUriAction);
fireDesktopEvent(e);
}
private void fireOpenUri(BinaryResource res, IOpenUriAction openUriAction) {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_OPEN_URI, res, openUriAction);
fireDesktopEvent(e);
}
private void fireRequestGeolocation() {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_REQUEST_GEOLOCATION);
fireDesktopEvent(e);
}
private void fireNotification(int eventType, IDesktopNotification notification) {
DesktopEvent e = new DesktopEvent(this, eventType, notification);
fireDesktopEvent(e);
}
private void fireReloadGUI() {
DesktopEvent e = new DesktopEvent(this, DesktopEvent.TYPE_RELOAD_GUI);
fireDesktopEvent(e);
}
// main handler
protected void fireDesktopEvent(DesktopEvent e) {
desktopListeners().fireEvent(e);
}
@Override
public void activateBookmark(Bookmark bm) {
activateBookmark(bm, true);
}
@Override
public void activateBookmark(Bookmark bm, boolean activateOutline) {
BookmarkUtility.activateBookmark(this, bm, activateOutline);
}
@Override
public Bookmark createBookmark() {
return BookmarkUtility.createBookmark(this);
}
@Override
public Bookmark createBookmark(IPage<?> page) {
return BookmarkUtility.createBookmark(page);
}
@Override
public void refreshPages(Class<?>... pageTypes) {
for (IOutline outline : getAvailableOutlines()) {
outline.refreshPages(pageTypes);
}
}
@Override
public void refreshPages(List<Class<? extends IPage<?>>> pages) {
for (IOutline outline : getAvailableOutlines()) {
outline.refreshPages(pages);
}
}
@Override
public void releaseUnusedPages() {
for (IOutline outline : getAvailableOutlines()) {
outline.releaseUnusedPages();
}
}
@Override
public void afterTablePageLoaded(IPageWithTable<?> tablePage) {
//extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.tablePageLoadedDelegate(tablePage);
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (Exception t) {
LOG.error("extension {}", ext, t);
}
}
}
@Override
protected void disposeChildren(List<? extends IWidget> widgetsToDispose) {
// do not include child forms in dispose because they will be disposed in closeInternal() using IForm#doClose
widgetsToDispose.removeAll(m_formStore.values());
super.disposeChildren(widgetsToDispose);
}
@Override
protected void initChildren(List<? extends IWidget> widgets) {
runWithDesktop(() -> interceptInitChildren(widgets));
}
protected void interceptInitChildren(List<? extends IWidget> widgets) {
// same as in dispose: exclude the forms. Form will be initialized when they are started.
widgets.removeAll(m_formStore.values());
super.initChildren(widgets);
}
/**
* Run the initialization runnable on behalf of this Desktop instance.
*/
protected void runWithDesktop(IRunnable runnable) {
ClientRunContexts.copyCurrent().withDesktop(this).run(runnable);
}
@Override
protected final void disposeInternal() {
try {
closeInternal();
}
catch (RuntimeException e) {
LOG.warn("Exception while disposing desktop.", e);
}
super.disposeInternal();
}
@Override
public void closeInternal() {
setOpenedInternal(false);
while (m_attachedGuiCount > 0) {
detachGui();
}
List<IForm> showedForms = new ArrayList<>();
// Remove showed forms
for (IForm form : m_formStore.values()) {
hideForm(form);
showedForms.add(form);
}
//release all locks
internalInterruptBlockingConditions();
internalCloseMessageBoxes(getMessageBoxes());
internalCloseFileChoosers(getFileChoosers());
internalCloseForms(showedForms);
internalCloseDesktopExtensions();
//again, release all locks that may have created by closing forms and client extensions
internalInterruptBlockingConditions();
internalCloseMessageBoxes(getMessageBoxes());
internalCloseFileChoosers(getFileChoosers());
internalCloseClientCallbacks();
fireDesktopClosed();
}
/**
* interrupt blocking MessageBoxes and FileChoosers, calling code must not continue work
*/
protected void internalInterruptBlockingConditions() {
Jobs.getJobManager().cancel(ModelJobs.newFutureFilterBuilder()
.andMatch(new SessionFutureFilter(ISession.CURRENT.get()))
.andMatchState(JobState.WAITING_FOR_BLOCKING_CONDITION)
.toFilter(), true);
}
protected void internalCloseDesktopExtensions() {
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.desktopClosingDelegate();
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (RuntimeException | PlatformError t) {
LOG.error("extension {}", ext, t);
}
}
}
protected void internalCloseMessageBoxes(List<IMessageBox> list) {
for (IMessageBox m : list) {
if (m != null) {
try {
m.doClose();
}
catch (RuntimeException | PlatformError e) {
LOG.error("Exception while closing messagebox", e);
}
finally {
m_messageBoxStore.remove(m);
}
}
}
}
protected void internalCloseFileChoosers(List<IFileChooser> list) {
for (IFileChooser f : list) {
if (f != null) {
try {
f.doClose();
}
catch (RuntimeException | PlatformError e) {
LOG.error("Exception while closing filechooser", e);
}
finally {
m_fileChooserStore.remove(f);
}
}
}
}
protected void internalCloseClientCallbacks() {
for (ClientCallback<?> c : CollectionUtility.arrayList(m_pendingPositionResponses)) {
if (c != null) {
try {
c.cancel(true);
c.failed(new ThreadInterruptedError("desktop is closing"));
}
catch (RuntimeException | PlatformError e) {
LOG.error("Exception while closing client callback", e);
}
}
}
}
protected boolean internalCloseForms(Collection<IForm> forms) {
for (IForm form : forms) {
if (form != null) {
try {
// close potential open MessageBoxes and FileChoosers, otherwise blocking threads will remain
internalCloseMessageBoxes(getMessageBoxes(form));
internalCloseFileChoosers(getFileChoosers(form));
try {
form.doClose();
}
finally {
form.dispose();
}
}
catch (RuntimeException | PlatformError e) {
LOG.error("Exception while closing forms", e);
}
}
}
return true;
}
/**
* How many times the attachGui() method has been called. Some behavior like URL or deep-link handling may be
* different the first time a desktop is attached. If the attached count is 1 it means the desktop has been created.
* If attached count is > 1 it means an existing desktop is re-used (probably by a reload with Ctrl + R in the
* browser).
*/
protected int getAttachedCount() {
return m_attachedCount;
}
private void setAttachedGuiCount(int attachedGuiCount) {
m_attachedGuiCount = attachedGuiCount;
propertySupport.setPropertyBool(PROP_GUI_AVAILABLE, m_attachedGuiCount > 0);
}
private void attachGui() {
m_attachedCount++;
setAttachedGuiCount(m_attachedGuiCount + 1);
if (m_attachedGuiCount == 1) {
// this is the first call to attachGui, call extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.guiAttachedDelegate();
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (Exception e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
}
PropertyMap params = getStartupRequestParams();
String geolocationServiceAvailableStr = params.get(IDesktop.PROP_GEOLOCATION_SERVICE_AVAILABLE);
boolean geolocationServiceAvailable = TypeCastUtility.castValue(geolocationServiceAvailableStr, boolean.class);
setGeolocationServiceAvailable(geolocationServiceAvailable);
boolean handleDeepLink = params.getOrDefault(DeepLinkUrlParameter.HANDLE_DEEP_LINK, true);
if (handleDeepLink) {
String deepLinkPath = params.get(DeepLinkUrlParameter.DEEP_LINK);
activateDefaultView(deepLinkPath);
}
}
/**
* Activates the initial, default view of the Desktop. This method checks whether the Scout application has been
* started with a deep-link. If this applies, the deep-link is executed. Otherwise or if the deep-link has caused an
* error execDefaultView is called.
*
* @param deepLinkPath
* The deep-link path which was passed as URL parameter to the Scout application (if available). This
* parameter may be null. See: {@link IDeepLinks}.
*/
protected void activateDefaultView(String deepLinkPath) {
if (safeHandleDeepLink(deepLinkPath)) {
return;
}
if (m_attachedCount <= 1) { // is startup?
try {
m_activatingDefaultView = true;
interceptDefaultView();
}
finally {
m_activatingDefaultView = false;
}
}
}
protected boolean safeHandleDeepLink(String deepLinkPath) {
try {
if (handleDeepLink(deepLinkPath)) {
return true;
}
}
catch (DeepLinkException e) {
LOG.info("Failed to execute deep-link '{}', reason: {}", deepLinkPath, e.getMessage());
showDeepLinkError(e);
}
return false;
}
@Override
public void doLogoAction() {
if (isLogoActionEnabled()) {
interceptLogoAction();
}
}
/**
* Checks whether the given path is a valid deep-link. If that is the case the deep-link is handled. When an error
* occurs while handling the deep-link a {@link DeepLinkException} is thrown, usually this exception is thrown when
* the business-logic failed to find the resource addressed by the deep-link (similar to the 404 HTTP error status).
*
* @param deepLinkPath
* @return
* @throws DeepLinkException
* when deep-link could not be executed
*/
protected boolean handleDeepLink(String deepLinkPath) throws DeepLinkException {
if (hasApplicationModalElement()) {
LOG.debug("Could not handle deep-link because modal element prevents changes on UI state. deepLinkPath={}", deepLinkPath);
return false;
}
if (StringUtility.isNullOrEmpty(deepLinkPath)) {
return false;
}
// Check if the same deep-link is activated
BrowserHistoryEntry entry = getBrowserHistoryEntry();
if (entry != null && deepLinkPath.equals(entry.getDeepLinkPath())) {
return false;
}
boolean handled = false;
IDeepLinks deepLinks = BEANS.get(IDeepLinks.class);
if (deepLinks.canHandleDeepLink(deepLinkPath)) {
try {
handled = deepLinks.handleDeepLink(deepLinkPath);
}
catch (RuntimeException e) {
throw new DeepLinkException(e);
}
}
else {
// non empty deepLinkPath provided but no handler found, throw exception to let user know
throw new DeepLinkException("No deep-link handler found. deepLinkPath={" + deepLinkPath + " }");
}
return handled;
}
protected boolean hasApplicationModalElement() {
if (m_messageBoxStore.containsApplicationModalMessageBoxes()) {
return true;
}
if (m_formStore.containsApplicationModalDialogs()) {
return true;
}
return m_fileChooserStore.containsApplicationModalFileChoosers();
}
/**
* Displays a message box showing a generic message that the deep-link could not be executed.
*/
protected void showDeepLinkError(DeepLinkException e) {
if (e.getCause() instanceof VetoException) {
// permission problems
BEANS.get(ExceptionHandler.class).handle(e.getCause());
}
else {
// other problems
String errorMessage = TEXTS.get("DeepLinkError");
MessageBoxes.createOk().withBody(errorMessage).show();
}
}
private void detachGui() {
setAttachedGuiCount(m_attachedGuiCount - 1);
if (m_attachedGuiCount == 0) {
//this is the last call to detachGui, call extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.guiDetachedDelegate();
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (Exception e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
}
}
public boolean runMenu(Class<? extends IMenu> menuType) {
for (IMenu m : getMenus()) {
if (runMenuRec(m, menuType)) {
return true;
}
}
return false;
}
private boolean runMenuRec(IMenu m, Class<? extends IMenu> menuType) {
if (m.getClass() == menuType) {
if (m.isVisible() && m.isEnabled()) {
m.doAction();
return true;
}
else {
return false;
}
}
// children
for (IMenu c : m.getChildActions()) {
if (runMenuRec(c, menuType)) {
return true;
}
}
return false;
}
@Override
public Collection<Object> getAddOns() {
return Collections.unmodifiableCollection(m_addOns);
}
@Override
public <T> T getAddOn(Class<T> addOnClass) {
return m_addOns.stream()
.filter(a -> addOnClass.isAssignableFrom(a.getClass()))
.map(addOnClass::cast)
.findAny().orElse(null);
}
@Override
public void addAddOn(Object addOn) {
m_addOns.add(addOn);
}
@Override
public void removeAddOn(Object addOn) {
m_addOns.remove(addOn);
}
@Override
public IDesktopUIFacade getUIFacade() {
return m_uiFacade;
}
private boolean isForcedClosing() {
return m_isForcedClosing;
}
private void setForcedClosing(boolean forcedClosing) {
m_isForcedClosing = forcedClosing;
}
@Override
public void setNavigationVisible(boolean visible) {
propertySupport.setProperty(PROP_NAVIGATION_VISIBLE, visible);
}
@Override
public boolean isNavigationVisible() {
return (boolean) propertySupport.getProperty(PROP_NAVIGATION_VISIBLE);
}
@Override
public void setNavigationHandleVisible(boolean visible) {
propertySupport.setProperty(PROP_NAVIGATION_HANDLE_VISIBLE, visible);
}
@Override
public boolean isNavigationHandleVisible() {
return (boolean) propertySupport.getProperty(PROP_NAVIGATION_HANDLE_VISIBLE);
}
@Override
public void setBenchVisible(boolean visible) {
propertySupport.setProperty(PROP_BENCH_VISIBLE, visible);
}
@Override
public boolean isBenchVisible() {
return (boolean) propertySupport.getProperty(PROP_BENCH_VISIBLE);
}
@Override
public BenchLayoutData getBenchLayoutData() {
return (BenchLayoutData) propertySupport.getProperty(PROP_BENCH_LAYOUT_DATA);
}
@Override
public void setBenchLayoutData(BenchLayoutData layoutData) {
propertySupport.setProperty(PROP_BENCH_LAYOUT_DATA, layoutData);
}
@Override
public void setHeaderVisible(boolean visible) {
propertySupport.setProperty(PROP_HEADER_VISIBLE, visible);
}
@Override
public boolean isHeaderVisible() {
return (boolean) propertySupport.getProperty(PROP_HEADER_VISIBLE);
}
protected void setInBackground(boolean inBackground) {
propertySupport.setPropertyBool(PROP_IN_BACKGROUND, inBackground);
m_dataChangeDesktopInForegroundListeners.setBuffering(inBackground || isDataChanging());
}
@Override
public boolean isInBackground() {
return propertySupport.getPropertyBool(PROP_IN_BACKGROUND);
}
@Override
public boolean isLogoActionEnabled() {
return propertySupport.getPropertyBool(PROP_LOGO_ACTION_ENABLED);
}
@Override
public void setLogoActionEnabled(boolean logoActionEnabled) {
propertySupport.setPropertyBool(PROP_LOGO_ACTION_ENABLED, logoActionEnabled);
}
@Override
public void setDense(boolean dense) {
propertySupport.setPropertyBool(PROP_DENSE, dense);
}
@Override
public boolean isDense() {
return propertySupport.getPropertyBool(PROP_DENSE);
}
/**
* local desktop extension that calls local exec methods and returns local contributions in this class itself
*/
private class P_LocalDesktopExtension implements IDesktopExtension {
@Override
public IDesktop getCoreDesktop() {
return AbstractDesktop.this;
}
@Override
public void setCoreDesktop(IDesktop desktop) {
// NOP
}
@Override
public void contributeOutlines(OrderedCollection<IOutline> outlines) {
List<Class<? extends IOutline>> configuredOutlines = getConfiguredOutlines();
if (configuredOutlines != null) {
for (Class<?> element : configuredOutlines) {
try {
IOutline o = (IOutline) element.getConstructor().newInstance();
outlines.addOrdered(o);
}
catch (Exception e) {
BEANS.get(ExceptionHandler.class).handle(new ProcessingException("error creating instance of class '" + element.getName() + "'.", e));
}
}
}
}
@Override
public void contributeActions(Collection<IAction> actions) {
for (Class<? extends IAction> actionClazz : getConfiguredActions()) {
actions.add(ConfigurationUtility.newInnerInstance(AbstractDesktop.this, actionClazz));
}
}
@Override
public ContributionCommand initDelegate() {
interceptInit();
return ContributionCommand.Continue;
}
@Override
public ContributionCommand desktopOpenedDelegate() {
interceptOpened();
return ContributionCommand.Continue;
}
@Override
public ContributionCommand desktopBeforeClosingDelegate() {
interceptBeforeClosing();
return ContributionCommand.Continue;
}
@Override
public ContributionCommand desktopClosingDelegate() {
interceptClosing();
return ContributionCommand.Continue;
}
@Override
public ContributionCommand guiAttachedDelegate() {
interceptGuiAttached();
return ContributionCommand.Continue;
}
@Override
public ContributionCommand guiDetachedDelegate() {
interceptGuiDetached();
return ContributionCommand.Continue;
}
@Override
public ContributionCommand outlineChangedDelegate(IOutline oldOutline, IOutline newOutline) {
interceptOutlineChanged(oldOutline, newOutline);
return ContributionCommand.Continue;
}
@Override
public ContributionCommand formAboutToShowDelegate(IHolder<IForm> formHolder) {
formHolder.setValue(interceptFormAboutToShow(formHolder.getValue()));
return ContributionCommand.Continue;
}
@Override
public ContributionCommand pageSearchFormChangedDelegate(IForm oldForm, IForm newForm) {
interceptPageSearchFormChanged(oldForm, newForm);
return ContributionCommand.Continue;
}
@Override
public ContributionCommand pageDetailFormChangedDelegate(IForm oldForm, IForm newForm) {
interceptPageDetailFormChanged(oldForm, newForm);
return ContributionCommand.Continue;
}
@Override
public ContributionCommand pageDetailTableChangedDelegate(ITable oldTable, ITable newTable) {
interceptPageDetailTableChanged(oldTable, newTable);
return ContributionCommand.Continue;
}
@Override
public ContributionCommand tablePageLoadedDelegate(IPageWithTable<?> tablePage) {
interceptTablePageLoaded(tablePage);
return ContributionCommand.Continue;
}
}
protected class P_UIFacade implements IDesktopUIFacade {
@Override
public void historyEntryActivateFromUI(String deepLinkPath) {
try {
handleDeepLink(deepLinkPath);
}
catch (DeepLinkException e) {
showDeepLinkError(e);
}
}
@Override
public void fireGuiAttached() {
attachGui();
}
@Override
public void fireGuiDetached() {
detachGui();
}
@Override
public void openFromUI() {
setOpenedInternal(true);
//extensions
for (IDesktopExtension ext : getDesktopExtensions()) {
try {
ContributionCommand cc = ext.desktopOpenedDelegate();
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (Exception e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
}
@Override
public void closeFromUI(boolean forcedClosing) {
setForcedClosing(forcedClosing);
// necessary so that no forms can be opened anymore.
if (forcedClosing) {
setOpenedInternal(false);
}
ClientSessionProvider.currentSession().stop();
}
@Override
public void activateForm(IForm form) {
if (form == null) {
// If form is null, outline is in front -> no view of the center area is selected anymore
// It is done here and not in activateForm itself because activateForm is called by hideForm() too,
// but in that case the outline may be still in the background
m_selectedViewTabs.remove(IForm.VIEW_ID_CENTER);
}
AbstractDesktop.this.activateForm(form);
}
@Override
public void setFocusedElementFromUI(IWidget widget) {
setFocusedElement(widget);
}
@Override
public void setNavigationVisibleFromUI(boolean visible) {
setNavigationVisible(visible);
}
@Override
public void setNavigationHandleVisibleFromUI(boolean visible) {
setNavigationHandleVisible(visible);
}
@Override
public void setHeaderVisibleFromUI(boolean visible) {
setHeaderVisible(visible);
}
@Override
public void setBenchVisibleFromUI(boolean visible) {
setBenchVisible(visible);
}
@Override
public void setInBackgroundFromUI(boolean inBackground) {
setInBackground(inBackground);
}
@Override
public void setGeoLocationServiceAvailableFromUI(boolean available) {
setGeolocationServiceAvailable(available);
}
@Override
public void initStartupRequestParamsFromUI() {
initStartupRequestParams();
}
@Override
public void fireGeolocationDetermined(String latitude, String longitude) {
Iterable<ClientCallback<Coordinates>> pendingCallbacks;
synchronized (m_pendingPositionResponses) {
pendingCallbacks = new ArrayList<>(m_pendingPositionResponses);
m_pendingPositionResponses.clear();
}
Coordinates location = new Coordinates(latitude, longitude);
for (ClientCallback<Coordinates> callback : pendingCallbacks) {
callback.done(location);
}
}
@Override
public void fireGeolocationFailed(String errorCode, String errorMessage) {
Iterable<ClientCallback<Coordinates>> pendingCallbacks;
synchronized (m_pendingPositionResponses) {
pendingCallbacks = new ArrayList<>(m_pendingPositionResponses);
m_pendingPositionResponses.clear();
}
setGeolocationServiceAvailable(false);
ProcessingException pe = new ProcessingException("Geolocation failed. ErrorCode: {}; ErrorMessage: {}", errorCode, errorMessage);
for (ClientCallback<Coordinates> callback : pendingCallbacks) {
callback.failed(pe);
}
}
@Override
public void fireLogoAction() {
doLogoAction();
}
@Override
public void removedNotificationFromUI(IDesktopNotification notification) {
AbstractDesktop.this.removeNotificationInternal(notification, false);
}
}
private class P_ActiveOutlineListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent e) {
if (e.getPropertyName().equals(IOutline.PROP_DETAIL_FORM)) {
setPageDetailForm(((IOutline) e.getSource()).getDetailForm());
}
else if (e.getPropertyName().equals(IOutline.PROP_DETAIL_TABLE)) {
setPageDetailTable(((IOutline) e.getSource()).getDetailTable());
}
else if (e.getPropertyName().equals(IOutline.PROP_SEARCH_FORM)) {
setPageSearchForm(((IOutline) e.getSource()).getSearchForm());
}
}
}
/**
* <p>
* Default behavior of the pre-hook is to delegate the before closing call to the each desktop extension. The method
* {@link IDesktopExtension#desktopBeforeClosingDelegate()} of each desktop extension will be called. If at least one
* of the desktop extension's delegate throws a {@link VetoException}, the closing process will be stopped.
* </p>
* <p>
* Additionally, before closing, a dialog is shown to allow the user to save unsaved forms. At this point it is also
* possible to cancel closing.
* </p>
* If the flag {@link AbstractDesktop#m_isForcedClosing} is set to <code>true</code>, closing is always continued,
* without considering desktopExtensions or unsaved forms.
*/
@Override
public boolean doBeforeClosingInternal() {
return isForcedClosing() || (continueClosingInDesktopExtensions() && continueClosingConsideringUnsavedForms(getUnsavedForms(), true));
}
protected boolean continueClosingConsideringUnsavedForms(List<IForm> forms, boolean isStopSession) {
if (forms.isEmpty()) {
return true;
}
try {
UnsavedFormChangesForm f = new UnsavedFormChangesForm(forms, isStopSession);
f.startNew();
f.waitFor();
if (f.getCloseSystemType() == IButton.SYSTEM_TYPE_CANCEL) {
return false;
}
}
catch (RuntimeException | PlatformError e) {
LOG.error("Error closing forms", e);
}
return true;
}
private boolean continueClosingInDesktopExtensions() {
boolean continueClosing = true;
List<IDesktopExtension> extensions = getDesktopExtensions();
if (extensions != null) {
for (IDesktopExtension ext : extensions) {
try {
ContributionCommand cc = ext.desktopBeforeClosingDelegate();
if (cc == ContributionCommand.Stop) {
break;
}
}
catch (VetoException e) { // NOSONAR
continueClosing = false;
}
catch (RuntimeException | PlatformError e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
}
return continueClosing;
}
@Override
public List<IForm> getUnsavedForms() {
return getUnsavedForms(null);
}
/**
* Open dialogs or views that need to be saved within the given {@link Set} of {@link IForm}s, or within all open
* forms when <code>formSet</code> is null.
*/
protected List<IForm> getUnsavedForms(Set<IForm> formSet) {
List<IForm> saveNeededForms = new ArrayList<>();
List<IForm> showedForms = m_formStore.values();
if (formSet != null) {
// identify (by displayParent) and add relevant open forms to the formSet
// formStore guarantees the order of insertion, therefore nested forms should be collected correctly
for (IForm form : showedForms) {
if (formSet.contains(form.getDisplayParent())) {
formSet.add(form);
}
}
}
// last element on the stack is the first that needs to be saved: iterate from end to start
for (int i = showedForms.size() - 1; i >= 0; i--) {
IForm f = showedForms.get(i);
if ((formSet == null || formSet.contains(f)) && f.isAskIfNeedSave() && f.isSaveNeeded()) {
saveNeededForms.add(f);
}
}
return saveNeededForms;
}
@Override
public boolean cancelForms(Set<IForm> formSet, boolean alwaysShowUnsavedChangesForm) {
if (formSet == null || formSet.isEmpty()) {
return true;
}
formSet = formSet.stream().filter(form -> getForms(form)
.stream()
.filter(IForm::isModal)
.findAny()
.map(f -> false)
.orElse(true)).collect(Collectors.toSet());
// if formSet contains only one element simply call doCancel on that form. Using the UnsavedFormChangesForm is not necessary in this case.
if (!alwaysShowUnsavedChangesForm && formSet.size() == 1) {
CollectionUtility.firstElement(formSet).doCancel();
return true;
}
return continueClosingConsideringUnsavedForms(getUnsavedForms(formSet), false) && internalCloseForms(formSet);
}
@Override
public boolean cancelForms(Set<IForm> formSet) {
return cancelForms(formSet, false);
}
@Override
public void closeForms(Set<IForm> formSet) {
if (formSet == null || formSet.isEmpty()) {
return;
}
// Extend the form set with all depending forms (display parent hierarchy)
getUnsavedForms(formSet);
// Close the given forms including the depending forms
internalCloseForms(formSet);
}
@Override
public String getTheme() {
return propertySupport.getPropertyString(PROP_THEME);
}
@Override
public void setTheme(String theme) {
propertySupport.setProperty(PROP_THEME, theme);
}
@Override
public NativeNotificationDefaults getNativeNotificationDefaults() {
return (NativeNotificationDefaults) propertySupport.getProperty(PROP_NATIVE_NOTIFICATION_DEFAULTS);
}
@Override
public void setNativeNotificationDefaults(NativeNotificationDefaults nativeNotificationDefaults) {
propertySupport.setProperty(PROP_NATIVE_NOTIFICATION_DEFAULTS, nativeNotificationDefaults);
}
/**
* The extension delegating to the local methods. This Extension is always at the end of the chain and will not call
* any further chain elements.
*/
protected static class LocalDesktopExtension<DESKTOP extends AbstractDesktop> extends AbstractExtension<DESKTOP> implements org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<DESKTOP> {
public LocalDesktopExtension(DESKTOP desktop) {
super(desktop);
}
@Override
public void execInit(DesktopInitChain chain) {
getOwner().execInit();
}
@Override
public void execOpened(DesktopOpenedChain chain) {
getOwner().execOpened();
}
@Override
public void execBeforeClosing(DesktopBeforeClosingChain chain) {
getOwner().execBeforeClosing();
}
@Override
public void execPageDetailFormChanged(DesktopPageDetailFormChangedChain chain, IForm oldForm, IForm newForm) {
getOwner().execPageDetailFormChanged(oldForm, newForm);
}
@Override
public void execTablePageLoaded(DesktopTablePageLoadedChain chain, IPageWithTable<?> tablePage) {
getOwner().execTablePageLoaded(tablePage);
}
@Override
public void execOutlineChanged(DesktopOutlineChangedChain chain, IOutline oldOutline, IOutline newOutline) {
getOwner().execOutlineChanged(oldOutline, newOutline);
}
@Override
public IForm execFormAboutToShow(DesktopFormAboutToShowChain chain, IForm form) {
return getOwner().execFormAboutToShow(form);
}
@Override
public void execClosing(DesktopClosingChain chain) {
getOwner().execClosing();
}
@Override
public void execPageSearchFormChanged(DesktopPageSearchFormChangedChain chain, IForm oldForm, IForm newForm) {
getOwner().execPageSearchFormChanged(oldForm, newForm);
}
@Override
public void execPageDetailTableChanged(DesktopPageDetailTableChangedChain chain, ITable oldTable, ITable newTable) {
getOwner().execPageDetailTableChanged(oldTable, newTable);
}
@Override
public void execGuiAttached(DesktopGuiAttachedChain chain) {
getOwner().execGuiAttached();
}
@Override
public void execGuiDetached(DesktopGuiDetachedChain chain) {
getOwner().execGuiDetached();
}
@Override
public void execDefaultView(DesktopDefaultViewChain chain) {
getOwner().execDefaultView();
}
@Override
public void execLogoAction(DesktopLogoActionChain chain) {
getOwner().execLogoAction();
}
}
protected final void interceptOpened() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopOpenedChain chain = new DesktopOpenedChain(extensions);
chain.execOpened();
}
protected final void interceptBeforeClosing() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopBeforeClosingChain chain = new DesktopBeforeClosingChain(extensions);
chain.execBeforeClosing();
}
protected final void interceptPageDetailFormChanged(IForm oldForm, IForm newForm) {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopPageDetailFormChangedChain chain = new DesktopPageDetailFormChangedChain(extensions);
chain.execPageDetailFormChanged(oldForm, newForm);
}
protected final void interceptTablePageLoaded(IPageWithTable<?> tablePage) {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopTablePageLoadedChain chain = new DesktopTablePageLoadedChain(extensions);
chain.execTablePageLoaded(tablePage);
}
protected final void interceptOutlineChanged(IOutline oldOutline, IOutline newOutline) {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopOutlineChangedChain chain = new DesktopOutlineChangedChain(extensions);
chain.execOutlineChanged(oldOutline, newOutline);
}
protected final IForm interceptFormAboutToShow(IForm form) {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopFormAboutToShowChain chain = new DesktopFormAboutToShowChain(extensions);
return chain.execFormAboutToShow(form);
}
protected final void interceptClosing() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopClosingChain chain = new DesktopClosingChain(extensions);
chain.execClosing();
}
protected final void interceptPageSearchFormChanged(IForm oldForm, IForm newForm) {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopPageSearchFormChangedChain chain = new DesktopPageSearchFormChangedChain(extensions);
chain.execPageSearchFormChanged(oldForm, newForm);
}
protected final void interceptPageDetailTableChanged(ITable oldTable, ITable newTable) {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopPageDetailTableChangedChain chain = new DesktopPageDetailTableChangedChain(extensions);
chain.execPageDetailTableChanged(oldTable, newTable);
}
protected final void interceptGuiAttached() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopGuiAttachedChain chain = new DesktopGuiAttachedChain(extensions);
chain.execGuiAttached();
}
protected final void interceptGuiDetached() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopGuiDetachedChain chain = new DesktopGuiDetachedChain(extensions);
chain.execGuiDetached();
}
protected final void interceptDefaultView() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopDefaultViewChain chain = new DesktopDefaultViewChain(extensions);
chain.execDefaultView();
}
protected final void interceptLogoAction() {
List<? extends org.eclipse.scout.rt.client.extension.ui.desktop.IDesktopExtension<? extends AbstractDesktop>> extensions = getAllExtensions();
DesktopLogoActionChain chain = new DesktopLogoActionChain(extensions);
chain.execLogoAction();
}
protected class P_LocalPropertyChangeListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName() == PROP_DISPLAY_STYLE) {
initDisplayStyle((String) evt.getNewValue());
}
}
}
}