package fi.utu.ville.exercises.helpers;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import com.vaadin.event.LayoutEvents.LayoutClickEvent;
import com.vaadin.event.LayoutEvents.LayoutClickListener;
import com.vaadin.server.Resource;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.CheckBox;
import com.vaadin.ui.Component;
import com.vaadin.ui.DateField;
import com.vaadin.ui.GridLayout;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Image;
import com.vaadin.ui.Label;
import com.vaadin.ui.NativeSelect;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Panel;
import com.vaadin.ui.Slider;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.Window;
import com.vaadin.ui.Window.CloseEvent;
import com.vaadin.ui.Window.CloseListener;
import com.vaadin.ui.themes.BaseTheme;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.DateFilter;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.EvaluationFilter;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.InvertedFilter;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.MatchAllFilter;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.MatchAnyFilter;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.StatSubmInfoFilter;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.StatSubmInfoFilterConnector;
import fi.utu.ville.exercises.helpers.StatsGiverHelper.TimeOnTaskFilter;
import fi.utu.ville.exercises.model.SubmissionInfo;
import fi.utu.ville.standardutils.Localizer;
import fi.utu.ville.standardutils.StandardIcon.Icon;
import fi.utu.ville.standardutils.StandardUIFactory;
/**
* <p>
* This class provides a GUI through which a user can create instances of different {@link StatSubmInfoFilter}s, connect them by {@link MatchAllFilter} or
* {@link MatchAnyFilter} and invert them by {@link InvertedFilter}.
* </p>
* <p>
* By default editors for three default filter-types ({@link DateFilter}, {@link TimeOnTaskFilter} and {@link EvaluationFilter}) are present.
* </p>
* <p>
* Editors for more filter-types can be added by implementing {@link FilterEditorFactory}-interface that can provide a user with general info about certain
* filter and instantiate {@link FilterEditor}-implementor that can be used to create and edit an instance of certain {@link StatSubmInfoFilter} -implementor.
* To make the new implementor available to the user construct this class with the implementor included in 'extraFilterFactories'-list.
* </p>
*
* @author Riku Haavisto
*
* @param <S>
* accepted {@link SubmissionInfo} implementor
*/
public class StatSubmInfoFilterEditor<S extends SubmissionInfo> implements
Serializable {
/**
* Implementor of this interface can show certain general information about certain {@link StatSubmInfoFilter} and instantiate {@link FilterEditor}s that
* can be used for creating and editing instances of that {@link StatSubmInfoFilter}-type.
*
* @author Riku Haavisto
*
* @param <A>
* accepted {@link SubmissionInfo} implementor
*/
public interface FilterEditorFactory<A extends SubmissionInfo> {
/**
* Instantiates a new instance of the represented {@link FilterEditor}.
*
* @param localizer
* {@link Localizer} that can be passed to new instance {@link FilterEditor} help localizing UI
* @return a new {@link FilterEditor}-instance
*/
FilterEditor<A> newEditorInstance(Localizer localizer);
/**
* Gives a short description of the represented filter that can be created and edited by editor fetched from this {@link FilterEditorFactory}.
*
* @param localizer
* {@link Localizer} for localizing UI
* @return short description of represented filter
*/
String getFilterDesc(Localizer localizer);
/**
* Gives a name of the represented filter that can be created and edited by editor fetched from this {@link FilterEditorFactory}.
*
* @param localizer
* {@link Localizer} for localizing UI
* @return name of represented filter
*/
String getFilterName(Localizer localizer);
/**
* Returns an icon representing the filter that can be created and edited by editor fetched from this {@link FilterEditorFactory}.
*
* @return {@link Resource} of icon for represented filter
*/
Resource getFilterIcon();
}
/**
* An implementor of this class can create and edit certain {@link StatSubmInfoFilter}-implementor.
*
* @author Riku Haavisto
*
* @param <A>
* accepted {@link SubmissionInfo} implementor
*/
public interface FilterEditor<A extends SubmissionInfo> {
/**
* Returns a new independent copy of this {@link FilterEditor} instance with matching state.
*
* @return an independent copy of this editor
*/
FilterEditor<A> getCopy();
/**
* Return {@link StatSubmInfoFilter}-implementor matching current state of this {@link FilterEditor}.
*
* @return {@link StatSubmInfoFilter} matching editor's current state
*/
StatSubmInfoFilter<A> getFilter();
/**
* <p>
* Return a string-presentation of the current state of this {@link FilterEditor}.
* </p>
* <p>
* This can be for example (localized) filter-name : values of editable variables. eg. Evaluation : from 0.4 to 0.8 .
* </p>
*
* @return a textual localized description of this editor's current state
*/
String getFilterStateDesc();
/**
* Return a {@link Component} containing a GUI through which a user can edit variables of this filter.
*
* @return a GUI for editing variables of a certain filter-implementor
*/
Component getFilterEditView();
/**
* <p>
* Returns a {@link Component} representing minimized view of this {@link FilterEditor}s state.
* </p>
* <p>
* This view should be really compact. Default implementations use as their minified-view the icon of represented filter-type (as returned by
* corresponding {@link FilterEditorFactory #getFilterIcon()} with {@link #getFilterStateDesc()} as its tooltip.
* </p>
*
* @return a minified view of the type and current state of this filter-editor
*/
Component getMinifiedView();
/**
* This method is called whenever user has edited this {@link FilterEditor} through {@link #getFilterEditView()} and tries to 'commit' the edits. If the
* filter-editor is left in an inconsistent state this method should return false. It is also possible (and nice) to notify the user on what is wrong
* with the current state of the editor ( by eg. adding info to the edit-view or by showing a {@link Notification}).
*
* @return true if editor is in consistent state
*/
boolean checkAndNotify();
}
/*
*
* Implementation of actual 'StatSubmInfoFilterEditor' that can be shown to
* user starts here.
*/
/**
*
*/
private static final long serialVersionUID = 40221094607340126L;
private EditorConnectorView<S> mainLevelFilter;
private final FilterFactoryKeeper<S> ffKeeper = new FilterFactoryKeeper<S>();
private final StatSubmInfoFilterTable<S> applyTo;
private final Localizer localizer;
private Button clearBtn;
private Button hideShowEditorBtn;
private Button applyBtn;
private boolean minimized = false;
private final Panel mainLevelPanel = new Panel();
private final VerticalLayout centeringLayout = new VerticalLayout();
private final VerticalLayout mainLayout = new VerticalLayout();
private final ClickListener clickListener = new ClickListener() {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public void buttonClick(ClickEvent event) {
if (event.getButton() == applyBtn) {
applyFilter();
} else if (event.getButton() == clearBtn) {
clearFilter();
} else if (event.getButton() == hideShowEditorBtn) {
setMinimized(!minimized);
}
}
};
/**
* Constructs a new {@link StatSubmInfoFilterEditor} that can be used to provide the user with a GUI for creating and editing a {@link StatSubmInfoFilter}
* that can be applied to given {@link StatSubmInfoFilterTable}.
*
* @param applyTo
* {@link StatSubmInfoFilterTable} that is filtered by this editor
* @param localizer
* {@link Localizer} for localizing UI
* @param extraFilterFactories
* {@link Collection} of all {@link FilterEditorFactory} -implementors that will be added to enable generating custom filters; can be null if
* only default filter-types are required
*/
public StatSubmInfoFilterEditor(StatSubmInfoFilterTable<S> applyTo,
Localizer localizer,
Collection<FilterEditorFactory<S>> extraFilterFactories) {
this.localizer = localizer;
this.applyTo = applyTo;
if (this.localizer == null || this.applyTo == null) {
throw new IllegalArgumentException(
"localizer and applyTo must be non-null: localizer= "
+ localizer + "; applyTo= " + applyTo);
}
mainLevelFilter = new EditorConnectorView<S>(null, ffKeeper, localizer);
init();
if (extraFilterFactories != null) {
for (FilterEditorFactory<S> extraFF : extraFilterFactories) {
ffKeeper.registerFilterEditorFactory(extraFF);
}
}
doLayout();
}
private void init() {
ffKeeper.registerFilterEditorFactory(new DateIntervalFilterEditor<S>(
localizer));
ffKeeper.registerFilterEditorFactory(new TimeonTaskFilterEditor<S>(
localizer));
ffKeeper.registerFilterEditorFactory(new EvaluationilterEditor<S>(
localizer));
}
/**
* Clears the current filter of this {@link StatSubmInfoFilterEditor} and updates the table to a non-filtered state.
*/
public void clearFilter() {
mainLevelFilter = new EditorConnectorView<S>(null, ffKeeper, localizer);
centeringLayout.removeAllComponents();
centeringLayout.addComponent(mainLevelFilter);
centeringLayout.setComponentAlignment(mainLevelFilter,
Alignment.TOP_CENTER);
applyTo.setFilter(getTotalFilter());
}
/**
* Applies the filter currently represented by this {@link StatSubmInfoFilterEditor} to the controlled {@link StatSubmInfoFilterTable}.
*/
public void applyFilter() {
applyTo.setFilter(getTotalFilter());
}
/**
* Minimizes or maximizes the actual filter-editor-view.
*
* @param minimized
* true if filter-editor should be minimized
*/
public void setMinimized(boolean minimized) {
this.minimized = minimized;
updateMinimState();
}
/**
* Returns the {@link StatSubmInfoFilter} matching the current state of this {@link StatSubmInfoFilterEditor}. The returned {@link StatSubmInfoFilter} might
* actually be made of several {@link StatSubmInfoFilter}s connected by {@link StatSubmInfoFilterConnector}s.
*
* @return {@link StatSubmInfoFilter} matching the current state of this {@link StatSubmInfoFilterEditor}
*/
public StatSubmInfoFilter<S> getTotalFilter() {
return mainLevelFilter.asFilter();
}
/**
* Returns a GUI through which the user can control this filter-editor.
*
* @return GUI for this filter-editor
*/
public Component getView() {
return mainLayout;
}
private void doLayout() {
mainLevelPanel.setWidth("100%");
centeringLayout.setWidth("100%");
centeringLayout.setMargin(true);
mainLevelPanel.setContent(centeringLayout);
centeringLayout.addComponent(mainLevelFilter);
centeringLayout.setComponentAlignment(mainLevelFilter,
Alignment.TOP_CENTER);
mainLayout.addComponent(mainLevelPanel);
hideShowEditorBtn = StandardUIFactory.getButton("show editor", null);
hideShowEditorBtn.addClickListener(clickListener);
clearBtn = StandardUIFactory.getButton("clear filter", null);
clearBtn.addClickListener(clickListener);
applyBtn = StandardUIFactory.getButton("apply filter", Icon.ATTACH);
applyBtn.addClickListener(clickListener);
HorizontalLayout buttons = new HorizontalLayout();
buttons.addComponents(hideShowEditorBtn, clearBtn, applyBtn);
buttons.setSpacing(true);
buttons.setMargin(true);
buttons.setSizeUndefined();
mainLayout.addComponent(buttons);
mainLayout.setComponentAlignment(buttons, Alignment.BOTTOM_RIGHT);
updateMinimState();
}
private void updateMinimState() {
if (minimized) {
hideShowEditorBtn.setCaption("show");
mainLevelPanel.setVisible(false);
} else {
hideShowEditorBtn.setCaption("hide");
mainLevelPanel.setVisible(true);
}
}
private static class FilterFactoryKeeper<B extends SubmissionInfo> {
private final List<FilterEditorFactory<B>> factories =
new ArrayList<FilterEditorFactory<B>>();
public void registerFilterEditorFactory(FilterEditorFactory<B> feFactory) {
factories.add(feFactory);
}
public List<FilterEditorFactory<B>> getAllEditorFactories() {
return new ArrayList<FilterEditorFactory<B>>(factories);
}
}
// *SPECIAL*-FilterEditor implementor used to provide somewhat similar
// interface to editing filter-connector as normal filter-editors use
//
// should not be added to factory-keeper as instances of
// connectors are created (and must be created) in a special way that
// gives them some other editors to connect
// there could also be different factory-collection for adding
// new 'ConnectorTypes' but that would probably be mostly confusing
// and I cannot think of a situation where one would really need
// connectors like match-any-two
private static class FilterConnectorEditor<S extends SubmissionInfo>
implements FilterEditor<S> {
public enum ConnectorType {
ALL("&"),
ANY("|");
public final String connector;
private ConnectorType(String connector) {
this.connector = connector;
}
}
private final NativeSelect typeSel = new NativeSelect();
public FilterConnectorEditor() {
for (ConnectorType ct : ConnectorType.values()) {
typeSel.addItem(ct);
typeSel.setItemCaption(ct, ct.name());
}
typeSel.setNullSelectionAllowed(false);
typeSel.select(ConnectorType.ALL);
}
@Override
public StatSubmInfoFilter<S> getFilter() {
if (getCurrType() == ConnectorType.ALL) {
return new MatchAllFilter<S>();
} else {
return new MatchAnyFilter<S>();
}
}
@Override
public Component getFilterEditView() {
return typeSel;
}
private ConnectorType getCurrType() {
return (ConnectorType) typeSel.getValue();
}
@Override
public Component getMinifiedView() {
Image img = new Image();
img.setDescription(getFilterStateDesc());
return img;
}
@Override
public FilterConnectorEditor<S> getCopy() {
FilterConnectorEditor<S> res = new FilterConnectorEditor<S>();
res.typeSel.select(getCurrType());
return res;
}
@Override
public String getFilterStateDesc() {
return getCurrType().name();
}
@Override
public boolean checkAndNotify() {
// always in 'ok'-state
return true;
}
}
/*
* The actual editor implementation starts here and is pretty nasty and
* hacky: connector-editor extends normal filter-editor and overrides almost
* all of its methods, and also has to check by 'instanceof' whether certain
* of its children is actually a connector-editor in some places.
*
* This hacky implementation should be kept well hidden from the public
* interface.
*/
private static class EditorConnectorView<B extends SubmissionInfo> extends
FilterEditorView<B> implements ClickListener {
/**
*
*/
private static final long serialVersionUID = -2768397561770574721L;
private final FilterFactoryKeeper<B> ffKeeper;
private final List<FilterEditorView<B>> children = new ArrayList<FilterEditorView<B>>();
// extra buttons
private final Button addEditorBtn = new Button();
private final Button groupToConnectorBtn = new Button();
// direct access to filter-connector editor ( no need to do castings
// that often...), this is just casted 'theEditor'
private FilterConnectorEditor<B> actEditor;
public EditorConnectorView(EditorConnectorView<B> parent,
FilterFactoryKeeper<B> ffKeeper, Localizer localizer) {
super(new FilterConnectorEditor<B>(), parent, localizer);
this.actEditor = (FilterConnectorEditor<B>) theEditor;
this.ffKeeper = ffKeeper;
ctrlMinified = false;
initLayout();
}
public EditorConnectorView(EditorConnectorView<B> parent,
FilterFactoryKeeper<B> ffKeeper, Localizer localizer,
FilterConnectorEditor<B> editorToUse, boolean inverted,
List<FilterEditorView<B>> childrenToCopy, boolean ctrlMinified) {
super(editorToUse, parent, localizer, inverted, ctrlMinified);
this.actEditor = editorToUse;
this.ffKeeper = ffKeeper;
for (FilterEditorView<B> childToCopy : childrenToCopy) {
children.add(childToCopy.getCopy(this));
}
initLayout();
}
private void initLayout() {
if (parent == null) {
minMaxCtrlBtn.setVisible(false);
ctrlMinified = false;
}
addEditorBtn.setDescription("Add editor");
addEditorBtn.setStyleName(BaseTheme.BUTTON_LINK);
addEditorBtn.addClickListener(this);
groupToConnectorBtn.setDescription("Group to connector");
groupToConnectorBtn.setStyleName(BaseTheme.BUTTON_LINK);
groupToConnectorBtn.addClickListener(this);
HorizontalLayout extraControlBtns = new HorizontalLayout();
extraControlBtns.addComponents(addEditorBtn, groupToConnectorBtn);
controlsLayout.addComponent(new Label("|"));
controlsLayout.addComponent(extraControlBtns);
updateGroupToState();
updateLayout();
}
@Override
public StatSubmInfoFilter<B> asFilter() {
/*
* Get base-filter from the super-classes asFilter()-method. If this
* filter is inverted the actual connnector-filter must be fetched
* from the inverted-filter
*/
StatSubmInfoFilterConnector<B> baseFilter;
if (!inverted) {
baseFilter = (StatSubmInfoFilterConnector<B>) super.asFilter();
} else {
baseFilter = (StatSubmInfoFilterConnector<B>) ((InvertedFilter<B>) super.asFilter()).getUnderlyingFilter();
}
// add all the children to the connector
for (FilterEditorView<B> filterEdit : children) {
baseFilter.connectFilter(filterEdit.asFilter());
}
// as the underlying connector filter was fetched to be able to add
// children to it, re-wrap it to inverted-filter if needed
if (inverted) {
return new InvertedFilter<B>(baseFilter);
} else {
return baseFilter;
}
}
private void updateGroupToState() {
// no mind in adding a sub-group if there is less than three
// children
if (children.size() >= 3) {
groupToConnectorBtn.setEnabled(true);
groupToConnectorBtn.setVisible(true);
} else {
groupToConnectorBtn.setEnabled(false);
groupToConnectorBtn.setVisible(false);
}
}
@Override
protected void updateBackingEditor(FilterEditor<B> newEditor) {
this.actEditor = (FilterConnectorEditor<B>) theEditor;
super.updateBackingEditor(newEditor);
}
@Override
protected void updateLayout() {
// override to update-layout mechanic completely to draw
// also the children, and to also implement minimizing differently
mainStateView.removeAllComponents();
mainStateView.setSpacing(true);
if (ctrlMinified) {
controlsLayout.setVisible(false);
Image img = new Image();
String connector = actEditor.getCurrType().connector;
String desc = actEditor.getCurrType().name() + ": ( ";
for (int i = 0, n = children.size(); i < n; i++) {
if (children.get(i).inverted) {
desc += "NOT ";
}
desc += children.get(i).theEditor.getFilterStateDesc();
if (i < n - 1) {
desc += " " + connector + " ";
}
}
desc += ")";
img.setDescription(desc);
mainStateView.addComponent(img);
} else {
controlsLayout.setVisible(true);
String connector = actEditor.getCurrType().connector;
Label startLbl = new Label("(");
mainStateView.addComponent(startLbl);
mainStateView.setComponentAlignment(startLbl,
Alignment.MIDDLE_LEFT);
if (children.isEmpty()) {
mainStateView.addComponent(new Label("EMPTY"));
} else {
for (int i = 0, n = children.size(); i < n; i++) {
Component childComp = children.get(i);
mainStateView.addComponent(childComp);
mainStateView.setComponentAlignment(childComp,
Alignment.MIDDLE_CENTER);
if (i < n - 1) {
Label connLbl = new Label(connector);
mainStateView.addComponent(connLbl);
mainStateView.setComponentAlignment(connLbl,
Alignment.MIDDLE_CENTER);
}
}
}
Label endLbl = new Label(")");
mainStateView.addComponent(endLbl);
mainStateView.setComponentAlignment(endLbl,
Alignment.MIDDLE_CENTER);
}
updateGroupToState();
}
public void removeEditor(FilterEditorView<B> toRem) {
// check whether the child is normal child or a connector;
// if it is a connector, do not delete its children but
// add them as children of this connector instead
if (toRem instanceof EditorConnectorView) {
EditorConnectorView<B> childConnView = (EditorConnectorView<B>) toRem;
List<FilterEditorView<B>> childsChildren = new ArrayList<FilterEditorView<B>>(
childConnView.children);
children.remove(childConnView);
List<FilterEditorView<B>> reAddChildren = new ArrayList<FilterEditorView<B>>();
for (FilterEditorView<B> childChild : childsChildren) {
reAddChildren.add(childChild.getCopy(this));
}
children.addAll(reAddChildren);
}
// if the child to remove is not a connector, just remove it
else {
children.remove(toRem);
}
updateLayout();
}
@Override
public void buttonClick(ClickEvent event) {
super.buttonClick(event);
if (event.getButton() == addEditorBtn) {
showAddNewPopup();
} else if (event.getButton() == groupToConnectorBtn) {
showGroupToPopup();
} else if (event.getButton() == minMaxCtrlBtn) {
updateLayout();
}
}
private FilterEditorView<B> addNewFilterEditor(FilterEditor<B> toAdd) {
FilterEditorView<B> wrappedEditor = new FilterEditorView<B>(toAdd,
this, localizer);
children.add(wrappedEditor);
updateLayout();
return wrappedEditor;
}
@Override
public EditorConnectorView<B> getCopy(EditorConnectorView<B> newParent) {
// this copy constructor will handle copying children by adding them
// to new list
// and calling their getCopy(parent) method with itself as the new
// parent
EditorConnectorView<B> res = new EditorConnectorView<B>(newParent,
ffKeeper, localizer, actEditor.getCopy(), inverted,
children, ctrlMinified);
return res;
}
private void showAddNewPopup() {
final Window popup = new Window();
popup.setWidth("400px");
popup.setHeight("400px");
popup.center();
popup.setModal(true);
VerticalLayout cont = new VerticalLayout();
cont.setMargin(true);
cont.setSpacing(true);
GridLayout factories = new GridLayout(3, 3);
factories.setSpacing(true);
factories.setMargin(true);
for (final FilterEditorFactory<B> fef : ffKeeper
.getAllEditorFactories()) {
VerticalLayout aFactory = new VerticalLayout();
Image img = new Image(null, fef.getFilterIcon());
img.setDescription(fef.getFilterDesc(localizer));
aFactory.addComponent(img);
aFactory.addComponent(new Label(fef.getFilterName(localizer)));
aFactory.addLayoutClickListener(new LayoutClickListener() {
/**
*
*/
private static final long serialVersionUID = -6943926604606589576L;
@Override
public void layoutClick(LayoutClickEvent event) {
FilterEditor<B> newEditor = fef
.newEditorInstance(localizer);
FilterEditorView<B> wrappedEditor = addNewFilterEditor(newEditor);
UI.getCurrent().removeWindow(popup);
wrappedEditor.showEditFilterPopup();
}
});
factories.addComponent(aFactory);
}
cont.addComponent(factories);
popup.setContent(cont);
UI.getCurrent().addWindow(popup);
}
@Override
protected void setNoControls(boolean noControls) {
super.setNoControls(noControls);
// override this to also set this for all the children
for (FilterEditorView<B> child : children) {
child.setNoControls(noControls);
}
}
private void showGroupToPopup() {
final Window popup = new Window();
popup.setWidth("400px");
popup.setHeight("400px");
popup.center();
popup.setModal(true);
VerticalLayout cont = new VerticalLayout();
cont.setMargin(true);
cont.setSpacing(true);
HorizontalLayout selLayout = new HorizontalLayout();
selLayout.setMargin(true);
selLayout.setSpacing(true);
// add all chhildren in no-controls mode
// and list of checkboxes under them to enable user to select which
// of the children to group under the new connector
final ArrayList<CheckBox> selChildren = new ArrayList<CheckBox>();
for (int i = 0, n = children.size(); i < n; i++) {
VerticalLayout aChild = new VerticalLayout();
FilterEditorView<B> copied = children.get(i).getCopy(null);
copied.setNoControls(true);
aChild.addComponent(copied);
CheckBox childCheckbox = new CheckBox();
selChildren.add(i, childCheckbox);
aChild.addComponent(childCheckbox);
selLayout.addComponent(aChild);
}
cont.addComponent(selLayout);
Button okButton = StandardUIFactory.getOKButton(localizer);
okButton.addClickListener(new ClickListener() {
/**
*
*/
private static final long serialVersionUID = -8713670656337690394L;
@Override
public void buttonClick(ClickEvent event) {
HashSet<Integer> selIndexes = new HashSet<Integer>();
for (int i = 0; i < selChildren.size(); i++) {
if (selChildren.get(i).getValue()) {
selIndexes.add(i);
}
}
if (selIndexes.isEmpty()
|| selIndexes.size() == children.size()) {
Notification
.show("You must select some but not all children to the new group!");
} else {
EditorConnectorView<B> newConnector = new EditorConnectorView<B>(
EditorConnectorView.this, ffKeeper, localizer);
ArrayList<FilterEditorView<B>> movedChildren = new ArrayList<FilterEditorView<B>>();
// a bit hacky solution but should work:
// remove items backwards from the children-list so that
// any index-shifting won't affect sub-sequent removals
for (int bI = children.size() - 1; bI > 0; bI--) {
if (selIndexes.contains(bI)) {
FilterEditorView<B> addToNew = children
.remove(bI);
// add to start of the movedChildren to
// re-invert the order regardless
// of the backwards iteration
FilterEditorView<B> copiedToNewParent = addToNew
.getCopy(newConnector);
movedChildren.add(0, copiedToNewParent);
}
}
// now add the moved children to new connector
for (int i = 0, n = movedChildren.size(); i < n; i++) {
// this is more efficient than using the public
// API...
newConnector.children.add(movedChildren.get(i));
}
newConnector.updateLayout();
children.add(newConnector);
updateLayout();
UI.getCurrent().removeWindow(popup);
}
}
});
cont.addComponent(okButton);
popup.setContent(cont);
UI.getCurrent().addWindow(popup);
}
}
/*
* BASIC-filter-editor that is sub-classed by the filter-connector-editor
*/
private static class FilterEditorView<B extends SubmissionInfo>
extends VerticalLayout implements ClickListener {
/**
*
*/
private static final long serialVersionUID = -3738173460239017664L;
protected FilterEditor<B> theEditor;
protected final Localizer localizer;
protected final EditorConnectorView<B> parent;
protected boolean inverted;
protected boolean ctrlMinified;
// buttons
private final Button remButton = new Button();
private final Button editButton = new Button();
private final Button invertButton = new Button();
protected final Button minMaxCtrlBtn = new Button();
// layouts
protected final HorizontalLayout mainStateView = new HorizontalLayout();
protected final HorizontalLayout invAndStateView = new HorizontalLayout();
protected final VerticalLayout ctrlAndMinMaxLayout = new VerticalLayout();
protected final HorizontalLayout controlsLayout = new HorizontalLayout();
public FilterEditorView(FilterEditor<B> theEditor,
EditorConnectorView<B> parent, Localizer localizer) {
this.theEditor = theEditor;
this.parent = parent;
this.inverted = false;
this.ctrlMinified = true;
this.localizer = localizer;
doLayout();
}
public FilterEditorView(FilterEditor<B> theEditor,
EditorConnectorView<B> parent, Localizer localizer,
boolean inverted, boolean ctrlMinified) {
this.theEditor = theEditor;
this.parent = parent;
this.inverted = inverted;
this.ctrlMinified = ctrlMinified;
this.localizer = localizer;
doLayout();
}
public StatSubmInfoFilter<B> asFilter() {
// add inverted-wrapping if needed
if (inverted) {
InvertedFilter<B> asInverted = new InvertedFilter<B>(
theEditor.getFilter());
return asInverted;
} else {
return theEditor.getFilter();
}
}
private void doLayout() {
invertButton.setStyleName(BaseTheme.BUTTON_LINK);
invertButton.addClickListener(this);
// set initial invert-state icon
updateInvertedIndicator();
invAndStateView.addComponent(invertButton);
invAndStateView.setComponentAlignment(invertButton,
Alignment.MIDDLE_LEFT);
mainStateView.addComponent(theEditor.getMinifiedView());
invAndStateView.addComponent(mainStateView);
minMaxCtrlBtn.setDescription("Minim-maxim");
minMaxCtrlBtn.setStyleName(BaseTheme.BUTTON_LINK);
minMaxCtrlBtn.addClickListener(this);
remButton.setStyleName(BaseTheme.BUTTON_LINK);
remButton.addClickListener(this);
if (parent == null) {
remButton.setEnabled(false);
remButton.setVisible(false);
}
editButton.setStyleName(BaseTheme.BUTTON_LINK);
editButton.addClickListener(this);
controlsLayout.addComponents(editButton, remButton);
controlsLayout.setMargin(true);
controlsLayout.addStyleName("ville-mild-bg");
ctrlAndMinMaxLayout.addComponents(controlsLayout, minMaxCtrlBtn);
ctrlAndMinMaxLayout.setComponentAlignment(controlsLayout,
Alignment.TOP_CENTER);
ctrlAndMinMaxLayout.setComponentAlignment(minMaxCtrlBtn,
Alignment.BOTTOM_CENTER);
addComponents(ctrlAndMinMaxLayout, invAndStateView);
setComponentAlignment(ctrlAndMinMaxLayout, Alignment.TOP_CENTER);
setComponentAlignment(invAndStateView, Alignment.BOTTOM_CENTER);
addStyleName("ville-dashed-border");
setMargin(true);
setSpacing(true);
setSizeUndefined();
updateMinMaxCrtl();
}
protected void updateBackingEditor(FilterEditor<B> newEditor) {
// overrides the 'theEditor' with new editor instance copied
// from it; this is used to ensure that the filter-editor under edit
// is disconnected from actual filter-editor in is 'committed' only
// when the editor-under-edit is in consistent state (and user wants
// to commit the changes); also enables canceling the editing
this.theEditor = newEditor;
updateLayout();
}
protected void updateLayout() {
mainStateView.removeAllComponents();
mainStateView.addComponent(theEditor.getMinifiedView());
}
protected void setNoControls(boolean noControls) {
// hide control-layout and remove listener from invert-button (but
// do not hide the button as it also indicates current invert-state)
ctrlAndMinMaxLayout.setVisible(!noControls);
if (noControls) {
invertButton.removeClickListener(this);
} else {
invertButton.addClickListener(this);
}
}
public FilterEditorView<B> getCopy(EditorConnectorView<B> newParent) {
FilterEditorView<B> res = new FilterEditorView<B>(
theEditor.getCopy(), newParent, localizer, inverted,
ctrlMinified);
return res;
}
private void updateMinMaxCrtl() {
if (ctrlMinified) {
controlsLayout.setVisible(false);
} else {
controlsLayout.setVisible(true);
}
}
private void updateInvertedIndicator() {
if (inverted) {
} else {
}
}
private void showEditFilterPopup() {
final Window popup = new Window();
popup.setWidth("400px");
popup.setHeight("400px");
popup.center();
popup.setModal(true);
VerticalLayout cont = new VerticalLayout();
cont.setMargin(true);
cont.setSpacing(true);
final FilterEditor<B> editCopy = theEditor.getCopy();
cont.addComponent(editCopy.getFilterEditView());
popup.setContent(cont);
popup.addCloseListener(new CloseListener() {
/**
*
*/
private static final long serialVersionUID = 8568957374426173040L;
@Override
public void windowClose(CloseEvent e) {
updateLayout();
}
});
// edit's are committed only if OK-button is clicked and
// editor's checkAndNotify returns true;
// closing the popup with other methods effectively cancel
// any edits done
Button okButton = StandardUIFactory.getOKButton(localizer);
okButton.addClickListener(new ClickListener() {
/**
*
*/
private static final long serialVersionUID = -6979725003624509202L;
@Override
public void buttonClick(ClickEvent event) {
if (editCopy.checkAndNotify()) {
UI.getCurrent().removeWindow(popup);
updateBackingEditor(editCopy);
}
}
});
cont.addComponent(okButton);
UI.getCurrent().addWindow(popup);
}
@Override
public void buttonClick(ClickEvent event) {
if (event.getButton() == invertButton) {
inverted = !inverted;
updateInvertedIndicator();
} else if (event.getButton() == remButton) {
if (parent != null) {
parent.removeEditor(this);
}
} else if (event.getButton() == editButton) {
showEditFilterPopup();
} else if (event.getButton() == minMaxCtrlBtn) {
ctrlMinified = !ctrlMinified;
updateMinMaxCrtl();
}
}
}
/*
*
* Implementations for editors of three general filter-types ( done-time,
* evaluation, time-on-task) start here.
*
* All of these implementations implement both FilterEditor and
* FilterEditorFactory at the same time, but that is of course not required;
* it is convenient (as eg. icon is readily available to in FilterEditor)
* but maybe a bit confusing...
*/
// TODO: localization of default editors
private static class DateIntervalFilterEditor<A extends SubmissionInfo>
implements FilterEditor<A>, FilterEditorFactory<A> {
private DateField start = new DateField();
private DateField end = new DateField();
private final Localizer localizer;
public DateIntervalFilterEditor(Localizer localizer) {
this.localizer = localizer;
}
@Override
public FilterEditor<A> newEditorInstance(Localizer localizer) {
return new DateIntervalFilterEditor<A>(localizer);
}
@Override
public String getFilterDesc(Localizer localizer) {
return "date-range-editor";
}
@Override
public String getFilterName(Localizer localizer) {
return "date-range-editor";
}
@Override
public Resource getFilterIcon() {
return null;
}
@Override
public StatSubmInfoFilter<A> getFilter() {
long startMillis = (start.getValue() != null ? start.getValue()
.getTime() : 0L);
long endMillis = (end.getValue() != null ? end.getValue().getTime()
: Long.MAX_VALUE);
return new DateFilter<A>(startMillis, endMillis);
}
@Override
public Component getFilterEditView() {
VerticalLayout res = new VerticalLayout();
res.addComponent(new Label("start"));
res.addComponent(start);
res.addComponent(new Label("end"));
res.addComponent(end);
return res;
}
@Override
public Component getMinifiedView() {
Image img = new Image();
img.setSource(getFilterIcon());
img.setDescription(getFilterStateDesc());
return img;
}
@Override
public DateIntervalFilterEditor<A> getCopy() {
DateField newStart = new DateField();
DateField newEnd = new DateField();
newStart.setValue(start.getValue());
newEnd.setValue(end.getValue());
DateIntervalFilterEditor<A> res = new DateIntervalFilterEditor<A>(
localizer);
res.end = newEnd;
res.start = newStart;
return res;
}
@Override
public String getFilterStateDesc() {
return getFilterName(localizer) + ": from " + start.getValue()
+ " to " + end.getValue();
}
@Override
public boolean checkAndNotify() {
long startMillis = (start.getValue() != null ? start.getValue()
.getTime() : 0L);
long endMillis = (end.getValue() != null ? end.getValue().getTime()
: Long.MAX_VALUE);
if (endMillis > startMillis) {
return true;
} else {
Notification
.show("If both are specified, start-date must come before end-date");
return false;
}
}
}
private static class EvaluationilterEditor<A extends SubmissionInfo>
implements FilterEditor<A>, FilterEditorFactory<A> {
private final Slider min;
private final Slider max;
private final Localizer localizer;
public EvaluationilterEditor(Localizer localizer) {
this.localizer = localizer;
min = new Slider();
max = new Slider();
min.setMin(0.0);
min.setMax(1.0);
max.setMin(0.0);
max.setMax(1.0);
min.setResolution(2);
max.setResolution(2);
min.setWidth("50%");
max.setWidth("50%");
}
@Override
public FilterEditor<A> newEditorInstance(Localizer localizer) {
return new EvaluationilterEditor<A>(localizer);
}
@Override
public String getFilterDesc(Localizer localizer) {
return "evaluation-range-editor";
}
@Override
public String getFilterName(Localizer localizer) {
return "evaluation-range-editor";
}
@Override
public Resource getFilterIcon() {
return null;
// return StandardIcon.STAR_MEDIUM.getIcon();
}
@Override
public StatSubmInfoFilter<A> getFilter() {
return new EvaluationFilter<A>(min.getValue(), max.getValue());
}
@Override
public Component getFilterEditView() {
VerticalLayout res = new VerticalLayout();
res.setSpacing(true);
res.setMargin(true);
res.setWidth("100%");
res.addComponent(new Label("min"));
res.addComponent(min);
res.addComponent(new Label("max"));
res.addComponent(max);
return res;
}
@Override
public Component getMinifiedView() {
Image img = new Image();
img.setSource(getFilterIcon());
img.setDescription(getFilterStateDesc());
return img;
}
@Override
public EvaluationilterEditor<A> getCopy() {
EvaluationilterEditor<A> res = new EvaluationilterEditor<A>(
localizer);
res.min.setValue(min.getValue());
res.max.setValue(max.getValue());
return res;
}
@Override
public String getFilterStateDesc() {
return getFilterName(localizer) + ": from " + min.getValue()
+ " to " + max.getValue();
}
@Override
public boolean checkAndNotify() {
if (min.getValue() < max.getValue()) {
return true;
} else {
Notification.show("Max must be at least equal to min");
return false;
}
}
}
private static class TimeonTaskFilterEditor<A extends SubmissionInfo>
implements FilterEditor<A>, FilterEditorFactory<A> {
private final TextField min;
private final TextField max;
private final Localizer localizer;
public TimeonTaskFilterEditor(Localizer localizer) {
min = new TextField();
max = new TextField();
this.localizer = localizer;
}
@Override
public TimeonTaskFilterEditor<A> newEditorInstance(Localizer localizer) {
return new TimeonTaskFilterEditor<A>(localizer);
}
@Override
public String getFilterDesc(Localizer localizer) {
return "time-on-task-range-editor";
}
@Override
public String getFilterName(Localizer localizer) {
return "time-on-task-range-editor";
}
@Override
public Resource getFilterIcon() {
return null;
// return StandardIcon.STOP_WATCH_MEDIUM.getIcon();
}
@Override
public TimeOnTaskFilter<A> getFilter() {
return new TimeOnTaskFilter<A>(getMin(), getMax());
}
private int getMin() {
String val = min.getValue();
if (val.length() == 0) {
return 0;
} else {
return Integer.parseInt(val);
}
}
private int getMax() {
String val = max.getValue();
if (val.length() == 0) {
return 0;
} else {
return Integer.parseInt(val);
}
}
@Override
public Component getFilterEditView() {
VerticalLayout res = new VerticalLayout();
res.addComponent(new Label("min"));
res.addComponent(min);
res.addComponent(new Label("max"));
res.addComponent(max);
return res;
}
@Override
public Component getMinifiedView() {
Image img = new Image();
img.setSource(getFilterIcon());
img.setDescription(getFilterStateDesc());
return img;
}
@Override
public TimeonTaskFilterEditor<A> getCopy() {
TimeonTaskFilterEditor<A> res = new TimeonTaskFilterEditor<A>(
localizer);
res.min.setValue(getMin() + "");
res.max.setValue(getMax() + "");
return res;
}
@Override
public String getFilterStateDesc() {
return getFilterName(localizer) + ": from " + getMin() + " to "
+ getMax();
}
@Override
public boolean checkAndNotify() {
if (getMin() <= getMax()) {
return true;
} else {
Notification.show("Max must be at least equal to min");
return false;
}
}
}
}