package name.abuchen.portfolio.ui.views;
import java.text.MessageFormat;
import java.util.function.Function;
import javax.inject.Inject;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.layout.TableColumnLayout;
import org.eclipse.jface.layout.TreeColumnLayout;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ColumnLabelProvider;
import org.eclipse.jface.viewers.ColumnPixelData;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.TreeViewerColumn;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
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.ToolBar;
import name.abuchen.portfolio.model.Account;
import name.abuchen.portfolio.model.AccountTransaction;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.CrossEntry;
import name.abuchen.portfolio.model.Portfolio;
import name.abuchen.portfolio.model.PortfolioTransaction;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.Transaction;
import name.abuchen.portfolio.model.Transaction.Unit;
import name.abuchen.portfolio.model.TransactionOwner;
import name.abuchen.portfolio.model.TransactionPair;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.CurrencyConverterImpl;
import name.abuchen.portfolio.money.ExchangeRateProviderFactory;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.snapshot.ClientPerformanceSnapshot;
import name.abuchen.portfolio.snapshot.GroupEarningsByAccount;
import name.abuchen.portfolio.snapshot.ReportingPeriod;
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.ClientFilterDropDown;
import name.abuchen.portfolio.ui.util.SimpleAction;
import name.abuchen.portfolio.ui.util.TableViewerCSVExporter;
import name.abuchen.portfolio.ui.util.TreeViewerCSVExporter;
import name.abuchen.portfolio.ui.util.viewers.Column;
import name.abuchen.portfolio.ui.util.viewers.ColumnViewerSorter;
import name.abuchen.portfolio.ui.util.viewers.ShowHideColumnHelper;
import name.abuchen.portfolio.ui.views.columns.NoteColumn;
public class PerformanceView extends AbstractHistoricView
{
@Inject
private ExchangeRateProviderFactory factory;
private ClientFilterDropDown clientFilter;
private TreeViewer calculation;
private StatementOfAssetsViewer snapshotStart;
private StatementOfAssetsViewer snapshotEnd;
private TableViewer earnings;
private TableViewer earningsByAccount;
private TableViewer taxes;
private TableViewer fees;
@Override
protected String getDefaultTitle()
{
return Messages.LabelPerformanceCalculation;
}
@Override
protected void addButtons(ToolBar toolBar)
{
super.addButtons(toolBar);
this.clientFilter = new ClientFilterDropDown(toolBar, getClient(), getPreferenceStore(),
filter -> notifyModelUpdated());
new ExportDropDown(toolBar); // NOSONAR
}
@Override
public void reportingPeriodUpdated()
{
ReportingPeriod period = getReportingPeriod();
CurrencyConverter converter = new CurrencyConverterImpl(factory, getClient().getBaseCurrency());
Client filteredClient = clientFilter.getSelectedFilter().filter(getClient());
ClientPerformanceSnapshot snapshot = new ClientPerformanceSnapshot(filteredClient, converter, period);
try
{
calculation.getTree().setRedraw(false);
calculation.setInput(snapshot);
calculation.expandAll();
calculation.getTree().getParent().layout();
}
finally
{
calculation.getTree().setRedraw(true);
}
snapshotStart.setInput(snapshot.getStartClientSnapshot(), clientFilter.getSelectedFilter());
snapshotEnd.setInput(snapshot.getEndClientSnapshot(), clientFilter.getSelectedFilter());
earnings.setInput(snapshot.getEarnings());
earningsByAccount.setInput(new GroupEarningsByAccount(snapshot).getItems());
taxes.setInput(snapshot.getTaxes());
fees.setInput(snapshot.getFees());
}
@Override
public void notifyModelUpdated()
{
reportingPeriodUpdated();
}
@Override
protected Control createBody(Composite parent)
{
// result tabs
CTabFolder folder = new CTabFolder(parent, SWT.BORDER);
createCalculationItem(folder, Messages.PerformanceTabCalculation);
snapshotStart = createStatementOfAssetsItem(folder, Messages.PerformanceTabAssetsAtStart);
snapshotEnd = createStatementOfAssetsItem(folder, Messages.PerformanceTabAssetsAtEnd);
earnings = createTransactionViewer(folder, Messages.PerformanceTabEarnings);
createEarningsByAccountsItem(folder, Messages.PerformanceTabEarningsByAccount);
taxes = createTransactionViewer(folder, Messages.PerformanceTabTaxes);
fees = createTransactionViewer(folder, Messages.PerformanceTabFees);
folder.setSelection(0);
reportingPeriodUpdated();
return folder;
}
private StatementOfAssetsViewer createStatementOfAssetsItem(CTabFolder folder, String title)
{
StatementOfAssetsViewer viewer = make(StatementOfAssetsViewer.class);
Control control = viewer.createControl(folder);
CTabItem item = new CTabItem(folder, SWT.NONE);
item.setText(title);
item.setControl(control);
return viewer;
}
private void createCalculationItem(CTabFolder folder, String title)
{
Composite container = new Composite(folder, SWT.NONE);
TreeColumnLayout layout = new TreeColumnLayout();
container.setLayout(layout);
calculation = new TreeViewer(container, SWT.FULL_SELECTION);
final Font boldFont = JFaceResources.getFontRegistry().getBold(container.getFont().getFontData()[0].getName());
TreeViewerColumn column = new TreeViewerColumn(calculation, SWT.NONE);
column.getColumn().setText(Messages.ColumnLable);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
if (element instanceof ClientPerformanceSnapshot.Category)
{
ClientPerformanceSnapshot.Category cat = (ClientPerformanceSnapshot.Category) element;
return cat.getLabel();
}
else if (element instanceof ClientPerformanceSnapshot.Position)
{
ClientPerformanceSnapshot.Position pos = (ClientPerformanceSnapshot.Position) element;
return pos.getLabel();
}
return null;
}
@Override
public Image getImage(Object element)
{
if (element instanceof ClientPerformanceSnapshot.Category)
{
return Images.CATEGORY.image();
}
else if (element instanceof ClientPerformanceSnapshot.Position)
{
ClientPerformanceSnapshot.Position position = (ClientPerformanceSnapshot.Position) element;
if (position.getSecurity() != null)
{
ClientPerformanceSnapshot snapshot = ((PerformanceContentProvider) calculation
.getContentProvider()).getSnapshot();
boolean hasHoldings = snapshot.getEndClientSnapshot().getPositionsByVehicle()
.get(position.getSecurity()) != null;
return hasHoldings ? Images.SECURITY.image() : Images.SECURITY_RETIRED.image();
}
else
{
return null;
}
}
return null;
}
@Override
public Font getFont(Object element)
{
if (element instanceof ClientPerformanceSnapshot.Category)
return boldFont;
return null;
}
});
layout.setColumnData(column.getColumn(), new ColumnPixelData(350));
column = new TreeViewerColumn(calculation, SWT.RIGHT);
column.getColumn().setText(Messages.ColumnValue);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
if (element instanceof ClientPerformanceSnapshot.Category)
{
ClientPerformanceSnapshot.Category cat = (ClientPerformanceSnapshot.Category) element;
return Values.Money.format(cat.getValuation(), getClient().getBaseCurrency());
}
else if (element instanceof ClientPerformanceSnapshot.Position)
{
ClientPerformanceSnapshot.Position pos = (ClientPerformanceSnapshot.Position) element;
return Values.Money.format(pos.getValuation(), getClient().getBaseCurrency());
}
return null;
}
@Override
public Font getFont(Object element)
{
if (element instanceof ClientPerformanceSnapshot.Category)
return boldFont;
return null;
}
});
layout.setColumnData(column.getColumn(), new ColumnPixelData(80));
calculation.getTree().setHeaderVisible(true);
calculation.getTree().setLinesVisible(true);
calculation.setContentProvider(new PerformanceContentProvider());
CTabItem item = new CTabItem(folder, SWT.NONE);
item.setText(title);
item.setControl(container);
hookContextMenu(calculation.getTree(), this::fillContextMenu);
}
private void fillContextMenu(IMenuManager manager) // NOSONAR
{
Object selection = ((IStructuredSelection) calculation.getSelection()).getFirstElement();
if (!(selection instanceof ClientPerformanceSnapshot.Position))
return;
Security security = ((ClientPerformanceSnapshot.Position) selection).getSecurity();
new SecurityContextMenu(this).menuAboutToShow(manager, security);
}
private TableViewer createTransactionViewer(CTabFolder folder, String title)
{
Composite container = new Composite(folder, SWT.NONE);
TableColumnLayout layout = new TableColumnLayout();
container.setLayout(layout);
TableViewer transactionViewer = new TableViewer(container, SWT.FULL_SELECTION);
ShowHideColumnHelper support = new ShowHideColumnHelper(PerformanceView.class.getSimpleName() + "@2" + title, //$NON-NLS-1$
getPreferenceStore(), transactionViewer, layout);
Column column = new Column(Messages.ColumnDate, SWT.None, 80);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
return Values.Date.format(((TransactionPair<?>) element).getTransaction().getDate());
}
});
column.setSorter(ColumnViewerSorter.create(e -> ((TransactionPair<?>) e).getTransaction().getDate()), SWT.UP);
support.addColumn(column);
column = new Column(Messages.ColumnTransactionType, SWT.LEFT, 100);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
Transaction t = ((TransactionPair<?>) element).getTransaction();
return t instanceof AccountTransaction ? ((AccountTransaction) t).getType().toString()
: ((PortfolioTransaction) t).getType().toString();
}
});
column.setSorter(ColumnViewerSorter.create(e -> {
Transaction t = ((TransactionPair<?>) e).getTransaction();
return t instanceof AccountTransaction ? ((AccountTransaction) t).getType().toString()
: ((PortfolioTransaction) t).getType().toString();
}));
support.addColumn(column);
column = new Column(Messages.ColumnAmount, SWT.RIGHT, 80);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
return Values.Money.format(((TransactionPair<?>) element).getTransaction().getMonetaryAmount(),
getClient().getBaseCurrency());
}
});
column.setSorter(ColumnViewerSorter.create(e -> ((TransactionPair<?>) e).getTransaction().getMonetaryAmount()));
support.addColumn(column);
addTaxesColumn(support);
addFeesColumn(support);
addSecurityColumn(support);
addPortfolioColumn(support);
addAccountColumn(support);
column = new NoteColumn();
column.setEditingSupport(null);
support.addColumn(column);
support.createColumns();
transactionViewer.getTable().setHeaderVisible(true);
transactionViewer.getTable().setLinesVisible(true);
transactionViewer.setContentProvider(ArrayContentProvider.getInstance());
CTabItem item = new CTabItem(folder, SWT.NONE);
item.setText(title);
item.setControl(container);
return transactionViewer;
}
private void addTaxesColumn(ShowHideColumnHelper support)
{
Column column = new Column(Messages.ColumnTaxes, SWT.RIGHT, 80);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
Transaction t = ((TransactionPair<?>) element).getTransaction();
if (t instanceof AccountTransaction)
{
AccountTransaction at = (AccountTransaction) t;
switch (at.getType())
{
case TAXES:
return Values.Money.format(at.getMonetaryAmount(), getClient().getBaseCurrency());
case TAX_REFUND:
return Values.Money.format(at.getMonetaryAmount().multiply(-1),
getClient().getBaseCurrency());
default:
// do nothing -> print unit sum
}
}
return Values.Money.format(t.getUnitSum(Unit.Type.TAX), getClient().getBaseCurrency());
}
});
column.setSorter(ColumnViewerSorter
.create(e -> ((TransactionPair<?>) e).getTransaction().getUnitSum(Unit.Type.TAX)));
support.addColumn(column);
}
private void addFeesColumn(ShowHideColumnHelper support)
{
Column column = new Column(Messages.ColumnFees, SWT.RIGHT, 80);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
Transaction t = ((TransactionPair<?>) element).getTransaction();
if (t instanceof AccountTransaction)
{
AccountTransaction at = (AccountTransaction) t;
switch (at.getType())
{
case FEES:
return Values.Money.format(t.getMonetaryAmount(), getClient().getBaseCurrency());
case FEES_REFUND:
return Values.Money.format(t.getMonetaryAmount().multiply(-1),
getClient().getBaseCurrency());
default:
// do nothing --> print unit sum
}
}
return Values.Money.format(t.getUnitSum(Unit.Type.FEE), getClient().getBaseCurrency());
}
});
column.setSorter(ColumnViewerSorter
.create(e -> ((TransactionPair<?>) e).getTransaction().getUnitSum(Unit.Type.FEE)));
support.addColumn(column);
}
private void addSecurityColumn(ShowHideColumnHelper support)
{
Column column = new Column(Messages.ColumnSecurity, SWT.LEFT, 250);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
Security security = ((TransactionPair<?>) element).getTransaction().getSecurity();
return security != null ? security.getName() : null;
}
@Override
public Image getImage(Object element)
{
Security security = ((TransactionPair<?>) element).getTransaction().getSecurity();
return security != null ? Images.SECURITY.image() : null;
}
});
column.setSorter(ColumnViewerSorter.create(e -> {
Security security = ((TransactionPair<?>) e).getTransaction().getSecurity();
return security != null ? security.getName() : null;
}));
support.addColumn(column);
}
private void addPortfolioColumn(ShowHideColumnHelper support)
{
Column column = new Column(Messages.ColumnPortfolio, SWT.LEFT, 100);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
Portfolio portfolio = ((TransactionPair<?>) element).getOwner() instanceof Portfolio
? (Portfolio) ((TransactionPair<?>) element).getOwner() : null;
return portfolio != null ? portfolio.getName() : null;
}
@Override
public Image getImage(Object element)
{
Portfolio portfolio = ((TransactionPair<?>) element).getOwner() instanceof Portfolio
? (Portfolio) ((TransactionPair<?>) element).getOwner() : null;
return portfolio != null ? Images.PORTFOLIO.image() : null;
}
});
column.setSorter(ColumnViewerSorter.create(e -> {
Portfolio portfolio = ((TransactionPair<?>) e).getOwner() instanceof Portfolio
? (Portfolio) ((TransactionPair<?>) e).getOwner() : null;
return portfolio != null ? portfolio.getName() : null;
}));
support.addColumn(column);
}
private void addAccountColumn(ShowHideColumnHelper support)
{
Column column = new Column(Messages.ColumnAccount, SWT.LEFT, 100);
Function<Object, Account> getAccount = element -> {
TransactionPair<?> pair = (TransactionPair<?>) element;
if (pair.getOwner() instanceof Account)
return (Account) pair.getOwner();
CrossEntry crossEntry = pair.getTransaction().getCrossEntry();
if (crossEntry == null)
return null;
TransactionOwner<?> other = crossEntry.getCrossOwner(pair.getTransaction());
return other instanceof Account ? ((Account) other) : null;
};
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
Account account = getAccount.apply(element);
return account != null ? account.getName() : null;
}
@Override
public Image getImage(Object element)
{
Account account = getAccount.apply(element);
return account != null ? Images.ACCOUNT.image() : null;
}
});
column.setSorter(ColumnViewerSorter.create(e -> {
Account account = getAccount.apply(e);
return account != null ? account.getName() : null;
}));
support.addColumn(column);
}
private void createEarningsByAccountsItem(CTabFolder folder, String title)
{
Composite container = new Composite(folder, SWT.NONE);
TableColumnLayout layout = new TableColumnLayout();
container.setLayout(layout);
earningsByAccount = new TableViewer(container, SWT.FULL_SELECTION);
ShowHideColumnHelper support = new ShowHideColumnHelper(PerformanceView.class.getSimpleName() + "@byaccounts", //$NON-NLS-1$
getPreferenceStore(), earningsByAccount, layout);
Column column = new Column(Messages.ColumnSource, SWT.LEFT, 400);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
GroupEarningsByAccount.Item item = (GroupEarningsByAccount.Item) element;
return item.getAccount().getName();
}
@Override
public Image getImage(Object element)
{
return Images.ACCOUNT.image();
}
});
column.setSorter(ColumnViewerSorter.create(GroupEarningsByAccount.Item.class, "account")); //$NON-NLS-1$
support.addColumn(column);
column = new Column(Messages.ColumnAmount, SWT.RIGHT, 80);
column.setLabelProvider(new ColumnLabelProvider()
{
@Override
public String getText(Object element)
{
GroupEarningsByAccount.Item item = (GroupEarningsByAccount.Item) element;
return Values.Money.format(item.getSum(), getClient().getBaseCurrency());
}
});
column.setSorter(ColumnViewerSorter.create(GroupEarningsByAccount.Item.class, "sum")); //$NON-NLS-1$
support.addColumn(column);
support.createColumns();
earningsByAccount.getTable().setHeaderVisible(true);
earningsByAccount.getTable().setLinesVisible(true);
earningsByAccount.setContentProvider(ArrayContentProvider.getInstance());
CTabItem item = new CTabItem(folder, SWT.NONE);
item.setText(title);
item.setControl(container);
}
private static class PerformanceContentProvider implements ITreeContentProvider
{
private ClientPerformanceSnapshot snapshot;
private ClientPerformanceSnapshot.Category[] categories;
@Override
public void inputChanged(Viewer viewer, Object oldInput, Object newInput)
{
if (newInput == null)
{
this.snapshot = null;
this.categories = new ClientPerformanceSnapshot.Category[0];
}
else if (newInput instanceof ClientPerformanceSnapshot)
{
this.snapshot = (ClientPerformanceSnapshot) newInput;
this.categories = snapshot.getCategories().toArray(new ClientPerformanceSnapshot.Category[0]);
}
else
{
throw new IllegalArgumentException("Unsupported type: " + newInput.getClass().getName()); //$NON-NLS-1$
}
}
@Override
public Object[] getElements(Object inputElement)
{
return this.categories;
}
@Override
public Object[] getChildren(Object parentElement)
{
if (parentElement instanceof ClientPerformanceSnapshot.Category)
return ((ClientPerformanceSnapshot.Category) parentElement).getPositions()
.toArray(new ClientPerformanceSnapshot.Position[0]);
return new Object[0];
}
@Override
public Object getParent(Object element)
{
if (element instanceof ClientPerformanceSnapshot.Position)
{
for (ClientPerformanceSnapshot.Category c : categories)
{
if (c.getPositions().contains(element))
return c;
}
}
return null;
}
@Override
public boolean hasChildren(Object element)
{
return element instanceof ClientPerformanceSnapshot.Category
&& !((ClientPerformanceSnapshot.Category) element).getPositions().isEmpty();
}
@Override
public void dispose()
{
// no resources to dispose
}
public ClientPerformanceSnapshot getSnapshot()
{
return snapshot;
}
}
private final class ExportDropDown extends AbstractDropDown
{
private ExportDropDown(ToolBar toolBar)
{
super(toolBar, Messages.MenuExportData, Images.EXPORT.image(), SWT.NONE);
}
@Override
public void menuAboutToShow(IMenuManager manager)
{
final String fileSuffix = ".csv"; //$NON-NLS-1$
manager.add(new SimpleAction(MessageFormat.format(Messages.LabelExport, Messages.PerformanceTabCalculation),
a -> new TreeViewerCSVExporter(calculation)
.export(Messages.PerformanceTabCalculation + fileSuffix)));
manager.add(new SimpleAction(
MessageFormat.format(Messages.LabelExport, Messages.PerformanceTabAssetsAtStart),
a -> new TableViewerCSVExporter(snapshotStart.getTableViewer())
.export(Messages.PerformanceTabAssetsAtStart + fileSuffix)));
manager.add(new SimpleAction(MessageFormat.format(Messages.LabelExport, Messages.PerformanceTabAssetsAtEnd),
a -> new TableViewerCSVExporter(snapshotEnd.getTableViewer())
.export(Messages.PerformanceTabAssetsAtEnd + fileSuffix)));
manager.add(new SimpleAction(MessageFormat.format(Messages.LabelExport, Messages.PerformanceTabEarnings),
a -> new TableViewerCSVExporter(earnings)
.export(Messages.PerformanceTabEarnings + fileSuffix)));
manager.add(new SimpleAction(
MessageFormat.format(Messages.LabelExport, Messages.PerformanceTabEarningsByAccount),
a -> new TableViewerCSVExporter(earningsByAccount)
.export(Messages.PerformanceTabEarningsByAccount + fileSuffix)));
manager.add(new SimpleAction(MessageFormat.format(Messages.LabelExport, Messages.PerformanceTabTaxes),
a -> new TableViewerCSVExporter(taxes).export(Messages.PerformanceTabTaxes + fileSuffix)));
manager.add(new SimpleAction(MessageFormat.format(Messages.LabelExport, Messages.PerformanceTabFees),
a -> new TableViewerCSVExporter(fees).export(Messages.PerformanceTabFees + fileSuffix)));
}
}
}