package name.abuchen.portfolio.ui.views;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import org.eclipse.e4.core.di.extensions.Preference;
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.layout.TableColumnLayout;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.FontDescriptor;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.window.ToolTip;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Shell;
import name.abuchen.portfolio.model.Account;
import name.abuchen.portfolio.model.Adaptable;
import name.abuchen.portfolio.model.Annotated;
import name.abuchen.portfolio.model.Attributable;
import name.abuchen.portfolio.model.Classification;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.InvestmentVehicle;
import name.abuchen.portfolio.model.Named;
import name.abuchen.portfolio.model.Portfolio;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.Taxonomy;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.ExchangeRate;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.snapshot.AssetCategory;
import name.abuchen.portfolio.snapshot.AssetPosition;
import name.abuchen.portfolio.snapshot.ClientSnapshot;
import name.abuchen.portfolio.snapshot.GroupByTaxonomy;
import name.abuchen.portfolio.snapshot.PortfolioSnapshot;
import name.abuchen.portfolio.snapshot.ReportingPeriod;
import name.abuchen.portfolio.snapshot.SecurityPosition;
import name.abuchen.portfolio.snapshot.filter.ClientFilter;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceRecord;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceSnapshot;
import name.abuchen.portfolio.ui.AbstractFinanceView;
import name.abuchen.portfolio.ui.Images;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.UIConstants;
import name.abuchen.portfolio.ui.dnd.SecurityDragListener;
import name.abuchen.portfolio.ui.dnd.SecurityTransfer;
import name.abuchen.portfolio.ui.util.AttributeComparator;
import name.abuchen.portfolio.ui.util.LabelOnly;
import name.abuchen.portfolio.ui.util.viewers.Column;
import name.abuchen.portfolio.ui.util.viewers.ColumnEditingSupport;
import name.abuchen.portfolio.ui.util.viewers.ColumnEditingSupport.MarkDirtyListener;
import name.abuchen.portfolio.ui.util.viewers.ColumnViewerSorter;
import name.abuchen.portfolio.ui.util.viewers.OptionLabelProvider;
import name.abuchen.portfolio.ui.util.viewers.ReportingPeriodColumnOptions;
import name.abuchen.portfolio.ui.util.viewers.SharesLabelProvider;
import name.abuchen.portfolio.ui.util.viewers.ShowHideColumnHelper;
import name.abuchen.portfolio.ui.util.viewers.StringEditingSupport;
import name.abuchen.portfolio.ui.views.columns.AttributeColumn;
import name.abuchen.portfolio.ui.views.columns.IsinColumn;
import name.abuchen.portfolio.ui.views.columns.NameColumn;
import name.abuchen.portfolio.ui.views.columns.NameColumn.NameColumnLabelProvider;
import name.abuchen.portfolio.ui.views.columns.NoteColumn;
import name.abuchen.portfolio.ui.views.columns.TaxonomyColumn;
@SuppressWarnings("restriction")
public class StatementOfAssetsViewer
{
@Inject
private IPreferenceStore preference;
private boolean useIndirectQuotation = false;
private TableViewer assets;
private Font boldFont;
private Menu contextMenu;
private AbstractFinanceView owner;
private ShowHideColumnHelper support;
private final Client client;
private ClientFilter clientFilter = ClientFilter.NO_FILTER;
private ClientSnapshot clientSnapshot;
private PortfolioSnapshot portfolioSnapshot;
private Taxonomy taxonomy;
@Inject
public StatementOfAssetsViewer(AbstractFinanceView owner, Client client)
{
this.owner = owner;
this.client = client;
}
@Inject
public void setUseIndirectQuotation(
@Preference(value = UIConstants.Preferences.USE_INDIRECT_QUOTATION) boolean useIndirectQuotation)
{
this.useIndirectQuotation = useIndirectQuotation;
if (assets != null)
assets.refresh();
}
public Control createControl(Composite parent)
{
Control control = createColumns(parent);
this.assets.getTable().addDisposeListener(e -> StatementOfAssetsViewer.this.widgetDisposed());
return control;
}
@PostConstruct
private void loadTaxonomy() // NOSONAR
{
String taxonomyId = preference.getString(this.getClass().getSimpleName());
if (taxonomyId != null)
{
for (Taxonomy t : client.getTaxonomies())
{
if (taxonomyId.equals(t.getId()))
{
this.taxonomy = t;
break;
}
}
}
if (this.taxonomy == null && !client.getTaxonomies().isEmpty())
this.taxonomy = client.getTaxonomies().get(0);
}
private Control createColumns(Composite parent) // NOSONAR
{
Composite container = new Composite(parent, SWT.NONE);
TableColumnLayout layout = new TableColumnLayout();
container.setLayout(layout);
assets = new TableViewer(container, SWT.FULL_SELECTION);
ColumnViewerToolTipSupport.enableFor(assets, ToolTip.NO_RECREATE);
ColumnEditingSupport.prepare(assets);
support = new ShowHideColumnHelper(StatementOfAssetsViewer.class.getName(), client, preference, assets, layout);
Column column = new Column("0", Messages.ColumnSharesOwned, SWT.RIGHT, 80); //$NON-NLS-1$
column.setLabelProvider(new SharesLabelProvider() // NOSONAR
{
@Override
public Long getValue(Object e)
{
Element element = (Element) e;
return element.isSecurity() ? element.getSecurityPosition().getShares() : null;
}
});
column.setComparator(new ElementComparator(new AttributeComparator(
e -> ((Element) e).isSecurity() ? ((Element) e).getSecurityPosition().getShares() : null)));
support.addColumn(column);
column = new NameColumn("1"); //$NON-NLS-1$
column.setLabelProvider(new NameColumnLabelProvider() // NOSONAR
{
@Override
public String getText(Object e)
{
if (((Element) e).isGroupByTaxonomy())
return Messages.LabelTotalSum;
return super.getText(e);
}
@Override
public Font getFont(Object e)
{
return ((Element) e).isGroupByTaxonomy() || ((Element) e).isCategory() ? boldFont : null;
}
@Override
public Image getImage(Object e)
{
if (((Element) e).isCategory())
return null;
return super.getImage(e);
}
});
column.setEditingSupport(new StringEditingSupport(Named.class, "name") //$NON-NLS-1$
{
@Override
public boolean canEdit(Object element)
{
boolean isCategory = ((Element) element).isCategory();
boolean isUnassignedCategory = isCategory && Classification.UNASSIGNED_ID
.equals(((Element) element).getCategory().getClassification().getId());
return !isUnassignedCategory ? super.canEdit(element) : false;
}
}.setMandatory(true).addListener(new MarkDirtyListener(this.owner)));
column.getSorter().wrap(ElementComparator::new);
support.addColumn(column);
column = new Column("2", Messages.ColumnTicker, SWT.None, 60); //$NON-NLS-1$
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
return element.isSecurity() ? element.getSecurity().getTickerSymbol() : null;
}
});
column.setComparator(new ElementComparator(new AttributeComparator(
e -> ((Element) e).isSecurity() ? ((Element) e).getSecurity().getTickerSymbol() : null)));
support.addColumn(column);
column = new Column("12", Messages.ColumnWKN, SWT.None, 60); //$NON-NLS-1$
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
return element.isSecurity() ? element.getSecurity().getWkn() : null;
}
});
column.setComparator(new ElementComparator(new AttributeComparator(
e -> ((Element) e).isSecurity() ? ((Element) e).getSecurity().getWkn() : null)));
column.setVisible(false);
support.addColumn(column);
column = new IsinColumn("3"); //$NON-NLS-1$
column.getEditingSupport().addListener(new MarkDirtyListener(this.owner));
column.getSorter().wrap(ElementComparator::new);
column.setVisible(false);
support.addColumn(column);
column = new Column("4", Messages.ColumnQuote, SWT.RIGHT, 60); //$NON-NLS-1$
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (!element.isSecurity())
return null;
Security security = element.getSecurity();
return Values.Quote.format(security.getCurrencyCode(),
element.getSecurityPosition().getPrice().getValue(), client.getBaseCurrency());
}
});
column.setComparator(new ElementComparator(new AttributeComparator(e -> {
Element element = (Element) e;
if (!element.isSecurity())
return null;
return Money.of(element.getSecurity().getCurrencyCode(),
element.getSecurityPosition().getPrice().getValue());
})));
support.addColumn(column);
column = new Column("qdate", Messages.ColumnDateOfQuote, SWT.LEFT, 80); //$NON-NLS-1$
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
return element.isSecurity() ? Values.Date.format(element.getSecurityPosition().getPrice().getTime())
: null;
}
});
column.setComparator(new ElementComparator(new AttributeComparator(e -> ((Element) e).isSecurity()
? ((Element) e).getSecurityPosition().getPrice().getTime() : null)));
column.setVisible(false);
support.addColumn(column);
column = new Column("5", Messages.ColumnMarketValue, SWT.RIGHT, 80); //$NON-NLS-1$
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
return Values.Money.format(element.getValuation(), client.getBaseCurrency());
}
@Override
public Font getFont(Object e)
{
return ((Element) e).isGroupByTaxonomy() || ((Element) e).isCategory() ? boldFont : null;
}
});
column.setSorter(ColumnViewerSorter.create(Element.class, "valuation").wrap(ElementComparator::new)); //$NON-NLS-1$
support.addColumn(column);
column = new Column("6", Messages.ColumnShareInPercent, SWT.RIGHT, 80); //$NON-NLS-1$
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (element.isGroupByTaxonomy())
return Values.Percent.format(1d);
if (element.isCategory())
return Values.Percent.format(element.getCategory().getShare());
else
return Values.Percent.format(element.getPosition().getShare());
}
@Override
public Font getFont(Object e)
{
return ((Element) e).isGroupByTaxonomy() || ((Element) e).isCategory() ? boldFont : null;
}
});
column.setSorter(ColumnViewerSorter.create(Element.class, "valuation").wrap(ElementComparator::new)); //$NON-NLS-1$
support.addColumn(column);
column = new Column("7", Messages.ColumnPurchasePrice, SWT.RIGHT, 60); //$NON-NLS-1$
column.setDescription(Messages.ColumnPurchasePrice_Description);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (element.isSecurity())
{
Money purchasePrice = element.getSecurityPosition().getFIFOPurchasePrice();
return Values.Money.formatNonZero(purchasePrice, client.getBaseCurrency());
}
return null;
}
});
column.setComparator(new ElementComparator(new AttributeComparator(e -> ((Element) e).isSecurity()
? ((Element) e).getSecurityPosition().getFIFOPurchasePrice() : null)));
column.setVisible(false);
support.addColumn(column);
column = new Column("8", Messages.ColumnPurchaseValue, SWT.RIGHT, 80); //$NON-NLS-1$
column.setDescription(Messages.ColumnPurchaseValue_Description);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
Money purchaseValue = element.getFIFOPurchaseValue();
return Values.Money.formatNonZero(purchaseValue, client.getBaseCurrency());
}
@Override
public Font getFont(Object e)
{
return ((Element) e).isGroupByTaxonomy() || ((Element) e).isCategory() ? boldFont : null;
}
});
column.setVisible(false);
column.setSorter(ColumnViewerSorter.create(Element.class, "FIFOPurchaseValue") //$NON-NLS-1$
.wrap(ElementComparator::new));
support.addColumn(column);
column = new Column("9", Messages.ColumnProfitLoss, SWT.RIGHT, 80); //$NON-NLS-1$
column.setLabelProvider(new ColumnLabelProvider() // NOSONAR
{
@Override
public String getText(Object e)
{
Money profitLoss = ((Element) e).getProfitLoss();
return Values.Money.formatNonZero(profitLoss, client.getBaseCurrency());
}
@Override
public Color getForeground(Object e)
{
Money profitLoss = ((Element) e).getProfitLoss();
return Display.getCurrent()
.getSystemColor(profitLoss.isNegative() ? SWT.COLOR_DARK_RED : SWT.COLOR_DARK_GREEN);
}
@Override
public Image getImage(Object e)
{
Money profitLoss = ((Element) e).getProfitLoss();
if (profitLoss.isZero())
return null;
return profitLoss.isNegative() ? Images.RED_ARROW.image() : Images.GREEN_ARROW.image();
}
@Override
public Font getFont(Object e)
{
return ((Element) e).isGroupByTaxonomy() || ((Element) e).isCategory() ? boldFont : null;
}
});
column.setVisible(false);
column.setSorter(ColumnViewerSorter.create(Element.class, "profitLoss").wrap(ElementComparator::new)); //$NON-NLS-1$
support.addColumn(column);
column = new NoteColumn();
column.getEditingSupport().addListener(new MarkDirtyListener(this.owner));
column.getSorter().wrap(ElementComparator::new);
support.addColumn(column);
// create a modifiable copy as all menus share the same list of
// reporting periods
List<ReportingPeriod> options = new ArrayList<>(owner.getPart().loadReportingPeriods());
addPerformanceColumns(options);
addDividendColumns(options);
addTaxonomyColumns();
addAttributeColumns();
addCurrencyColumns();
support.createColumns();
assets.getTable().setHeaderVisible(true);
assets.getTable().setLinesVisible(true);
assets.setContentProvider(new StatementOfAssetsContentProvider());
assets.addDragSupport(DND.DROP_MOVE, //
new Transfer[] { SecurityTransfer.getTransfer() }, //
new SecurityDragListener(assets));
LocalResourceManager resources = new LocalResourceManager(JFaceResources.getResources(), assets.getTable());
boldFont = resources.createFont(FontDescriptor.createFrom(assets.getTable().getFont()).setStyle(SWT.BOLD));
return container;
}
private void addPerformanceColumns(List<ReportingPeriod> options)
{
Column column = new Column("ttwror", Messages.ColumnTWROR, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnTTWROR_Option, options));
column.setGroupLabel(Messages.GroupLabelPerformance);
column.setDescription(Messages.ColumnTWROR_Description);
column.setLabelProvider(
new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getTrueTimeWeightedRateOfReturn));
column.setVisible(false);
support.addColumn(column);
column = new Column("irr", Messages.ColumnIRR, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnIRRPerformanceOption, options));
column.setMenuLabel(Messages.ColumnIRR_MenuLabel);
column.setGroupLabel(Messages.GroupLabelPerformance);
column.setLabelProvider(new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getIrr));
column.setVisible(false);
support.addColumn(column);
column = new Column("capitalgains", Messages.ColumnCapitalGains, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnCapitalGains_Option, options));
column.setGroupLabel(Messages.GroupLabelPerformance);
column.setDescription(Messages.ColumnCapitalGains_Description);
column.setLabelProvider(new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getCapitalGainsOnHoldings));
column.setVisible(false);
support.addColumn(column);
column = new Column("capitalgains%", Messages.ColumnCapitalGainsPercent, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnCapitalGainsPercent_Option, options));
column.setGroupLabel(Messages.GroupLabelPerformance);
column.setDescription(Messages.ColumnCapitalGainsPercent_Description);
column.setLabelProvider(
new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getCapitalGainsOnHoldingsPercent));
column.setVisible(false);
support.addColumn(column);
column = new Column("delta", Messages.ColumnAbsolutePerformance_MenuLabel, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnAbsolutePerformance_Option, options));
column.setGroupLabel(Messages.GroupLabelPerformance);
column.setDescription(Messages.ColumnAbsolutePerformance_Description);
column.setLabelProvider(new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getDelta));
column.setVisible(false);
support.addColumn(column);
column = new Column("delta%", Messages.ColumnAbsolutePerformancePercent_MenuLabel, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnAbsolutePerformancePercent_Option, options));
column.setGroupLabel(Messages.GroupLabelPerformance);
column.setDescription(Messages.ColumnAbsolutePerformancePercent_Description);
column.setLabelProvider(new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getDeltaPercent));
column.setVisible(false);
support.addColumn(column);
}
private void addDividendColumns(List<ReportingPeriod> options)
{
Column column = new Column("sumdiv", Messages.ColumnDividendSum, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnDividendSum + " {0}", options)); //$NON-NLS-1$
column.setGroupLabel(Messages.GroupLabelDividends);
column.setMenuLabel(Messages.ColumnDividendSum_MenuLabel);
column.setLabelProvider(new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getSumOfDividends, false));
column.setVisible(false);
support.addColumn(column);
column = new Column("d%", Messages.ColumnDividendTotalRateOfReturn, SWT.RIGHT, 80); //$NON-NLS-1$
column.setOptions(new ReportingPeriodColumnOptions(Messages.ColumnDividendTotalRateOfReturn + " {0}", options)); //$NON-NLS-1$
column.setGroupLabel(Messages.GroupLabelDividends);
column.setDescription(Messages.ColumnDividendTotalRateOfReturn_Description);
column.setLabelProvider(
new ReportingPeriodLabelProvider(SecurityPerformanceRecord::getTotalRateOfReturnDiv, false));
column.setVisible(false);
support.addColumn(column);
}
private void addAttributeColumns()
{
client.getSettings() //
.getAttributeTypes() //
.filter(a -> a.supports(Security.class)) //
.forEach(attribute -> {
Column column = new AttributeColumn(attribute);
column.setVisible(false);
if (column.getSorter() != null)
column.getSorter().wrap(ElementComparator::new);
column.getEditingSupport().addListener(new MarkDirtyListener(this.owner));
support.addColumn(column);
});
}
private void addTaxonomyColumns()
{
for (Taxonomy t : client.getTaxonomies())
{
Column column = new TaxonomyColumn(t);
column.setVisible(false);
if (column.getSorter() != null)
column.getSorter().wrap(ElementComparator::new);
support.addColumn(column);
}
}
private void addCurrencyColumns() // NOSONAR
{
Column column = new Column("baseCurrency", Messages.ColumnCurrency, SWT.LEFT, 80); //$NON-NLS-1$
column.setGroupLabel(Messages.ColumnForeignCurrencies);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (!element.isPosition())
return null;
return element.getPosition().getInvestmentVehicle().getCurrencyCode();
}
});
column.setComparator(new ElementComparator(new AttributeComparator(e -> ((Element) e).isPosition()
? ((Element) e).getPosition().getInvestmentVehicle().getCurrencyCode() : null)));
column.setVisible(false);
support.addColumn(column);
column = new Column("exchangeRate", Messages.ColumnExchangeRate, SWT.RIGHT, 80); //$NON-NLS-1$
column.setGroupLabel(Messages.ColumnForeignCurrencies);
column.setLabelProvider(new ColumnLabelProvider() // NOSONAR
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (!element.isPosition())
return null;
String baseCurrency = element.getPosition().getInvestmentVehicle().getCurrencyCode();
CurrencyConverter converter = getCurrencyConverter();
ExchangeRate rate = converter.getRate(getDate(), baseCurrency);
if (useIndirectQuotation)
rate = rate.inverse();
return Values.ExchangeRate.format(rate.getValue());
}
@Override
public String getToolTipText(Object e)
{
String text = getText(e);
if (text == null)
return null;
String term = getCurrencyConverter().getTermCurrency();
String base = ((Element) e).getPosition().getInvestmentVehicle().getCurrencyCode();
return text + ' ' + (useIndirectQuotation ? base + '/' + term : term + '/' + base);
}
});
column.setVisible(false);
support.addColumn(column);
column = new Column("marketValueBaseCurrency", //$NON-NLS-1$
Messages.ColumnMarketValue + Messages.BaseCurrencyCue, SWT.RIGHT, 80);
column.setDescription(Messages.ColumnMarketValueBaseCurrency);
column.setGroupLabel(Messages.ColumnForeignCurrencies);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (!element.isPosition())
return null;
return Values.Money.format(element.getPosition().getPosition().calculateValue(),
client.getBaseCurrency());
}
});
column.setComparator(new ElementComparator(new AttributeComparator(e -> ((Element) e).isPosition()
? ((Element) e).getPosition().getPosition().calculateValue() : null)));
column.setVisible(false);
support.addColumn(column);
column = new Column("purchaseValueBaseCurrency", //$NON-NLS-1$
Messages.ColumnPurchaseValue + Messages.BaseCurrencyCue, SWT.RIGHT, 80);
column.setDescription(Messages.ColumnPurchaseValueBaseCurrency);
column.setGroupLabel(Messages.ColumnForeignCurrencies);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (!element.isPosition())
return null;
return Values.Money.formatNonZero(element.getPosition().getPosition().getFIFOPurchaseValue(),
client.getBaseCurrency());
}
});
column.setComparator(new ElementComparator(new AttributeComparator(e -> ((Element) e).isPosition()
? ((Element) e).getPosition().getPosition().getFIFOPurchaseValue() : null)));
column.setVisible(false);
support.addColumn(column);
column = new Column("profitLossBaseCurrency", //$NON-NLS-1$
Messages.ColumnProfitLoss + Messages.BaseCurrencyCue, SWT.RIGHT, 80);
column.setDescription(Messages.ColumnProfitLossBaseCurrency);
column.setGroupLabel(Messages.ColumnForeignCurrencies);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object e)
{
Element element = (Element) e;
if (!element.isPosition())
return null;
return Values.Money.formatNonZero(element.getPosition().getPosition().getProfitLoss(),
client.getBaseCurrency());
}
});
column.setComparator(new ElementComparator(new AttributeComparator(e -> ((Element) e).isPosition()
? ((Element) e).getPosition().getPosition().getProfitLoss() : null)));
column.setVisible(false);
support.addColumn(column);
}
public void hookMenuListener(IMenuManager manager, final AbstractFinanceView view)
{
Element element = (Element) ((IStructuredSelection) assets.getSelection()).getFirstElement();
if (element == null)
return;
if (element.isAccount())
{
new AccountContextMenu(view).menuAboutToShow(manager, element.getAccount(), null);
}
else if (element.isSecurity())
{
Portfolio portfolio = portfolioSnapshot != null ? portfolioSnapshot.getSource() : null;
new SecurityContextMenu(view).menuAboutToShow(manager, element.getSecurity(), portfolio);
}
}
public TableViewer getTableViewer()
{
return assets;
}
public void showConfigMenu(Shell shell)
{
if (contextMenu == null)
{
MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$
menuMgr.setRemoveAllWhenShown(true);
menuMgr.addMenuListener(StatementOfAssetsViewer.this::menuAboutToShow);
contextMenu = menuMgr.createContextMenu(shell);
}
contextMenu.setVisible(true);
}
private void menuAboutToShow(IMenuManager manager) // NOSONAR
{
manager.add(new LabelOnly(Messages.LabelTaxonomies));
for (final Taxonomy t : client.getTaxonomies())
{
Action action = new Action(t.getName())
{
@Override
public void run()
{
taxonomy = t;
if (clientSnapshot != null)
internalSetInput(clientSnapshot.groupByTaxonomy(taxonomy));
else
internalSetInput(portfolioSnapshot.groupByTaxonomy(taxonomy));
}
};
action.setChecked(t.equals(taxonomy));
manager.add(action);
}
manager.add(new Separator());
manager.add(new LabelOnly(Messages.LabelColumns));
support.menuAboutToShow(manager);
}
public void showSaveMenu(Shell shell)
{
support.showSaveMenu(shell);
}
public void setInput(ClientSnapshot snapshot, ClientFilter filter)
{
this.clientSnapshot = snapshot;
this.portfolioSnapshot = null;
this.clientFilter = Objects.requireNonNull(filter);
internalSetInput(snapshot.groupByTaxonomy(taxonomy));
}
public void setInput(PortfolioSnapshot snapshot)
{
this.clientSnapshot = null;
this.portfolioSnapshot = snapshot;
this.clientFilter = ClientFilter.NO_FILTER;
internalSetInput(snapshot != null ? snapshot.groupByTaxonomy(taxonomy) : null);
}
public ShowHideColumnHelper getColumnHelper()
{
return support;
}
private void internalSetInput(GroupByTaxonomy grouping)
{
assets.getTable().setRedraw(false);
try
{
assets.setInput(grouping);
assets.refresh();
}
finally
{
assets.getTable().setRedraw(true);
}
}
private CurrencyConverter getCurrencyConverter()
{
if (clientSnapshot != null)
return clientSnapshot.getCurrencyConverter();
else if (portfolioSnapshot != null)
return portfolioSnapshot.getCurrencyConverter();
else
throw new IllegalArgumentException();
}
private LocalDate getDate()
{
if (clientSnapshot != null)
return clientSnapshot.getTime();
else if (portfolioSnapshot != null)
return portfolioSnapshot.getTime();
else
return null;
}
private void widgetDisposed()
{
if (taxonomy != null)
preference.setValue(this.getClass().getSimpleName(), taxonomy.getId());
if (contextMenu != null)
contextMenu.dispose();
}
public static class Element implements Adaptable
{
/**
* The sortOrder is used to separate asset categories and asset
* positions and thereby sort positions only within a category even
* though there is a flat list of elements. See
* {@link ElementComparator}.
*/
private final int sortOrder;
private GroupByTaxonomy groupByTaxonomy;
private AssetCategory category;
private AssetPosition position;
private Map<ReportingPeriod, SecurityPerformanceRecord> performance = new HashMap<>();
private Element(AssetCategory category, int sortOrder)
{
this.category = category;
this.sortOrder = sortOrder;
}
private Element(AssetPosition position, int sortOrder)
{
this.position = position;
this.sortOrder = sortOrder;
}
private Element(GroupByTaxonomy groupByTaxonomy, int sortOrder)
{
this.groupByTaxonomy = groupByTaxonomy;
this.sortOrder = sortOrder;
}
public int getSortOrder()
{
return sortOrder;
}
public void setPerformance(ReportingPeriod period, SecurityPerformanceRecord record)
{
performance.put(period, record);
}
public SecurityPerformanceRecord getPerformance(ReportingPeriod period)
{
return performance.get(period);
}
public boolean isGroupByTaxonomy()
{
return groupByTaxonomy != null;
}
public boolean isCategory()
{
return category != null;
}
public boolean isPosition()
{
return position != null;
}
public boolean isSecurity()
{
return position != null && position.getSecurity() != null;
}
public boolean isAccount()
{
return position != null && position.getInvestmentVehicle() instanceof Account;
}
public AssetCategory getCategory()
{
return category;
}
public AssetPosition getPosition()
{
return position;
}
public SecurityPosition getSecurityPosition()
{
return position != null ? position.getPosition() : null;
}
public Security getSecurity()
{
return position != null ? position.getSecurity() : null;
}
public Account getAccount()
{
return isAccount() ? (Account) position.getInvestmentVehicle() : null;
}
public Money getValuation()
{
if (position != null)
return position.getValuation();
else if (category != null)
return category.getValuation();
else
return groupByTaxonomy.getValuation();
}
public Money getFIFOPurchaseValue()
{
if (position != null)
return position.getFIFOPurchaseValue();
else if (category != null)
return category.getFIFOPurchaseValue();
else
return groupByTaxonomy.getFIFOPurchaseValue();
}
public Money getProfitLoss()
{
if (position != null)
return position.getProfitLoss();
else if (category != null)
return category.getProfitLoss();
else
return groupByTaxonomy.getProfitLoss();
}
@Override
public <T> T adapt(Class<T> type) // NOSONAR
{
if (type == Security.class || type == Attributable.class)
{
return type.cast(getSecurity());
}
else if (type == Named.class || type == Annotated.class)
{
if (isSecurity())
return type.cast(getSecurity());
else if (isAccount())
return type.cast(getAccount());
else if (isCategory())
return type.cast(getCategory().getClassification());
else
return null;
}
else if (type == InvestmentVehicle.class)
{
if (isSecurity())
return type.cast(getSecurity());
else if (isAccount())
return type.cast(getAccount());
else
return null;
}
else
{
return null;
}
}
}
public static class ElementComparator implements Comparator<Object>
{
private Comparator<Object> comparator;
public ElementComparator(Comparator<Object> wrapped)
{
this.comparator = wrapped;
}
@Override
public int compare(Object o1, Object o2)
{
int a = ((Element) o1).getSortOrder();
int b = ((Element) o2).getSortOrder();
if (a != b)
{
int direction = ColumnViewerSorter.SortingContext.getSortDirection();
return direction == SWT.DOWN ? a - b : b - a;
}
return comparator.compare(o1, o2);
}
}
private static class StatementOfAssetsContentProvider implements IStructuredContentProvider
{
private Element[] elements;
@Override
public void inputChanged(Viewer v, Object oldInput, Object newInput)
{
if (newInput == null)
{
this.elements = new Element[0];
}
else if (newInput instanceof GroupByTaxonomy)
{
this.elements = flatten((GroupByTaxonomy) newInput);
}
else
{
throw new IllegalArgumentException("Unsupported type: " + newInput.getClass().getName()); //$NON-NLS-1$
}
}
private Element[] flatten(GroupByTaxonomy categories)
{
// when flattening, assign sortOrder to keep the tree structure for
// sorting (only positions within a category are sorted)
int sortOrder = 0;
List<Element> answer = new ArrayList<>();
for (AssetCategory cat : categories.asList())
{
answer.add(new Element(cat, sortOrder));
sortOrder++;
for (AssetPosition p : cat.getPositions())
answer.add(new Element(p, sortOrder));
sortOrder++;
}
answer.add(new Element(categories, ++sortOrder));
return answer.toArray(new Element[0]);
}
@Override
public Object[] getElements(Object inputElement)
{
return this.elements;
}
@Override
public void dispose()
{
// no resources to dispose
}
}
private final class ReportingPeriodLabelProvider extends OptionLabelProvider<ReportingPeriod>
{
private boolean showColorAndArrows;
private Function<SecurityPerformanceRecord, Object> valueProvider;
public ReportingPeriodLabelProvider(Function<SecurityPerformanceRecord, Object> valueProvider)
{
this(valueProvider, true);
}
public ReportingPeriodLabelProvider(Function<SecurityPerformanceRecord, Object> valueProvider,
boolean showUpAndDownArrows)
{
this.valueProvider = valueProvider;
this.showColorAndArrows = showUpAndDownArrows;
}
private Object getValue(Object e, ReportingPeriod option)
{
Element element = (Element) e;
if (element.isSecurity())
{
calculatePerformance(element, option);
SecurityPerformanceRecord record = element.getPerformance(option);
// record is null if there are no transactions for the security
// in the given period
return record != null ? valueProvider.apply(record) : null;
}
return null;
}
@Override
public String getText(Object e, ReportingPeriod option)
{
Object value = getValue(e, option);
if (value == null)
return null;
if (value instanceof Money)
return Values.Money.format((Money) value, client.getBaseCurrency());
else if (value instanceof Double)
return Values.Percent.format((Double) value);
return null;
}
@Override
public Color getForeground(Object e, ReportingPeriod option)
{
if (!showColorAndArrows)
return null;
Object value = getValue(e, option);
if (value == null)
return null;
double doubleValue = 0;
if (value instanceof Money)
doubleValue = ((Money) value).getAmount();
else if (value instanceof Double)
doubleValue = (Double) value;
if (doubleValue < 0)
return Display.getCurrent().getSystemColor(SWT.COLOR_DARK_RED);
else if (doubleValue > 0)
return Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GREEN);
else
return null;
}
@Override
public Image getImage(Object element, ReportingPeriod option)
{
if (!showColorAndArrows)
return null;
Object value = getValue(element, option);
if (value == null)
return null;
double doubleValue = 0;
if (value instanceof Money)
doubleValue = ((Money) value).getAmount();
else if (value instanceof Double)
doubleValue = (Double) value;
if (doubleValue > 0)
return Images.GREEN_ARROW.image();
if (doubleValue < 0)
return Images.RED_ARROW.image();
return null;
}
private void calculatePerformance(Element element, ReportingPeriod period)
{
// already calculated?
if (element.getPerformance(period) != null)
return;
if (clientSnapshot == null && portfolioSnapshot == null)
return;
Client filteredClient = clientFilter.filter(client);
SecurityPerformanceSnapshot sps;
if (clientSnapshot != null)
{
sps = SecurityPerformanceSnapshot.create(filteredClient, clientSnapshot.getCurrencyConverter(), period);
}
else
{
sps = SecurityPerformanceSnapshot.create(filteredClient, portfolioSnapshot.getCurrencyConverter(),
portfolioSnapshot.getSource(), period);
}
Map<Security, SecurityPerformanceRecord> map = sps.getRecords().stream()
.collect(Collectors.toMap(SecurityPerformanceRecord::getSecurity, r -> r));
Arrays.stream(((StatementOfAssetsContentProvider) assets.getContentProvider()).elements) // NOSONAR
.filter(Element::isSecurity)
.forEach(e -> e.setPerformance(period, map.get(e.getSecurity())));
}
}
}