de.codesourcery.eve.skills.ui.components.impl.AssetListComponent.java Source code

Java tutorial

Introduction

Here is the source code for de.codesourcery.eve.skills.ui.components.impl.AssetListComponent.java

Source

/**
 * Copyright 2004-2009 Tobias Gierke <tobias.gierke@code-sourcery.de>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package de.codesourcery.eve.skills.ui.components.impl;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagLayout;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.ListSelectionModel;
import javax.swing.SortOrder;
import javax.swing.SwingConstants;
import javax.swing.RowSorter.SortKey;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableCellRenderer;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import de.codesourcery.eve.skills.assets.IAssetManager;
import de.codesourcery.eve.skills.datamodel.Asset;
import de.codesourcery.eve.skills.datamodel.AssetList;
import de.codesourcery.eve.skills.datamodel.ICharacter;
import de.codesourcery.eve.skills.datamodel.ILocation;
import de.codesourcery.eve.skills.db.datamodel.InventoryCategory;
import de.codesourcery.eve.skills.db.datamodel.InventoryGroup;
import de.codesourcery.eve.skills.db.datamodel.InventoryType;
import de.codesourcery.eve.skills.ui.components.AbstractComponent;
import de.codesourcery.eve.skills.ui.components.ComponentWrapper;
import de.codesourcery.eve.skills.ui.components.ICharacterSelectionProviderAware;
import de.codesourcery.eve.skills.ui.components.ISelectionListener;
import de.codesourcery.eve.skills.ui.components.ISelectionProvider;
import de.codesourcery.eve.skills.ui.components.impl.TotalItemVolumeComponent.IDataProvider;
import de.codesourcery.eve.skills.ui.model.AbstractViewFilter;
import de.codesourcery.eve.skills.ui.model.DefaultComboBoxModel;
import de.codesourcery.eve.skills.ui.model.IViewFilter;
import de.codesourcery.eve.skills.ui.model.TableColumnBuilder;
import de.codesourcery.eve.skills.ui.model.impl.AssetListTableModel;
import de.codesourcery.eve.skills.ui.renderer.FixedBooleanTableCellRenderer;
import de.codesourcery.eve.skills.ui.utils.ImprovedSplitPane;
import de.codesourcery.eve.skills.ui.utils.Misc;
import de.codesourcery.eve.skills.ui.utils.PlainTextTransferable;
import de.codesourcery.eve.skills.ui.utils.PopupMenuBuilder;
import de.codesourcery.eve.skills.ui.utils.UITask;

public class AssetListComponent extends AbstractComponent implements ICharacterSelectionProviderAware {
    private static final Logger log = Logger.getLogger(AssetListComponent.class);

    private static final Collection<Asset> NO_ASSETS = Collections.emptyList();

    private static final TypeWrapper ANY_TYPE = new TypeWrapper() {
        @Override
        public String toString() {
            return "ANY TYPE";
        }
    };

    private final PopupMenuBuilder popupMenuBuilder = new PopupMenuBuilder();

    private static class TypeWrapper {

        private InventoryType type;

        public TypeWrapper() {
        }

        public TypeWrapper(InventoryType type) {
            if (type == null) {
                throw new IllegalArgumentException("type cannot be NULL");
            }
            this.type = type;
        }

        @Override
        public String toString() {
            return type.getName();
        }
    }

    // swing
    private JTable table;

    private JCheckBox ignorePackaging = new JCheckBox();
    private JCheckBox ignoreLocations = new JCheckBox();
    private JCheckBox mergeAssetsByType = new JCheckBox();

    // filtering by name
    private final JTextField nameFilter = new JTextField();

    // location filtering
    private final JCheckBox filterByLocation = new JCheckBox("Filter by location?");
    private final JComboBox locationComboBox = new JComboBox();

    // category filter
    private final JCheckBox filterByCategory = new JCheckBox("Filter by category?");
    private final JComboBox categoryComboBox = new JComboBox();

    private final DefaultComboBoxModel<InventoryCategory> categoryModel = new DefaultComboBoxModel<InventoryCategory>();

    private final TotalItemVolumeComponent<Asset> selectedVolume = new TotalItemVolumeComponent<Asset>(
            new IDataProvider<Asset>() {

                @Override
                public int getQuantity(Asset obj) {
                    return obj.getQuantity();
                }

                @Override
                public double getVolumePerUnit(Asset obj) {
                    return obj.getType().getVolume();
                }
            });

    // group filter
    private final JCheckBox filterByGroup = new JCheckBox("Filter by group?");
    private final JComboBox groupComboBox = new JComboBox();
    private final DefaultComboBoxModel<InventoryGroup> groupModel = new DefaultComboBoxModel<InventoryGroup>();

    private final DefaultComboBoxModel<ILocation> locationModel = new DefaultComboBoxModel<ILocation>();

    private final JCheckBox filterByType = new JCheckBox("Filter by item type?");
    private final JComboBox typeComboBox = new JComboBox();
    private final DefaultComboBoxModel<TypeWrapper> typeModel = new DefaultComboBoxModel<TypeWrapper>();

    private ActionListener actionListener = new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
            AssetListComponent.this.actionPerformed(e);
        }
    };

    // other stuff
    @Resource(name = "asset-manager")
    private IAssetManager assetManager;

    private ISelectionProvider<ICharacter> selectionProvider;
    private final AssetListModel model = new AssetListModel(new AssetList());

    private final ISelectionListener<ICharacter> characterSelectionListener = new ISelectionListener<ICharacter>() {

        @Override
        public void selectionChanged(ICharacter selected) {
            updateAssetListModel();
        }
    };

    private IViewFilter<Asset> viewFilter = new AbstractViewFilter<Asset>() {

        @Override
        public boolean isHiddenUnfiltered(Asset asset) {

            if (!StringUtils.isBlank(nameFilter.getText())) {
                if (!asset.getType().getName().toLowerCase().contains((nameFilter.getText().toLowerCase()))) {
                    return true;
                }
            }

            if (filterByLocation.isSelected()) {

                final ILocation selected = (ILocation) locationComboBox.getSelectedItem();

                if (selected != null && !matchesLocation(selected, asset)) {
                    return true;
                }
            }

            if (filterByType.isSelected()) {

                final TypeWrapper selected = (TypeWrapper) typeComboBox.getSelectedItem();

                if (selected != null && selected != ANY_TYPE && !matchesInventoryType(selected, asset)) {
                    return true;
                }
            }

            if (filterByCategory.isSelected()) {
                final InventoryCategory selected = (InventoryCategory) categoryComboBox.getSelectedItem();
                if (selected != null && !matchesInventoryCategory(selected, asset)) {
                    return true;
                }
            }

            if (filterByGroup.isSelected()) {
                final InventoryGroup selected = (InventoryGroup) groupComboBox.getSelectedItem();
                if (selected != null && !matchesInventoryGroup(selected, asset)) {
                    return true;
                }
            }

            return false;
        }

        protected boolean matchesInventoryGroup(InventoryGroup group, Asset asset) {
            return asset.getType().getGroup().getId().equals(group.getId());
        }

        protected boolean matchesInventoryCategory(InventoryCategory cat, Asset asset) {
            return asset.getType().getGroup().getCategory().getId().equals(cat.getId());
        }

        protected boolean matchesInventoryType(TypeWrapper required, Asset asset) {
            if (required == ANY_TYPE) {
                return false;
            }

            return required.type.equals(asset.getType());
        }

        protected boolean matchesLocation(ILocation required, Asset asset) {
            if (required.isAnyLocation()) {
                return true;
            }

            return asset.hasLocation(required);
        }
    };

    private void actionPerformed(ActionEvent e) {
        final Object src = e.getSource();
        if (src == ignorePackaging || src == ignoreLocations || src == mergeAssetsByType) {
            updateAssetListModel();
        } else if (src == filterByLocation || src == locationComboBox) {
            model.viewFilterChanged();
        } else if (src == filterByType || src == typeComboBox) {
            model.viewFilterChanged();
            if (src == filterByType) {
                updateComboBoxModels(model.getAssetList());
            }
        } else if (src == filterByCategory || src == categoryComboBox) {
            model.viewFilterChanged();
            if (src == filterByCategory) {
                updateComboBoxModels(model.getAssetList());
            }
        } else if (src == filterByGroup || src == groupComboBox) {
            model.viewFilterChanged();
            if (src == filterByGroup) {
                updateComboBoxModels(model.getAssetList());
            }
        }
    }

    public AssetListComponent() {
        super();
        registerChildren(this.selectedVolume);
    }

    private final class AssetListModel extends AssetListTableModel {

        public AssetListModel(AssetList assetList) {
            super(assetList,
                    new TableColumnBuilder().add("Item type").add("Category").add("Group").add("Location")
                            .add("Quantity", Integer.class).add("Packaged ?", Boolean.class)
                            .add("Stored in", String.class));
        }

        @Override
        protected Object getColumnValue(Asset asset, int columnIndex) {
            switch (columnIndex) {
            case 0:
                final InventoryType type = asset.getType();
                return type != null ? asset.getType().getName() : "<no type>";
            case 1:
                return asset.getType().getGroup().getCategory().getCategoryName();
            case 2:
                return asset.getType().getGroup().getGroupName();
            case 3:
                return locationToString(asset);
            case 4:
                return asset.getQuantity();
            case 5:
                return asset.isPackaged();
            case 6:
                if (asset.getContainer() != null) {
                    return asset.getContainer().getType().getName();
                }
                return "";
            default:
                throw new IllegalArgumentException("Unhandled column index: " + columnIndex);
            }
        }

    }

    protected static final String locationToString(Asset asset) {

        if (asset.hasMultipleLocations()) {
            return "<multiple locations>";
        }

        final ILocation loc = asset.getLocation();
        return loc == null ? "<no location>" : loc.getDisplayName();
    }

    @Override
    protected void onAttachHook(IComponentCallback callback) {

        this.selectionProvider.addSelectionListener(characterSelectionListener);

        updateAssetListModel();
    }

    @Override
    protected void onDetachHook() {
        if (this.selectionProvider != null) {
            this.selectionProvider.removeSelectionListener(characterSelectionListener);
        }
    }

    @Override
    protected void disposeHook() {

        table = null;
        model.dispose();
        if (this.selectionProvider != null) {
            this.selectionProvider.removeSelectionListener(characterSelectionListener);
        }
    }

    @Override
    protected JPanel createPanel() {

        // Merge controls.
        final JPanel mergeControlsPanel = new JPanel();
        mergeControlsPanel.setLayout(new GridBagLayout());
        mergeControlsPanel.setBorder(BorderFactory.createTitledBorder("Merging"));

        int y = 0;

        // merge by type
        mergeAssetsByType.setSelected(true);
        mergeAssetsByType.addActionListener(actionListener);

        mergeControlsPanel.add(mergeAssetsByType, constraints(0, y).anchorWest().end());
        mergeControlsPanel.add(new JLabel("Merge assets by type", SwingConstants.LEFT),
                constraints(1, y++).width(2).end());

        // "ignore different packaging"
        ignorePackaging.setSelected(true);
        ignorePackaging.addActionListener(actionListener);

        mergeControlsPanel.add(new JLabel(""), constraints(0, y).anchorWest().end());
        mergeControlsPanel.add(ignorePackaging, constraints(1, y).anchorWest().end());
        final JLabel label1 = new JLabel("Merge different packaging", SwingConstants.RIGHT);
        mergeControlsPanel.add(label1, constraints(2, y++).end());

        // "ignore different locations"
        ignoreLocations.setSelected(true);
        ignoreLocations.addActionListener(actionListener);

        mergeControlsPanel.add(new JLabel(""), constraints(0, y).anchorWest().end());
        mergeControlsPanel.add(ignoreLocations, constraints(1, y).anchorWest().end());
        final JLabel label2 = new JLabel("Merge different locations", SwingConstants.RIGHT);
        mergeControlsPanel.add(label2, constraints(2, y++).end());

        linkComponentEnabledStates(mergeAssetsByType, ignoreLocations, ignorePackaging, label1, label2);

        /*
         * Filter controls.
         */

        final JPanel filterControlsPanel = new JPanel();
        filterControlsPanel.setLayout(new GridBagLayout());
        filterControlsPanel.setBorder(BorderFactory.createTitledBorder("Filters"));

        y = 0;
        // filter by location combo box
        filterByLocation.addActionListener(actionListener);
        locationComboBox.addActionListener(actionListener);

        filterByLocation.setSelected(false);
        linkComponentEnabledStates(filterByLocation, locationComboBox);

        locationComboBox.setRenderer(new LocationRenderer());
        locationComboBox.setPreferredSize(new Dimension(150, 20));
        locationComboBox.setModel(locationModel);

        filterControlsPanel.add(filterByLocation, constraints(0, y).end());
        filterControlsPanel.add(locationComboBox, constraints(1, y++).end());

        // filter by type combo box
        filterByType.addActionListener(actionListener);
        typeComboBox.addActionListener(actionListener);

        filterByType.setSelected(false);

        linkComponentEnabledStates(filterByType, typeComboBox);

        typeComboBox.setPreferredSize(new Dimension(150, 20));
        typeComboBox.setModel(typeModel);

        filterControlsPanel.add(filterByType, constraints(0, y).end());
        filterControlsPanel.add(typeComboBox, constraints(1, y++).end());

        // filter by item category combobox
        filterByCategory.addActionListener(actionListener);
        categoryComboBox.addActionListener(actionListener);
        categoryComboBox.setRenderer(new DefaultListCellRenderer() {

            @Override
            public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
                    boolean cellHasFocus) {

                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

                setText(getDisplayName((InventoryCategory) value));
                setEnabled(categoryComboBox.isEnabled());
                return this;
            }
        });

        filterByCategory.setSelected(false);
        linkComponentEnabledStates(filterByCategory, categoryComboBox);

        categoryComboBox.setPreferredSize(new Dimension(150, 20));
        categoryComboBox.setModel(categoryModel);

        filterControlsPanel.add(filterByCategory, constraints(0, y).end());
        filterControlsPanel.add(categoryComboBox, constraints(1, y++).end());

        // filter by item group combobox
        filterByGroup.addActionListener(actionListener);
        groupComboBox.addActionListener(actionListener);

        filterByGroup.setSelected(false);

        linkComponentEnabledStates(filterByGroup, groupComboBox);

        groupComboBox.setPreferredSize(new Dimension(150, 20));
        groupComboBox.setModel(groupModel);

        filterControlsPanel.add(filterByGroup, constraints(0, y).end());
        filterControlsPanel.add(groupComboBox, constraints(1, y++).end());

        /*
         * Table panel.
         */

        table = new JTable() {

            @Override
            public TableCellRenderer getCellRenderer(int row, int column) {

                // subclassing hack is needed because table
                // returns different renderes depending on column type
                final TableCellRenderer result = super.getCellRenderer(row, column);

                return new TableCellRenderer() {

                    @Override
                    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
                            boolean hasFocus, int row, int column) {

                        final Component comp = result.getTableCellRendererComponent(table, value, isSelected,
                                hasFocus, row, column);

                        final int modelRow = table.convertRowIndexToModel(row);
                        final Asset asset = model.getRow(modelRow);

                        final StringBuilder label = new StringBuilder("<HTML><BODY>");
                        label.append(asset.getItemId() + " - flags: " + asset.getFlags() + "<BR>");
                        if (asset.hasMultipleLocations()) {
                            label.append("<BR>");
                            for (ILocation loc : asset.getLocations()) {
                                label.append(loc.getDisplayName()).append("<BR>");
                            }
                        }

                        label.append("</BODY></HTML>");
                        ((JComponent) comp).setToolTipText(label.toString());

                        return comp;
                    }
                };
            }
        };

        model.setViewFilter(this.viewFilter);

        table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {

            @Override
            public void valueChanged(ListSelectionEvent e) {
                updateSelectedVolume();
            }
        });

        FixedBooleanTableCellRenderer.attach(table);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        table.setModel(model);
        table.setBorder(BorderFactory.createLineBorder(Color.BLACK));
        table.setRowSorter(model.getRowSorter());

        popupMenuBuilder.addItem("Refine...", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                final List<Asset> assets = getSelectedAssets();
                if (assets == null || assets.isEmpty()) {
                    return;
                }

                final ICharacter c = selectionProvider.getSelectedItem();
                final RefiningComponent comp = new RefiningComponent(c);
                comp.setItemsToRefine(assets);
                ComponentWrapper.wrapComponent("Refining", comp).setVisible(true);
            }

            @Override
            public boolean isEnabled() {
                return table.getSelectedRow() != -1;
            }
        });

        popupMenuBuilder.addItem("Copy selection to clipboard (text)", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                final List<Asset> assets = getSelectedAssets();
                if (assets == null || assets.isEmpty()) {
                    return;
                }

                new PlainTextTransferable(toPlainText(assets)).putOnClipboard();
            }

            @Override
            public boolean isEnabled() {
                return table.getSelectedRow() != -1;
            }
        });

        popupMenuBuilder.addItem("Copy selection to clipboard (CSV)", new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                final List<Asset> assets = getSelectedAssets();
                if (assets == null || assets.isEmpty()) {
                    return;
                }

                final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

                clipboard.setContents(new PlainTextTransferable(toCsv(assets)), null);
            }

            @Override
            public boolean isEnabled() {
                return table.getSelectedRow() != -1;
            }
        });

        table.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        this.popupMenuBuilder.attach(table);

        final JScrollPane scrollPane = new JScrollPane(table);

        /*
         * Name filter
         */

        final JPanel nameFilterPanel = new JPanel();
        nameFilterPanel.setLayout(new GridBagLayout());
        nameFilterPanel.setBorder(BorderFactory.createTitledBorder("Filter by name"));
        nameFilterPanel.setPreferredSize(new Dimension(150, 70));
        nameFilter.setColumns(10);
        nameFilter.getDocument().addDocumentListener(new DocumentListener() {

            @Override
            public void changedUpdate(DocumentEvent e) {
                model.viewFilterChanged();
            }

            @Override
            public void insertUpdate(DocumentEvent e) {
                model.viewFilterChanged();
            }

            @Override
            public void removeUpdate(DocumentEvent e) {
                model.viewFilterChanged();
            }
        });

        nameFilterPanel.add(nameFilter, constraints(0, 0).resizeHorizontally().end());
        final JButton clearButton = new JButton("Clear");
        clearButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                nameFilter.setText(null);
            }
        });
        nameFilterPanel.add(clearButton, constraints(1, 0).noResizing().end());

        // Selected volume
        final JPanel selectedVolumePanel = this.selectedVolume.getPanel();

        // add control panels to result panel
        final JPanel topPanel = new JPanel();
        topPanel.setLayout(new GridBagLayout());

        topPanel.add(mergeControlsPanel, constraints(0, 0).height(2).weightX(0).anchorWest().end());
        topPanel.add(filterControlsPanel, constraints(1, 0).height(2).anchorWest().weightX(0).end());
        topPanel.add(nameFilterPanel, constraints(2, 0).height(1).anchorWest().useRemainingWidth().end());
        topPanel.add(selectedVolumePanel, constraints(2, 1).height(1).anchorWest().useRemainingWidth().end());

        final JSplitPane splitPane = new ImprovedSplitPane(JSplitPane.VERTICAL_SPLIT, topPanel, scrollPane);

        splitPane.setDividerLocation(0.3d);

        final JPanel content = new JPanel();
        content.setLayout(new GridBagLayout());
        content.add(splitPane, constraints().resizeBoth().useRemainingSpace().end());

        return content;
    }

    private void updateSelectedVolume() {
        final int[] viewRows = table.getSelectedRows();
        if (ArrayUtils.isEmpty(viewRows)) {
            selectedVolume.setItems(NO_ASSETS);
            return;
        }

        final List<Asset> selected = new ArrayList<Asset>();

        for (int viewRow : viewRows) {
            final int modelRow = table.convertRowIndexToModel(viewRow);
            selected.add(model.getRow(modelRow));
        }
        selectedVolume.setItems(selected);
    }

    private List<Asset> getSelectedAssets() {
        final int[] selection = table.getSelectedRows();
        if (ArrayUtils.isEmpty(selection)) {
            return null;
        }

        final List<Asset> assets = new ArrayList<Asset>();
        for (int viewRow : selection) {
            final int modelRow = model.convertViewRowToModel(viewRow);
            assets.add(model.getRow(modelRow));
        }
        return assets;
    }

    private String toCsv(List<Asset> assets) {
        // sort ascending by item name
        Collections.sort(assets, new Comparator<Asset>() {

            @Override
            public int compare(Asset arg0, Asset arg1) {
                return arg0.getType().getName().compareTo(arg1.getType().getName());
            }
        });

        final StringBuilder result = new StringBuilder();
        result.append(pad("Item;Location;Packaged;Quantity"));
        for (Asset a : assets) {

            final String loc;
            if (a.hasMultipleLocations()) {
                loc = "multiple locations";
            } else {
                loc = a.getLocation().getDisplayName();
            }

            final String line = pad(a.getType().getName(), 45) + ";" + pad(loc, 20) + ";"
                    + (a.isPackaged() ? "X" : " ") + ";" + pad(Integer.toString(a.getQuantity())) + "\n";

            result.append(line);
        }

        return result.toString();
    }

    private String pad(String s) {
        return StringUtils.rightPad(s, 12);
    }

    private String pad(String s, int len) {
        return StringUtils.rightPad(s, len);
    }

    private String toPlainText(List<Asset> assets) {

        // sort ascending by item name
        Collections.sort(assets, new Comparator<Asset>() {

            @Override
            public int compare(Asset arg0, Asset arg1) {
                return arg0.getType().getName().compareTo(arg1.getType().getName());
            }
        });

        final StringBuilder result = new StringBuilder();
        result.append(createLine("Item"));
        for (Asset a : assets) {
            result.append(createLine(a.getType().getName()));
        }

        return result.toString();
    }

    private String createLine(String col1) {
        return StringUtils.rightPad(col1, 45) + "\n";
    }

    protected void updateAssetListModel() {

        final ICharacter character = this.selectionProvider.getSelectedItem();

        if (character == null) {
            log.debug("populateModel(): No character set");
            model.clear();
            return;
        }

        if (assetManager == null) {
            throw new IllegalStateException("No asset manager set ?");
        }

        log.info("createModel(): Fetching assets for character " + character);

        final UITask updateTask = new UITask() {

            private AssetList assets;

            @Override
            public String getId() {
                return "fetch_assets_for_" + character.getName();
            }

            @Override
            public void run() throws Exception {

                displayStatus("Fetching assets for " + character.getName());

                /*
                 * TODO: THREADING BUG !!!! AssetManager  internally triggers lazy
                 * TODO: THREADING BUG !!!!  fetching and the current thread is NOT the EDT here....
                 */
                assets = assetManager.getAssets(character);

                log.info("createModel(): Got " + assets.size() + " assets.");

                if (mergeAssetsByType.isSelected()) {
                    long delta = -System.currentTimeMillis();
                    assets = assets.createMergedAssetListByType(ignorePackaging.isSelected(),
                            ignoreLocations.isSelected());
                    delta += System.currentTimeMillis();
                    log.info("createModel(): after merging = " + assets.size() + " [ " + delta + " millis");
                }
            }

            @Override
            public void successHook() throws Exception {
                updateAssetListModel(assets);
            }

            @Override
            public void failureHook(Throwable t) throws Exception {
                log.error("updateAssetListModel(): Caught ", t);
                displayError("Unable to retrieve asset list from server: " + t.getMessage(), t);
                assets = new AssetList();
            }
        };

        // do stuff
        submitFutureTask(updateTask, true);
    }

    protected static <T> List<T> toList(Collection<T> data) {
        return new ArrayList<T>(data);
    }

    protected void updateAssetListModel(AssetList assets) {

        final int selectedRow = table.getSelectedRow();
        final Asset oldRowSelection = (selectedRow != -1)
                ? model.getRow(model.getRowSorter().convertRowIndexToModel(selectedRow))
                : null;

        if (log.isTraceEnabled()) {
            log.trace("updateAssetListModel(): selected = " + oldRowSelection);
        }

        model.setAssetList(assets, !this.mergeAssetsByType.isSelected());

        updateComboBoxModels(assets);

        /*
         * Try to restore old selection. 
         */

        model.getRowSorter().setSortKeys(Arrays.asList(new SortKey(0, SortOrder.ASCENDING)));

        if (oldRowSelection != null) {
            // try to find same item
            int matchByItemId = -1;
            int matchByType = -1;
            for (int i = 0; i < model.getRowCount(); i++) {
                final Asset row = model.getRow(i);
                if (row.getItemId() == oldRowSelection.getItemId()) {
                    matchByItemId = i;
                    break;
                } else if (matchByType == -1l && row.getType().equals(oldRowSelection.getType())) {
                    matchByType = i;
                    break;
                }
            }

            int modelRow = matchByItemId;
            if (modelRow == -1) {
                modelRow = matchByType;
            }
            if (modelRow != -1) {
                int viewRow = model.convertModelRowToView(modelRow);
                if (log.isTraceEnabled()) {
                    log.debug("updateAssetList(): Scrolling to view row " + viewRow);
                }

                Misc.scrollTableToRow(table, viewRow);
                table.setRowSelectionInterval(viewRow, viewRow);
            } else {
                log.debug("updateAssetList(): Failed to restore selection");
            }
        }
    }

    private void updateComboBoxModels(AssetList assets) {
        // update location model

        final List<ILocation> locations = assets.getLocations();
        locations.add(0, ILocation.ANY_LOCATION);

        final ILocation selected = (ILocation) locationComboBox.getSelectedItem();

        this.locationModel.setData(locations);

        if (selected != null && selected != ILocation.ANY_LOCATION) {

            for (ILocation newLoc : locations) {
                if (newLoc.getDisplayName().equals(selected.getDisplayName())) {
                    locationComboBox.setSelectedItem(newLoc);
                }
            }
        }

        // update inventory type model
        final List<InventoryType> types = assets.getInventoryTypes();

        final List<TypeWrapper> wrappers = new ArrayList<TypeWrapper>();

        final Map<Long, InventoryCategory> categories = new HashMap<Long, InventoryCategory>();

        final Map<Long, InventoryGroup> groups = new HashMap<Long, InventoryGroup>();

        for (InventoryType t : types) {
            final InventoryGroup group = t.getGroup();
            final InventoryCategory cat = t.getGroup().getCategory();
            categories.put(cat.getId(), cat);
            if (filterByCategory.isSelected() && categoryModel.getSelectedItem() != null) {
                if (group.getCategory() == categoryModel.getSelectedItem()) {
                    groups.put(group.getId(), group);
                }
            } else {
                groups.put(group.getId(), group);
            }
        }

        for (InventoryType t : types) {
            if (filterByCategory.isSelected() && categoryModel.getSelectedItem() != null
                    && t.getGroup().getCategory() != categoryModel.getSelectedItem()) {
                continue;
            }
            if (filterByGroup.isSelected() && groupModel.getSelectedItem() != null
                    && !t.getGroup().getGroupID().equals(groupModel.getSelectedItem().getGroupID())) {
                continue;
            }
            wrappers.add(new TypeWrapper(t));
        }
        wrappers.add(0, ANY_TYPE);

        // try restoring old selections
        final TypeWrapper oldSelection = (TypeWrapper) this.typeComboBox.getSelectedItem();

        this.typeModel.setData(wrappers);

        if (oldSelection != null && oldSelection != ANY_TYPE) {
            // try to restore selection 
            for (TypeWrapper newWrapper : wrappers) {
                if (newWrapper.type.equals(oldSelection.type)) {
                    this.typeComboBox.setSelectedItem(newWrapper);
                    break;
                }
            }
        }

        // update inventory group model
        final InventoryGroup oldGroup = (InventoryGroup) groupComboBox.getSelectedItem();

        this.groupModel.setData(sortUsingLabelProvider(groups.values(), new ILabelProvider<InventoryGroup>() {

            @Override
            public String getLabel(InventoryGroup obj) {
                return getDisplayName(obj);
            }
        }));

        if (oldGroup != null) {
            // try to restore selection 
            for (InventoryGroup newWrapper : groups.values()) {
                if (newWrapper.getGroupID().equals(oldGroup.getGroupID())) {
                    this.groupComboBox.setSelectedItem(newWrapper);
                    break;
                }
            }
        }

        // update inventory category model
        final InventoryCategory oldCat = (InventoryCategory) categoryComboBox.getSelectedItem();

        this.categoryModel
                .setData(sortUsingLabelProvider(categories.values(), new ILabelProvider<InventoryCategory>() {

                    @Override
                    public String getLabel(InventoryCategory obj) {
                        return getDisplayName(obj);
                    }
                }));

        if (oldCat != null) {
            // try to restore selection 
            for (InventoryCategory newWrapper : categories.values()) {
                if (newWrapper.getId().equals(oldCat.getId())) {
                    this.categoryComboBox.setSelectedItem(newWrapper);
                    break;
                }
            }
        }
    }

    protected static final String getDisplayName(InventoryCategory cat) {
        if (cat == null) {
            return null;
        }
        return toPrettyName(cat.getCategoryName());
    }

    protected static final String getDisplayName(InventoryGroup group) {
        if (group == null) {
            return null;
        }
        return toPrettyName(group.getGroupName());
    }

    protected static final String toPrettyName(String name) {
        if (StringUtils.isBlank(name)) {
            return name;
        }
        return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase().replaceAll("_", " ");
    }

    public void setSelectionProvider(ISelectionProvider<ICharacter> selectionProvider) {
        assertDetached();

        this.selectionProvider = selectionProvider;
    }

    private final class LocationRenderer extends DefaultListCellRenderer {

        @Override
        public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
                boolean cellHasFocus) {
            super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

            if (value != null) {
                setText(((ILocation) value).getDisplayName());
            }
            return this;
        }
    }

}