package name.abuchen.portfolio.ui.views.dashboard;
import java.util.StringJoiner;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.util.LocalSelectionTransfer;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DragSource;
import org.eclipse.swt.dnd.DragSourceAdapter;
import org.eclipse.swt.dnd.DragSourceEvent;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ToolBar;
import name.abuchen.portfolio.model.Dashboard;
import name.abuchen.portfolio.ui.Images;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.util.AbstractDropDown;
import name.abuchen.portfolio.ui.util.ContextMenu;
import name.abuchen.portfolio.ui.util.InfoToolTip;
import name.abuchen.portfolio.ui.util.SimpleAction;
import name.abuchen.portfolio.ui.views.AbstractHistoricView;
public class DashboardView extends AbstractHistoricView
{
private static final class WidgetDragSourceAdapter extends DragSourceAdapter
{
private final LocalSelectionTransfer transfer;
private final Control dragSource;
private WidgetDragSourceAdapter(LocalSelectionTransfer transfer, Control control)
{
this.transfer = transfer;
this.dragSource = control;
}
@Override
public void dragSetData(DragSourceEvent event)
{
Control widgetComposite = dragSource;
while (!(widgetComposite.getData() instanceof Dashboard.Widget))
widgetComposite = widgetComposite.getParent();
transfer.setSelection(new StructuredSelection(widgetComposite));
}
@Override
public void dragStart(DragSourceEvent event)
{
Control control = ((DragSource) event.getSource()).getControl();
while (!(control.getData() instanceof Dashboard.Widget))
control = control.getParent();
Point size = control.getSize();
GC gc = new GC(control);
Image image = new Image(control.getDisplay(), size.x, size.y);
gc.copyArea(image, 0, 0);
gc.dispose();
event.image = image;
}
}
private static final class WidgetDropTargetAdapter extends DropTargetAdapter
{
private final LocalSelectionTransfer transfer;
private final Composite dropTarget;
private final Consumer<Dashboard.Widget> listener;
private WidgetDropTargetAdapter(LocalSelectionTransfer transfer, Composite dropTarget,
Consumer<Dashboard.Widget> listener)
{
this.transfer = transfer;
this.dropTarget = dropTarget;
this.listener = listener;
}
@Override
public void drop(final DropTargetEvent event)
{
Object droppedElement = ((StructuredSelection) transfer.getSelection()).getFirstElement();
if (!(droppedElement instanceof Composite))
return;
// check if dropped upon itself
Composite droppedComposite = (Composite) droppedElement;
if (droppedComposite.equals(dropTarget))
return;
Dashboard.Widget droppedWidget = (Dashboard.Widget) droppedComposite.getData();
if (droppedWidget == null)
throw new IllegalArgumentException();
Composite oldParent = droppedComposite.getParent();
Dashboard.Column oldColumn = (Dashboard.Column) oldParent.getData();
if (oldColumn == null)
throw new IllegalArgumentException();
Composite newParent = dropTarget;
while (!(newParent.getData() instanceof Dashboard.Column))
newParent = newParent.getParent();
Dashboard.Column newColumn = (Dashboard.Column) newParent.getData();
droppedComposite.setParent(newParent);
if (dropTarget.getData() instanceof Dashboard.Widget)
{
// dropped on another widget
droppedComposite.moveAbove(dropTarget);
Dashboard.Widget dropTargetWidget = (Dashboard.Widget) dropTarget.getData();
oldColumn.getWidgets().remove(droppedWidget);
newColumn.getWidgets().add(newColumn.getWidgets().indexOf(dropTargetWidget), droppedWidget);
}
else if (dropTarget.getData() instanceof Dashboard.Column)
{
// dropped on another column
Composite filler = (Composite) newParent.getData(FILLER_KEY);
droppedComposite.moveAbove(filler);
oldColumn.getWidgets().remove(droppedWidget);
newColumn.getWidgets().add(droppedWidget);
}
else
{
throw new IllegalArgumentException();
}
listener.accept(droppedWidget);
oldParent.layout();
newParent.layout();
}
@Override
public void dragEnter(DropTargetEvent event)
{
Composite filler = (Composite) dropTarget.getData(FILLER_KEY);
(filler != null ? filler : dropTarget)
.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WIDGET_BACKGROUND));
}
@Override
public void dragLeave(DropTargetEvent event)
{
Composite filler = (Composite) dropTarget.getData(FILLER_KEY);
(filler != null ? filler : dropTarget).setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
}
}
public static final String INFO_MENU_GROUP_NAME = "info"; //$NON-NLS-1$
private static final String SELECTED_DASHBOARD_KEY = "selected-dashboard"; //$NON-NLS-1$
private static final String DELEGATE_KEY = "$delegate"; //$NON-NLS-1$
private static final String FILLER_KEY = "$filler"; //$NON-NLS-1$
@Inject
private IPreferenceStore preferences;
private DashboardResources resources;
private Composite container;
private Dashboard dashboard;
private DashboardData dashboardData;
@Override
protected String getDefaultTitle()
{
return Messages.LabelDashboard;
}
@Override
public void notifyModelUpdated()
{
this.dashboardData.clearCache();
updateWidgets();
}
@Override
public void reportingPeriodUpdated()
{
dashboardData.setDefaultReportingPeriod(getReportingPeriod());
updateWidgets();
}
@Override
protected void addButtons(ToolBar toolBar)
{
super.addButtons(toolBar);
AbstractDropDown.create(toolBar, Messages.MenuConfigureDashboards, Images.SAVE.image(), SWT.NONE, manager -> {
getClient().getDashboards().forEach(d -> {
Action action = new SimpleAction(d.getName(), a -> selectDashboard(d));
action.setChecked(d.equals(dashboard));
manager.add(action);
});
manager.add(new Separator());
manager.add(new SimpleAction(Messages.ConfigurationNew, a -> createNewDashboard(null)));
manager.add(new SimpleAction(Messages.ConfigurationDuplicate, a -> createNewDashboard(dashboard)));
manager.add(new SimpleAction(Messages.ConfigurationRename, a -> renameDashboard(dashboard)));
manager.add(new SimpleAction(Messages.ConfigurationDelete, a -> deleteDashboard(dashboard)));
});
AbstractDropDown.create(toolBar, Messages.MenuConfigureCurrentDashboard, Images.CONFIG.image(), SWT.NONE,
manager -> manager.add(
new SimpleAction(Messages.MenuNewDashboardColumn, a -> createNewColumn())));
}
@Override
protected Control createBody(Composite parent)
{
resources = new DashboardResources(parent);
dashboardData = make(DashboardData.class);
dashboardData.setDefaultReportingPeriods(getReportingPeriods());
dashboardData.setDefaultReportingPeriod(getReportingPeriod());
int indexOfSelectedDashboard = Math.max(0, preferences.getInt(SELECTED_DASHBOARD_KEY));
dashboard = getClient().getDashboards() //
.skip(indexOfSelectedDashboard) //
.findFirst().orElseGet(() -> {
Dashboard newDashboard = createDefaultDashboard();
getClient().addDashboard(newDashboard);
markDirty();
return newDashboard;
});
container = new Composite(parent, SWT.NONE);
container.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
selectDashboard(dashboard);
container.addDisposeListener(e -> preferences.setValue(SELECTED_DASHBOARD_KEY,
getClient().getDashboards().collect(Collectors.toList()).indexOf(dashboard)));
return container;
}
private void buildColumns()
{
for (Dashboard.Column column : dashboard.getColumns())
{
Composite composite = buildColumn(container, column);
for (Dashboard.Widget widget : column.getWidgets())
{
WidgetFactory factory = WidgetFactory.valueOf(widget.getType());
if (factory == null)
continue;
buildDelegate(composite, factory, widget);
}
}
}
private Composite buildColumn(Composite composite, Dashboard.Column column)
{
Composite columnControl = new Composite(composite, SWT.NONE);
columnControl.setBackground(composite.getBackground());
columnControl.setData(column);
GridLayoutFactory.fillDefaults().numColumns(1).spacing(0, 0).applyTo(columnControl);
GridDataFactory.fillDefaults().grab(true, true).applyTo(columnControl);
addDropListener(columnControl);
// Each column has an empty composite at the bottom to serve as target
// for the column context menu. A separate composite is needed because
// *all* context menus attached to nested composites are always shown.
Composite filler = new Composite(columnControl, SWT.NONE);
filler.setBackground(columnControl.getBackground());
GridDataFactory.fillDefaults().grab(true, true).applyTo(filler);
columnControl.setData(FILLER_KEY, filler);
new ContextMenu(filler, manager -> {
MenuManager subMenu = new MenuManager(Messages.MenuNewWidget);
for (WidgetFactory type : WidgetFactory.values())
subMenu.add(new SimpleAction(type.getLabel(), a -> addNewWidget(columnControl, type)));
manager.add(subMenu);
manager.add(new Separator());
manager.add(new SimpleAction(Messages.MenuAddNewDashboardColumnLeft, a -> createNewColumn(column, columnControl)));
manager.add(new SimpleAction(Messages.MenuDeleteDashboardColumn, a -> deleteColumn(columnControl)));
}).hook();
return columnControl;
}
private WidgetDelegate buildDelegate(Composite columnControl, WidgetFactory widgetType, Dashboard.Widget widget)
{
WidgetDelegate delegate = widgetType.create(widget, dashboardData);
Composite element = delegate.createControl(columnControl, resources);
element.setData(widget);
element.setData(DELEGATE_KEY, delegate);
Composite filler = (Composite) columnControl.getData(FILLER_KEY);
element.moveAbove(filler);
new ContextMenu(delegate.getTitleControl(), manager -> widgetMenuAboutToShow(manager, delegate)).hook();
InfoToolTip.attach(delegate.getTitleControl(), () -> buildToolTip(delegate));
addDragListener(element);
addDropListener(element);
for (Control child : element.getChildren())
addDragListener(child);
GridDataFactory.fillDefaults().grab(true, false).applyTo(element);
return delegate;
}
private String buildToolTip(WidgetDelegate delegate)
{
StringJoiner text = new StringJoiner("\n"); //$NON-NLS-1$
delegate.getWidgetConfigs().forEach(c -> text.add(c.getLabel()));
return text.toString();
}
private void widgetMenuAboutToShow(IMenuManager manager, WidgetDelegate delegate)
{
manager.add(new Separator(INFO_MENU_GROUP_NAME));
manager.add(new Separator("edit")); //$NON-NLS-1$
delegate.getWidgetConfigs().forEach(c -> c.menuAboutToShow(manager));
manager.add(new Separator());
manager.add(new SimpleAction(Messages.MenuDeleteWidget, a -> {
Composite composite = findCompositeFor(delegate);
if (composite == null)
throw new IllegalArgumentException();
Composite parent = composite.getParent();
Dashboard.Column column = (Dashboard.Column) parent.getData();
if (!column.getWidgets().remove(delegate.getWidget()))
throw new IllegalArgumentException();
composite.dispose();
parent.layout();
markDirty();
}));
}
private Composite findCompositeFor(WidgetDelegate delegate)
{
for (Control column : container.getChildren())
{
if (!(column instanceof Composite))
continue;
for (Control child : ((Composite) column).getChildren())
{
if (!(child instanceof Composite))
continue;
if (delegate.equals(child.getData(DELEGATE_KEY)))
return (Composite) child;
}
}
return null;
}
private void addDragListener(Control control)
{
LocalSelectionTransfer transfer = LocalSelectionTransfer.getTransfer();
DragSourceAdapter dragAdapter = new WidgetDragSourceAdapter(transfer, control);
DragSource dragSource = new DragSource(control, DND.DROP_MOVE | DND.DROP_COPY);
dragSource.setTransfer(new Transfer[] { transfer });
dragSource.addDragListener(dragAdapter);
}
private void addDropListener(Composite parent)
{
LocalSelectionTransfer transfer = LocalSelectionTransfer.getTransfer();
DropTargetAdapter dragAdapter = new WidgetDropTargetAdapter(transfer, parent, w -> markDirty());
DropTarget dropTarget = new DropTarget(parent, DND.DROP_MOVE | DND.DROP_COPY);
dropTarget.setTransfer(new Transfer[] { transfer });
dropTarget.addDropListener(dragAdapter);
}
private void updateWidgets()
{
for (Control column : container.getChildren())
{
for (Control child : ((Composite) column).getChildren())
{
WidgetDelegate delegate = (WidgetDelegate) child.getData(DELEGATE_KEY);
if (delegate != null)
delegate.update();
}
}
}
private void selectDashboard(Dashboard board)
{
this.dashboardData.setDashboard(board);
this.dashboard = board;
updateTitle(board.getName());
for (Control column : container.getChildren())
column.dispose();
buildColumns();
GridLayoutFactory.fillDefaults().numColumns(dashboard.getColumns().size()) //
.equalWidth(true).spacing(10, 10).applyTo(container);
container.layout(true);
updateWidgets();
}
private void createNewDashboard(Dashboard template)
{
Dashboard newDashboard = template != null ? template.copy() : createDefaultDashboard();
InputDialog dialog = new InputDialog(Display.getCurrent().getActiveShell(), Messages.MenuRenameDashboard,
Messages.ColumnName, newDashboard.getName(), null);
if (dialog.open() != InputDialog.OK)
return;
newDashboard.setName(dialog.getValue());
getClient().addDashboard(newDashboard);
markDirty();
selectDashboard(newDashboard);
}
private void renameDashboard(Dashboard board)
{
InputDialog dialog = new InputDialog(Display.getCurrent().getActiveShell(), Messages.MenuRenameDashboard,
Messages.ColumnName, board.getName(), null);
if (dialog.open() != InputDialog.OK)
return;
board.setName(dialog.getValue());
markDirty();
updateTitle(board.getName());
}
private void deleteDashboard(Dashboard board)
{
getClient().removeDashboard(board);
markDirty();
selectDashboard(getClient().getDashboards().findFirst().orElseGet(() -> {
Dashboard newDashboard = createDefaultDashboard();
getClient().addDashboard(newDashboard);
markDirty();
return newDashboard;
}));
}
private void addNewWidget(Composite columnControl, WidgetFactory widgetType)
{
Dashboard.Column column = (Dashboard.Column) columnControl.getData();
Dashboard.Widget widget = new Dashboard.Widget();
widget.setLabel(widgetType.getLabel());
widget.setType(widgetType.name());
column.getWidgets().add(widget);
WidgetDelegate delegate = buildDelegate(columnControl, widgetType, widget);
markDirty();
delegate.update();
columnControl.layout(true);
}
private void createNewColumn()
{
Dashboard.Column column = new Dashboard.Column();
dashboard.getColumns().add(column);
buildColumn(container, column);
GridLayoutFactory.fillDefaults().numColumns(dashboard.getColumns().size()).equalWidth(true).spacing(10, 10)
.applyTo(container);
container.layout(true);
}
private void createNewColumn(Dashboard.Column beforeColumn, Composite beforeColumnControl)
{
int index = dashboard.getColumns().indexOf(beforeColumn);
Dashboard.Column newColumn = new Dashboard.Column();
dashboard.getColumns().add(index, newColumn);
buildColumn(container, newColumn).moveAbove(beforeColumnControl);
GridLayoutFactory.fillDefaults().numColumns(dashboard.getColumns().size()).equalWidth(true).spacing(10, 10)
.applyTo(container);
container.layout(true);
}
private void deleteColumn(Composite columnControl)
{
Dashboard.Column column = (Dashboard.Column) columnControl.getData();
dashboard.getColumns().remove(column);
markDirty();
columnControl.dispose();
GridLayoutFactory.fillDefaults().numColumns(dashboard.getColumns().size()).equalWidth(true).spacing(10, 10)
.applyTo(container);
container.layout(true);
}
private Dashboard createDefaultDashboard()
{
Dashboard newDashboard = new Dashboard();
newDashboard.setName(Messages.LabelDashboard);
newDashboard.getConfiguration().put(Dashboard.Config.REPORTING_PERIOD.name(), "L1Y0"); //$NON-NLS-1$
Dashboard.Column column = new Dashboard.Column();
newDashboard.getColumns().add(column);
Dashboard.Widget widget = new Dashboard.Widget();
widget.setType(WidgetFactory.HEADING.name());
widget.setLabel(Messages.LabelKeyIndicators);
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.TTWROR.name());
widget.setLabel(WidgetFactory.TTWROR.getLabel());
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.IRR.name());
widget.setLabel(WidgetFactory.IRR.getLabel());
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.ABSOLUTE_CHANGE.name());
widget.setLabel(WidgetFactory.ABSOLUTE_CHANGE.getLabel());
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.DELTA.name());
widget.setLabel(WidgetFactory.DELTA.getLabel());
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.HEADING.name());
widget.setLabel(Messages.LabelTTWROROneDay);
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.TTWROR.name());
widget.setLabel(WidgetFactory.TTWROR.getLabel());
widget.getConfiguration().put(Dashboard.Config.REPORTING_PERIOD.name(), "T1"); //$NON-NLS-1$
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.ABSOLUTE_CHANGE.name());
widget.setLabel(WidgetFactory.ABSOLUTE_CHANGE.getLabel());
widget.getConfiguration().put(Dashboard.Config.REPORTING_PERIOD.name(), "T1"); //$NON-NLS-1$
column.getWidgets().add(widget);
column = new Dashboard.Column();
newDashboard.getColumns().add(column);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.HEADING.name());
widget.setLabel(Messages.LabelRiskIndicators);
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.MAXDRAWDOWN.name());
widget.setLabel(WidgetFactory.MAXDRAWDOWN.getLabel());
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.MAXDRAWDOWNDURATION.name());
widget.setLabel(WidgetFactory.MAXDRAWDOWNDURATION.getLabel());
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.VOLATILITY.name());
widget.setLabel(WidgetFactory.VOLATILITY.getLabel());
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.SEMIVOLATILITY.name());
widget.setLabel(WidgetFactory.SEMIVOLATILITY.getLabel());
column.getWidgets().add(widget);
column = new Dashboard.Column();
newDashboard.getColumns().add(column);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.HEADING.name());
widget.setLabel(Messages.PerformanceTabCalculation);
column.getWidgets().add(widget);
widget = new Dashboard.Widget();
widget.setType(WidgetFactory.CALCULATION.name());
widget.setLabel(WidgetFactory.CALCULATION.getLabel());
column.getWidgets().add(widget);
return newDashboard;
}
}