From 7fa402d36e0e194424589f4d7efeae5610d2c6eb Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Tue, 10 Apr 2018 13:44:31 +0200 Subject: [PATCH] CMS wizard and error feedback --- .../argeo/cms/e4/rap/AbstractRapE4App.java | 82 +++++--- .../cms/e4/rap/CmsE4EntryPointFactory.java | 44 +++++ .../org/argeo/cms/ui/dialogs/CmsFeedback.java | 91 +++++++++ .../argeo/cms/ui/dialogs/CmsWizardDialog.java | 177 ++++++++++++++++++ .../src/org/argeo/eclipse/ui/Selected.java | 21 +++ .../eclipse/ui/dialogs/FeedbackDialog.java | 3 +- .../eclipse/ui/dialogs/LightweightDialog.java | 148 ++++++++++++--- 7 files changed, 510 insertions(+), 56 deletions(-) create mode 100644 org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4EntryPointFactory.java create mode 100644 org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsFeedback.java create mode 100644 org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsWizardDialog.java create mode 100644 org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/Selected.java diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/AbstractRapE4App.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/AbstractRapE4App.java index c70e381c3..68415f31a 100644 --- a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/AbstractRapE4App.java +++ b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/AbstractRapE4App.java @@ -1,22 +1,25 @@ package org.argeo.cms.e4.rap; -import java.security.PrivilegedAction; import java.util.HashMap; import java.util.Map; import javax.security.auth.Subject; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.cms.ui.dialogs.CmsFeedback; import org.eclipse.rap.e4.E4ApplicationConfig; -import org.eclipse.rap.e4.E4EntryPointFactory; import org.eclipse.rap.rwt.application.Application; import org.eclipse.rap.rwt.application.Application.OperationMode; import org.eclipse.rap.rwt.application.ApplicationConfiguration; -import org.eclipse.rap.rwt.application.EntryPoint; +import org.eclipse.rap.rwt.application.ExceptionHandler; import org.eclipse.rap.rwt.client.WebClient; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; public abstract class AbstractRapE4App implements ApplicationConfiguration { + private final static Log log = LogFactory.getLog(AbstractRapE4App.class); + private final BundleContext bc = FrameworkUtil.getBundle(AbstractRapE4App.class).getBundleContext(); private String pageTitle; @@ -24,41 +27,58 @@ public abstract class AbstractRapE4App implements ApplicationConfiguration { private String path; public void configure(Application application) { + application.setExceptionHandler(new ExceptionHandler() { + + @Override + public void handleException(Throwable throwable) { + CmsFeedback.show("Unexpected RWT exception", throwable); + // log.error("Unexpected RWT exception", throwable); + + } + }); + String lifeCycleUri = "bundleclass://" + bc.getBundle().getSymbolicName() + "/" + CmsLoginLifecycle.class.getName(); Map properties = new HashMap(); properties.put(WebClient.PAGE_TITLE, pageTitle); E4ApplicationConfig config = new E4ApplicationConfig(e4Xmi, lifeCycleUri, null, false, true, true); - config.isClearPersistedState(); - E4EntryPointFactory entryPointFactory = new E4EntryPointFactory(config) { - - @Override - public EntryPoint create() { - Subject subject = new Subject(); - EntryPoint ep = createEntryPoint(); - EntryPoint authEp = new EntryPoint() { - - @Override - public int createUI() { - return Subject.doAs(subject, new PrivilegedAction() { - - @Override - public Integer run() { - return ep.createUI(); - } - - }); - } - }; - return authEp; - } - - protected EntryPoint createEntryPoint() { - return super.create(); - } + Subject subject = new Subject(); + addEntryPoint(application, subject, config, properties); + // config.isClearPersistedState(); + // E4EntryPointFactory entryPointFactory = new E4EntryPointFactory(config) { + // + // @Override + // public EntryPoint create() { + // Subject subject = new Subject(); + // EntryPoint ep = createEntryPoint(); + // EntryPoint authEp = new EntryPoint() { + // + // @Override + // public int createUI() { + // return Subject.doAs(subject, new PrivilegedAction() { + // + // @Override + // public Integer run() { + // return ep.createUI(); + // } + // + // }); + // } + // }; + // return authEp; + // } + // + // protected EntryPoint createEntryPoint() { + // return super.create(); + // } + // + // }; + } - }; + protected void addEntryPoint(Application application, Subject subject, E4ApplicationConfig config, + Map properties) { + CmsE4EntryPointFactory entryPointFactory = new CmsE4EntryPointFactory(subject, config); application.addEntryPoint(path, entryPointFactory, properties); application.setOperationMode(OperationMode.SWT_COMPATIBILITY); } diff --git a/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4EntryPointFactory.java b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4EntryPointFactory.java new file mode 100644 index 000000000..6b974770e --- /dev/null +++ b/org.argeo.cms.e4.rap/src/org/argeo/cms/e4/rap/CmsE4EntryPointFactory.java @@ -0,0 +1,44 @@ +package org.argeo.cms.e4.rap; + +import java.security.PrivilegedAction; + +import javax.security.auth.Subject; + +import org.eclipse.rap.e4.E4ApplicationConfig; +import org.eclipse.rap.e4.E4EntryPointFactory; +import org.eclipse.rap.rwt.application.EntryPoint; + +public class CmsE4EntryPointFactory extends E4EntryPointFactory { + private Subject subject; + + public CmsE4EntryPointFactory(Subject subject, E4ApplicationConfig config) { + super(config); + this.subject = subject; + } + + @Override + public EntryPoint create() { + // Subject subject = new Subject(); + EntryPoint ep = createEntryPoint(); + EntryPoint authEp = new EntryPoint() { + + @Override + public int createUI() { + return Subject.doAs(subject, new PrivilegedAction() { + + @Override + public Integer run() { + return ep.createUI(); + } + + }); + } + }; + return authEp; + } + + protected EntryPoint createEntryPoint() { + return super.create(); + } + +} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsFeedback.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsFeedback.java new file mode 100644 index 000000000..61cce4ed3 --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsFeedback.java @@ -0,0 +1,91 @@ +package org.argeo.cms.ui.dialogs; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.eclipse.ui.Selected; +import org.argeo.eclipse.ui.dialogs.LightweightDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +public class CmsFeedback extends LightweightDialog { + private final static Log log = LogFactory.getLog(CmsFeedback.class); + + private String message; + private Throwable exception; + + public CmsFeedback(Shell parentShell, String message, Throwable e) { + super(parentShell); + this.message = message; + this.exception = e; + log.error(message, e); + } + + public static void show(String message, Throwable e) { + // rethrow ThreaDeath in order to make sure that RAP will properly clean + // up the UI thread + if (e instanceof ThreadDeath) + throw (ThreadDeath) e; + + new CmsFeedback(getDisplay().getActiveShell(), message, e).open(); + } + + public static void show(String message) { + new CmsFeedback(getDisplay().getActiveShell(), message, null).open(); + } + + /** Tries to find a display */ + private static Display getDisplay() { + try { + Display display = Display.getCurrent(); + if (display != null) + return display; + else + return Display.getDefault(); + } catch (Exception e) { + return Display.getCurrent(); + } + } + + protected Control createDialogArea(Composite parent) { + parent.setLayout(new GridLayout(2, false)); + + Label messageLbl = new Label(parent, SWT.WRAP); + if (message != null) + messageLbl.setText(message); + else if (exception != null) + messageLbl.setText(exception.getLocalizedMessage()); + + Button close = new Button(parent, SWT.FLAT); + close.setText("Close"); + close.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false)); + close.addSelectionListener((Selected) (e) -> closeShell(OK)); + + // Composite composite = new Composite(dialogarea, SWT.NONE); + // composite.setLayout(new GridLayout(2, false)); + // composite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + if (exception != null) { + Text stack = new Text(parent, SWT.MULTI | SWT.LEAD | SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL); + stack.setEditable(false); + stack.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); + StringWriter sw = new StringWriter(); + exception.printStackTrace(new PrintWriter(sw)); + stack.setText(sw.toString()); + } + + // parent.pack(); + return messageLbl; + } + +} diff --git a/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsWizardDialog.java b/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsWizardDialog.java new file mode 100644 index 000000000..cafde7e27 --- /dev/null +++ b/org.argeo.cms.ui/src/org/argeo/cms/ui/dialogs/CmsWizardDialog.java @@ -0,0 +1,177 @@ +package org.argeo.cms.ui.dialogs; + +import java.lang.reflect.InvocationTargetException; + +import org.argeo.cms.CmsException; +import org.argeo.cms.util.CmsUtils; +import org.argeo.eclipse.ui.EclipseUiUtils; +import org.argeo.eclipse.ui.Selected; +import org.argeo.eclipse.ui.dialogs.LightweightDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.wizard.IWizard; +import org.eclipse.jface.wizard.IWizardContainer2; +import org.eclipse.jface.wizard.IWizardPage; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +public class CmsWizardDialog extends LightweightDialog implements IWizardContainer2 { + private static final long serialVersionUID = -2123153353654812154L; + + private IWizard wizard; + private IWizardPage currentPage; + + private Label titleBar; + private Label message; + private Composite body; + private Composite buttons; + private Button back; + private Button next; + private Button finish; + + public CmsWizardDialog(Shell parentShell, IWizard wizard) { + super(parentShell); + this.wizard = wizard; + wizard.setContainer(this); + // create the pages + wizard.addPages(); + currentPage = wizard.getStartingPage(); + if (currentPage == null) + throw new CmsException("At least one wizard page is required"); + } + + @Override + protected Control createDialogArea(Composite parent) { + updateWindowTitle(); + + Composite messageArea = new Composite(parent, SWT.NONE); + messageArea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false)); + { + messageArea.setLayout(CmsUtils.noSpaceGridLayout(new GridLayout(2, false))); + titleBar = new Label(messageArea, SWT.WRAP); + titleBar.setFont(EclipseUiUtils.getBoldFont(parent)); + titleBar.setLayoutData(new GridData(SWT.BEGINNING, SWT.FILL, true, false)); + updateTitleBar(); + Button cancelButton = new Button(messageArea, SWT.FLAT); + cancelButton.setText("Cancel"); + cancelButton.setLayoutData(new GridData(SWT.END, SWT.TOP, false, false, 1, 3)); + cancelButton.addSelectionListener((Selected) (e) -> closeShell(CANCEL)); + message = new Label(messageArea, SWT.WRAP); + message.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, 1, 2)); + updateMessage(); + } + + body = new Composite(parent, SWT.BORDER); + body.setLayout(CmsUtils.noSpaceGridLayout()); + body.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + showPage(currentPage); + + buttons = new Composite(parent, SWT.NONE); + buttons.setLayoutData(new GridData(SWT.END, SWT.FILL, true, false)); + { + boolean singlePage = wizard.getPageCount() == 1; + GridLayout layout = new GridLayout(singlePage ? 1 : 3, true); + layout.marginWidth = 0; + layout.marginHeight = 0; + buttons.setLayout(layout); + // TODO revert order for right-to-left languages + + if (!singlePage) { + back = new Button(buttons, SWT.PUSH); + back.setText("Back"); + back.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + back.addSelectionListener((Selected) (e) -> backPressed()); + + next = new Button(buttons, SWT.PUSH); + next.setText("Next"); + next.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + next.addSelectionListener((Selected) (e) -> nextPressed()); + } + finish = new Button(buttons, SWT.PUSH); + finish.setText("Finish"); + finish.setLayoutData(new GridData(SWT.FILL, SWT.FILL, false, false)); + finish.addSelectionListener((Selected) (e) -> finishPressed()); + + updateButtons(); + } + return body; + } + + @Override + public IWizardPage getCurrentPage() { + return currentPage; + } + + @Override + public Shell getShell() { + return getForegoundShell(); + } + + @Override + public void showPage(IWizardPage page) { + // clear + for (Control c : body.getChildren()) + c.dispose(); + page.createControl(body); + currentPage = page; + } + + @Override + public void updateButtons() { + if (back != null) + back.setEnabled(wizard.getPreviousPage(currentPage) != null); + if (next != null) + next.setEnabled(wizard.getNextPage(currentPage) != null && currentPage.canFlipToNextPage()); + finish.setEnabled(wizard.canFinish()); + } + + @Override + public void updateMessage() { + message.setText(currentPage.getMessage()); + } + + @Override + public void updateTitleBar() { + titleBar.setText(currentPage.getTitle()); + } + + @Override + public void updateWindowTitle() { + setTitle(wizard.getWindowTitle()); + } + + @Override + public void run(boolean fork, boolean cancelable, IRunnableWithProgress runnable) + throws InvocationTargetException, InterruptedException { + runnable.run(null); + } + + @Override + public void updateSize() { + // TODO pack? + } + + protected boolean onCancel() { + return wizard.performCancel(); + } + + protected void nextPressed() { + IWizardPage page = wizard.getNextPage(currentPage); + showPage(page); + } + + protected void backPressed() { + IWizardPage page = wizard.getPreviousPage(currentPage); + showPage(page); + } + + protected void finishPressed() { + if (wizard.performFinish()) + closeShell(OK); + } +} diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/Selected.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/Selected.java new file mode 100644 index 000000000..4e95c8cb8 --- /dev/null +++ b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/Selected.java @@ -0,0 +1,21 @@ +package org.argeo.eclipse.ui; + +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; + +/** + * {@link SelectionListener} as a functional interface in order to use lambda + * expression in UI code. + * {@link SelectionListener#widgetDefaultSelected(SelectionEvent)} does nothing + * by default. + */ +@FunctionalInterface +public interface Selected extends SelectionListener { + @Override + public void widgetSelected(SelectionEvent e); + + default public void widgetDefaultSelected(SelectionEvent e) { + // does nothing + } + +} diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/FeedbackDialog.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/FeedbackDialog.java index 3b81f6d55..1fd4340ed 100644 --- a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/FeedbackDialog.java +++ b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/FeedbackDialog.java @@ -78,7 +78,7 @@ public class FeedbackDialog extends LightweightDialog { log.error(message, e); } - public void open() { + public int open() { if (shell != null) throw new EclipseUiException("There is already a shell"); shell = new Shell(getDisplay(), SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP); @@ -105,6 +105,7 @@ public class FeedbackDialog extends LightweightDialog { }); shell.open(); + return OK; } protected void closeShell() { diff --git a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/LightweightDialog.java b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/LightweightDialog.java index 33a0d2781..e816ec718 100644 --- a/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/LightweightDialog.java +++ b/org.argeo.eclipse.ui/src/org/argeo/eclipse/ui/dialogs/LightweightDialog.java @@ -32,11 +32,19 @@ import org.eclipse.swt.widgets.Shell; /** Generic lightweight dialog, not based on JFace. */ public class LightweightDialog { - // private final static Log log = LogFactory.getLog(LightweightDialog.class); + // must be the same value as org.eclipse.jface.window.Window#OK + public final static int OK = 0; + // must be the same value as org.eclipse.jface.window.Window#CANCEL + public final static int CANCEL = 1; private Shell parentShell; private Shell backgroundShell; - private Shell shell; + private Shell foregoundShell; + + private Integer returnCode = null; + private boolean block = true; + + private String title; /** Tries to find a display */ private static Display getDisplay() { @@ -55,38 +63,47 @@ public class LightweightDialog { this.parentShell = parentShell; } - public void open() { - if (shell != null) + public int open() { + if (foregoundShell != null) throw new EclipseUiException("There is already a shell"); - backgroundShell = new Shell(parentShell, SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP); - backgroundShell.setMaximized(true); + backgroundShell = new Shell(parentShell, SWT.DIALOG_TRIM | SWT.ON_TOP); + backgroundShell.setFullScreen(true); + // backgroundShell.setMaximized(true); backgroundShell.setAlpha(128); backgroundShell.setBackground(getDisplay().getSystemColor(SWT.COLOR_BLACK)); backgroundShell.open(); - shell = new Shell(backgroundShell, SWT.NO_TRIM | SWT.BORDER | SWT.ON_TOP); - shell.setLayout(new GridLayout()); - // shell.setText("Error"); - shell.setSize(getInitialSize()); - createDialogArea(shell); + foregoundShell = new Shell(backgroundShell, SWT.NO_TRIM | SWT.ON_TOP); + if (title != null) + setTitle(title); + foregoundShell.setLayout(new GridLayout()); + foregoundShell.setSize(getInitialSize()); + createDialogArea(foregoundShell); // shell.pack(); // shell.layout(); Rectangle shellBounds = Display.getCurrent().getBounds();// RAP - Point dialogSize = shell.getSize(); + Point dialogSize = foregoundShell.getSize(); int x = shellBounds.x + (shellBounds.width - dialogSize.x) / 2; int y = shellBounds.y + (shellBounds.height - dialogSize.y) / 2; - shell.setLocation(x, y); + foregoundShell.setLocation(x, y); - shell.addShellListener(new ShellAdapter() { + foregoundShell.addShellListener(new ShellAdapter() { private static final long serialVersionUID = -2701270481953688763L; @Override public void shellDeactivated(ShellEvent e) { - closeShell(); + if (returnCode == null)// not yet closed + closeShell(CANCEL); + } + + @Override + public void shellClosed(ShellEvent e) { + notifyClose(); } + }); - shell.open(); + foregoundShell.open(); // after the foreground shell has been opened backgroundShell.addFocusListener(new FocusListener() { private static final long serialVersionUID = 3137408447474661070L; @@ -97,19 +114,47 @@ public class LightweightDialog { @Override public void focusGained(FocusEvent event) { - closeShell(); + if (returnCode == null)// not yet closed + closeShell(CANCEL); } }); + + if (block) { + runEventLoop(foregoundShell); + } + if (returnCode == null) + returnCode = OK; + return returnCode; } - protected void closeShell() { - if (shell != null) { - shell.close(); - shell.dispose(); - shell = null; + // public synchronized int openAndWait() { + // open(); + // while (returnCode == null) + // try { + // wait(100); + // } catch (InterruptedException e) { + // // silent + // } + // return returnCode; + // } + + private synchronized void notifyClose() { + if (returnCode == null) + returnCode = CANCEL; + notifyAll(); + } + + protected void closeShell(int returnCode) { + this.returnCode = returnCode; + if (CANCEL == returnCode) + onCancel(); + if (foregoundShell != null && !foregoundShell.isDisposed()) { + foregoundShell.close(); + foregoundShell.dispose(); + foregoundShell = null; } - if (backgroundShell != null) { + if (backgroundShell != null && !backgroundShell.isDisposed()) { backgroundShell.close(); backgroundShell.dispose(); } @@ -119,7 +164,7 @@ public class LightweightDialog { // if (exception != null) // return new Point(800, 600); // else - return new Point(400, 400); + return new Point(600, 400); } protected Control createDialogArea(Composite parent) { @@ -128,4 +173,59 @@ public class LightweightDialog { dialogarea.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); return dialogarea; } + + protected Shell getBackgroundShell() { + return backgroundShell; + } + + protected Shell getForegoundShell() { + return foregoundShell; + } + + public void setBlockOnOpen(boolean shouldBlock) { + block = shouldBlock; + } + + private void runEventLoop(Shell loopShell) { + Display display; + if (foregoundShell == null) { + display = Display.getCurrent(); + } else { + display = loopShell.getDisplay(); + } + + while (loopShell != null && !loopShell.isDisposed()) { + try { + if (!display.readAndDispatch()) { + display.sleep(); + } + } catch (Throwable e) { + handleException(e); + } + } + if (!display.isDisposed()) + display.update(); + } + + protected void handleException(Throwable t) { + if (t instanceof ThreadDeath) { + // Don't catch ThreadDeath as this is a normal occurrence when + // the thread dies + throw (ThreadDeath) t; + } + // Try to keep running. + t.printStackTrace(); + } + + /** @return false, if the dialog should not be closed. */ + protected boolean onCancel() { + return true; + } + + public void setTitle(String title) { + this.title = title; + if (getForegoundShell() != null) + getForegoundShell().setText(title); + } + } \ No newline at end of file -- 2.30.2