fix docu and code cleanup

This commit is contained in:
anhefti 2020-03-05 15:25:05 +01:00
parent b811bb5f29
commit df884bd8d2
4 changed files with 316 additions and 298 deletions

View file

@ -11,3 +11,7 @@ TODO
Exam Configuration
-------------------
TODO
Configuration Templates
------------------------

View file

@ -82,13 +82,35 @@ After successful login, one will see the main graphical user interface of the SE
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/overview/overview.png
The main content usually is a list or a form.
In the header above on the right hand, we see the username of the currently logged in user and an action button the sign out and go back to the login page.
Overview
^^^^^^^^
The main content usually consist of a list or a form.
Lists
^^^^^^
A list shows all the objects of a particular activity in a table page. If the list contains as for one page, a page navigation is shown at the bottom of the list with the information of the current page and the number of pages along with a page navigation that can be used to navigate forward and backward thought the list pages.
Almost all lists have the ability to filter the content by certain column filter that are right above the corresponding columns. To filter a list one can use the column filter input to narrow down a specific collection of content. Accordingly to the value type of the column, there are different types of filter:
- Selection, to select one instance of a defined collection of values (drop-down).
- Text input, to write some text that a value must contain.
- Date selection, To select a from-date from a date-picker. A date selection can also have an additional time selection within separate input field
- Date range selection, To select a from- and a to-date within different inputs and a date-picker. A date range selection can also have an additional time range selection within separate input fields
.. image:: images/overview/list.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/overview/list.png
A list can also be sorted by a column by clicking in the column header and the order of sorting can be changed by clicking again on the same column header. Depending on the column type, not all columns has the sort functionality.
Most columns have a short tool-tip description that pops up while the mouse pointer stays over the column header for a moment.
Forms
^^^^^^
.. image:: images/overview/form_readonly.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/overview/form_readonly.png
.. image:: images/overview/form_edit.png
:align: center
:target: https://raw.githubusercontent.com/SafeExamBrowser/seb-server/master/docs/images/overview/form_edit.png

View file

@ -311,19 +311,18 @@ public class SebClientConfigForm implements TemplateComposer {
);
formHandle.getForm().getFieldInput(SebClientConfig.ATTR_FALLBACK)
.addListener(SWT.Selection, event -> {
formHandle.process(
FALLBACK_ATTRIBUTES::contains,
ffa -> {
boolean selected = ((Button) event.widget).getSelection();
ffa.setVisible(selected);
if (!selected && ffa.hasError()) {
ffa.resetError();
ffa.setStringValue(StringUtils.EMPTY);
}
.addListener(SWT.Selection, event -> formHandle.process(
FALLBACK_ATTRIBUTES::contains,
ffa -> {
boolean selected = ((Button) event.widget).getSelection();
ffa.setVisible(selected);
if (!selected && ffa.hasError()) {
ffa.resetError();
ffa.setStringValue(StringUtils.EMPTY);
}
);
});
}
));
final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class);
this.pageService.pageActionBuilder(formContext.clearEntityKeys())

View file

@ -1,283 +1,276 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.content.action;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.template.ImageCell;
import org.eclipse.rap.rwt.template.Template;
import org.eclipse.rap.rwt.template.TextCell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.RGBA;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEventListener;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEventListener;
import ch.ethz.seb.sebserver.gui.service.page.event.PageEventListener;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
public class ActionPane implements TemplateComposer {
private static final String ACTION_EVENT_CALL_KEY = "ACTION_EVENT_CALL";
private static final LocTextKey TITLE_KEY = new LocTextKey("sebserver.actionpane.title");
private final PageService pageService;
private final WidgetFactory widgetFactory;
private final Map<String, Tree> actionTrees = new HashMap<>();
protected ActionPane(final PageService pageService) {
this.pageService = pageService;
this.widgetFactory = pageService.getWidgetFactory();
}
@Override
public void compose(final PageContext pageContext) {
final Label label = this.widgetFactory.labelLocalized(
pageContext.getParent(),
CustomVariant.TEXT_H2,
TITLE_KEY);
final GridData titleLayout = new GridData(SWT.FILL, SWT.TOP, true, false);
titleLayout.verticalIndent = 10;
titleLayout.horizontalIndent = 10;
if (StringUtils.isBlank(label.getText())) {
titleLayout.heightHint = 0;
}
label.setLayoutData(titleLayout);
label.setData(
PageEventListener.LISTENER_ATTRIBUTE_KEY,
new ActionPublishEventListener() {
@Override
public void notify(final ActionPublishEvent event) {
final Composite parent = pageContext.getParent();
final Tree treeForGroup = getTreeForGroup(parent, event.action.definition);
final TreeItem actionItem = ActionPane.this.widgetFactory.treeItemLocalized(
treeForGroup,
event.action.definition.title);
final Image image = event.active
? event.action.definition.icon.getImage(parent.getDisplay())
: event.action.definition.icon.getGreyedImage(parent.getDisplay());
if (!event.active) {
actionItem.setForeground(new Color(parent.getDisplay(), new RGBA(150, 150, 150, 50)));
}
actionItem.setImage(image);
actionItem.setData(ACTION_EVENT_CALL_KEY, event.action);
parent.layout();
}
});
final Composite composite = new Composite(pageContext.getParent(), SWT.NONE);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false);
final GridLayout gridLayout = new GridLayout();
gridLayout.horizontalSpacing = 0;
gridData.heightHint = 0;
composite.setLayoutData(gridData);
composite.setLayout(gridLayout);
composite.setData(
PageEventListener.LISTENER_ATTRIBUTE_KEY,
new ActionActivationEventListener() {
@Override
public void notify(final ActionActivationEvent event) {
final Composite parent = pageContext.getParent();
for (final ActionDefinition ad : event.actions) {
final TreeItem actionItem = findAction(parent, ad);
if (actionItem == null) {
continue;
}
final Image image = event.activation
? ad.icon.getImage(parent.getDisplay())
: ad.icon.getGreyedImage(parent.getDisplay());
actionItem.setImage(image);
if (event.activation) {
actionItem.setForeground(null);
} else {
actionItem.setForeground(new Color(parent.getDisplay(), new RGBA(150, 150, 150, 50)));
ActionPane.this.pageService.getPolyglotPageService().injectI18n(actionItem, ad.title);
}
}
if (event.decoration != null) {
final TreeItem actionItemToDecorate = findAction(parent, event.decoration._1);
if (actionItemToDecorate != null && event.decoration._2 != null) {
actionItemToDecorate.setImage(0,
event.decoration._2.icon.getImage(parent.getDisplay()));
ActionPane.this.pageService.getPolyglotPageService().injectI18n(
actionItemToDecorate,
event.decoration._2.title);
}
}
}
});
}
private TreeItem findAction(final Composite parent, final ActionDefinition actionDefinition) {
final Tree treeForGroup = getTreeForGroup(parent, actionDefinition);
if (treeForGroup == null) {
return null;
}
for (int i = 0; i < treeForGroup.getItemCount(); i++) {
final TreeItem item = treeForGroup.getItem(i);
if (item == null) {
continue;
}
final PageAction action = (PageAction) item.getData(ACTION_EVENT_CALL_KEY);
if (action == null) {
continue;
}
if (action.definition == actionDefinition) {
return item;
}
}
return null;
}
private Tree getTreeForGroup(final Composite parent, final ActionDefinition actionDefinition) {
clearDisposedTrees();
final ActionCategory category = actionDefinition.category;
if (!this.actionTrees.containsKey(category.name())) {
final Tree actionTree = createActionTree(parent, actionDefinition.category);
this.actionTrees.put(category.name(), actionTree);
}
return this.actionTrees.get(category.name());
}
private Tree createActionTree(final Composite parent, final ActionCategory category) {
final Composite composite = new Composite(parent, SWT.NONE);
final GridData layout = new GridData(SWT.FILL, SWT.TOP, true, false);
composite.setLayoutData(layout);
final GridLayout gridLayout = new GridLayout();
gridLayout.marginHeight = 0;
composite.setLayout(gridLayout);
composite.setData(RWT.CUSTOM_VARIANT, "actionPane");
composite.setData("CATEGORY", category);
final Control[] children = parent.getChildren();
for (final Control child : children) {
final ActionCategory c = (ActionCategory) child.getData("CATEGORY");
if (c != null && c.slotPosition > category.slotPosition) {
composite.moveAbove(child);
break;
}
}
// title
if (this.pageService.getI18nSupport().hasText(category.title)) {
final Label actionsTitle = this.widgetFactory.labelLocalized(
composite,
CustomVariant.TEXT_H3,
category.title);
final GridData titleLayout = new GridData(SWT.FILL, SWT.TOP, true, false);
actionsTitle.setLayoutData(titleLayout);
}
// action tree
final Tree actions = this.widgetFactory.treeLocalized(
composite,
SWT.SINGLE | SWT.FULL_SELECTION);
actions.setData(RWT.CUSTOM_VARIANT, "actions");
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false);
actions.setLayoutData(gridData);
final Template template = new Template();
final ImageCell imageCell = new ImageCell(template);
imageCell.setLeft(0, 0)
.setWidth(40)
.setTop(0)
.setBottom(0, 0)
.setHorizontalAlignment(SWT.LEFT)
.setBackground(null);
imageCell.setBindingIndex(0);
final TextCell textCell = new TextCell(template);
textCell.setLeft(0, 30)
.setWidth(150)
.setTop(7)
.setBottom(0, 0)
.setHorizontalAlignment(SWT.LEFT);
textCell.setBindingIndex(0);
actions.setData(RWT.ROW_TEMPLATE, template);
actions.addListener(SWT.Selection, event -> {
final TreeItem treeItem = (TreeItem) event.item;
final PageAction action = (PageAction) treeItem.getData(ACTION_EVENT_CALL_KEY);
this.pageService.executePageAction(action);
if (!treeItem.isDisposed()) {
treeItem.getParent().deselectAll();
final PageAction switchAction = action.getSwitchAction();
if (switchAction != null) {
final PolyglotPageService polyglotPageService = this.pageService.getPolyglotPageService();
polyglotPageService.injectI18n(treeItem, switchAction.definition.title);
treeItem.setImage(switchAction.definition.icon.getImage(treeItem.getDisplay()));
treeItem.setData(ACTION_EVENT_CALL_KEY, switchAction);
}
}
});
return actions;
}
private void clearDisposedTrees() {
new ArrayList<>(this.actionTrees.entrySet())
.stream()
.forEach(entry -> {
final Control c = entry.getValue();
// of tree is already disposed.. remove it
if (c.isDisposed()) {
this.actionTrees.remove(entry.getKey());
}
// check access from current thread
try {
c.getBounds();
} catch (final Exception e) {
this.actionTrees.remove(entry.getKey());
}
});
}
}
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.content.action;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.template.ImageCell;
import org.eclipse.rap.rwt.template.Template;
import org.eclipse.rap.rwt.template.TextCell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.RGBA;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionActivationEventListener;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEvent;
import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEventListener;
import ch.ethz.seb.sebserver.gui.service.page.event.PageEventListener;
import ch.ethz.seb.sebserver.gui.service.page.impl.PageAction;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory.CustomVariant;
@Lazy
@Component
public class ActionPane implements TemplateComposer {
private static final String ACTION_EVENT_CALL_KEY = "ACTION_EVENT_CALL";
private static final LocTextKey TITLE_KEY = new LocTextKey("sebserver.actionpane.title");
private final PageService pageService;
private final WidgetFactory widgetFactory;
private final Map<String, Tree> actionTrees = new HashMap<>();
protected ActionPane(final PageService pageService) {
this.pageService = pageService;
this.widgetFactory = pageService.getWidgetFactory();
}
@Override
public void compose(final PageContext pageContext) {
final Label label = this.widgetFactory.labelLocalized(
pageContext.getParent(),
CustomVariant.TEXT_H2,
TITLE_KEY);
final GridData titleLayout = new GridData(SWT.FILL, SWT.TOP, true, false);
titleLayout.verticalIndent = 10;
titleLayout.horizontalIndent = 10;
if (StringUtils.isBlank(label.getText())) {
titleLayout.heightHint = 0;
}
label.setLayoutData(titleLayout);
label.setData(
PageEventListener.LISTENER_ATTRIBUTE_KEY,
(ActionPublishEventListener) event -> {
final Composite parent = pageContext.getParent();
final Tree treeForGroup = getTreeForGroup(parent, event.action.definition);
final TreeItem actionItem = ActionPane.this.widgetFactory.treeItemLocalized(
treeForGroup,
event.action.definition.title);
final Image image = event.active
? event.action.definition.icon.getImage(parent.getDisplay())
: event.action.definition.icon.getGreyedImage(parent.getDisplay());
if (!event.active) {
actionItem.setForeground(new Color(parent.getDisplay(), new RGBA(150, 150, 150, 50)));
}
actionItem.setImage(image);
actionItem.setData(ACTION_EVENT_CALL_KEY, event.action);
parent.layout();
});
final Composite composite = new Composite(pageContext.getParent(), SWT.NONE);
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false);
final GridLayout gridLayout = new GridLayout();
gridLayout.horizontalSpacing = 0;
gridData.heightHint = 0;
composite.setLayoutData(gridData);
composite.setLayout(gridLayout);
composite.setData(
PageEventListener.LISTENER_ATTRIBUTE_KEY,
(ActionActivationEventListener) event -> {
final Composite parent = pageContext.getParent();
for (final ActionDefinition ad : event.actions) {
final TreeItem actionItem = findAction(parent, ad);
if (actionItem == null) {
continue;
}
final Image image = event.activation
? ad.icon.getImage(parent.getDisplay())
: ad.icon.getGreyedImage(parent.getDisplay());
actionItem.setImage(image);
if (event.activation) {
actionItem.setForeground(null);
} else {
actionItem.setForeground(new Color(parent.getDisplay(), new RGBA(150, 150, 150, 50)));
ActionPane.this.pageService.getPolyglotPageService().injectI18n(actionItem, ad.title);
}
}
if (event.decoration != null) {
final TreeItem actionItemToDecorate = findAction(parent, event.decoration._1);
if (actionItemToDecorate != null && event.decoration._2 != null) {
actionItemToDecorate.setImage(0,
event.decoration._2.icon.getImage(parent.getDisplay()));
ActionPane.this.pageService.getPolyglotPageService().injectI18n(
actionItemToDecorate,
event.decoration._2.title);
}
}
});
}
private TreeItem findAction(final Composite parent, final ActionDefinition actionDefinition) {
final Tree treeForGroup = getTreeForGroup(parent, actionDefinition);
if (treeForGroup == null) {
return null;
}
for (int i = 0; i < treeForGroup.getItemCount(); i++) {
final TreeItem item = treeForGroup.getItem(i);
if (item == null) {
continue;
}
final PageAction action = (PageAction) item.getData(ACTION_EVENT_CALL_KEY);
if (action == null) {
continue;
}
if (action.definition == actionDefinition) {
return item;
}
}
return null;
}
private Tree getTreeForGroup(final Composite parent, final ActionDefinition actionDefinition) {
clearDisposedTrees();
final ActionCategory category = actionDefinition.category;
if (!this.actionTrees.containsKey(category.name())) {
final Tree actionTree = createActionTree(parent, actionDefinition.category);
this.actionTrees.put(category.name(), actionTree);
}
return this.actionTrees.get(category.name());
}
private Tree createActionTree(final Composite parent, final ActionCategory category) {
final Composite composite = new Composite(parent, SWT.NONE);
final GridData layout = new GridData(SWT.FILL, SWT.TOP, true, false);
composite.setLayoutData(layout);
final GridLayout gridLayout = new GridLayout();
gridLayout.marginHeight = 0;
composite.setLayout(gridLayout);
composite.setData(RWT.CUSTOM_VARIANT, "actionPane");
composite.setData("CATEGORY", category);
final Control[] children = parent.getChildren();
for (final Control child : children) {
final ActionCategory c = (ActionCategory) child.getData("CATEGORY");
if (c != null && c.slotPosition > category.slotPosition) {
composite.moveAbove(child);
break;
}
}
// title
if (this.pageService.getI18nSupport().hasText(category.title)) {
final Label actionsTitle = this.widgetFactory.labelLocalized(
composite,
CustomVariant.TEXT_H3,
category.title);
final GridData titleLayout = new GridData(SWT.FILL, SWT.TOP, true, false);
actionsTitle.setLayoutData(titleLayout);
}
// action tree
final Tree actions = this.widgetFactory.treeLocalized(
composite,
SWT.SINGLE | SWT.FULL_SELECTION);
actions.setData(RWT.CUSTOM_VARIANT, "actions");
final GridData gridData = new GridData(SWT.FILL, SWT.TOP, true, false);
actions.setLayoutData(gridData);
final Template template = new Template();
final ImageCell imageCell = new ImageCell(template);
imageCell.setLeft(0, 0)
.setWidth(40)
.setTop(0)
.setBottom(0, 0)
.setHorizontalAlignment(SWT.LEFT)
.setBackground(null);
imageCell.setBindingIndex(0);
final TextCell textCell = new TextCell(template);
textCell.setLeft(0, 30)
.setWidth(150)
.setTop(7)
.setBottom(0, 0)
.setHorizontalAlignment(SWT.LEFT);
textCell.setBindingIndex(0);
actions.setData(RWT.ROW_TEMPLATE, template);
actions.addListener(SWT.Selection, event -> {
final TreeItem treeItem = (TreeItem) event.item;
final PageAction action = (PageAction) treeItem.getData(ACTION_EVENT_CALL_KEY);
this.pageService.executePageAction(action);
if (!treeItem.isDisposed()) {
treeItem.getParent().deselectAll();
final PageAction switchAction = action.getSwitchAction();
if (switchAction != null) {
final PolyglotPageService polyglotPageService = this.pageService.getPolyglotPageService();
polyglotPageService.injectI18n(treeItem, switchAction.definition.title);
treeItem.setImage(switchAction.definition.icon.getImage(treeItem.getDisplay()));
treeItem.setData(ACTION_EVENT_CALL_KEY, switchAction);
}
}
});
return actions;
}
private void clearDisposedTrees() {
new ArrayList<>(this.actionTrees.entrySet())
.forEach(entry -> {
final Control c = entry.getValue();
// of tree is already disposed.. remove it
if (c.isDisposed()) {
this.actionTrees.remove(entry.getKey());
}
// check access from current thread
try {
c.getBounds();
} catch (final Exception e) {
this.actionTrees.remove(entry.getKey());
}
});
}
}