All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tentackle.fx.table.DefaultTableConfiguration Maven / Gradle / Ivy

/*
 * Tentackle - https://tentackle.org.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

package org.tentackle.fx.table;

import javafx.collections.ObservableList;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;

import org.tentackle.common.LocaleProvider;
import org.tentackle.fx.FxRuntimeException;
import org.tentackle.fx.bind.FxBindingFactory;
import org.tentackle.fx.bind.FxTableBinder;
import org.tentackle.fx.component.FxTableView;
import org.tentackle.fx.component.FxTreeTableView;
import org.tentackle.prefs.PersistedPreferences;
import org.tentackle.reflect.EffectiveClassProvider;
import org.tentackle.reflect.ReflectionHelper;

import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.prefs.BackingStoreException;

/**
 * Default implementation of a table configuration.
 *
 * @param  type of the objects contained within the table's items list
 * @author harald
 */
public class DefaultTableConfiguration implements TableConfiguration {

  /*
   * TableView and TreeTableView don't inherit from each other and thus
   * each spawn their own related types such as TableColum, TreeTableColumn, etc...
   * As a result, since {@link TableConfiguration} and {@link TableColumnConfiguration} apply
   * to both tables and treetables, a lot of semantically identical code is necessary.
   * However, it is not really duplicate code. There's not much we can do to reduce it.
   */

  /** preferences key for table and column width. */
  private static final String PREF_WIDTH = "width";

  /** preferences key for table height. */
  private static final String PREF_TABLE_HEIGHT = "height";

  /** preferences key for column view index. */
  private static final String PREF_COLUMN_INDEX = "index";

  /** preferences key for column visibility. */
  private static final String PREF_COLUMN_VISIBILITY = "visible";

  /** preferences key for column sort type. */
  private static final String PREF_COLUMN_SORT_TYPE = "sort_type";

  /** preferences key for column sort order. */
  private static final String PREF_COLUMN_SORT_INDEX = "sort_ndx";



  /**
   * Template object.
   */
  private final S template;

  /**
   * The table's name.
   */
  private final String name;

  /**
   * The row object's class.
   */
  private final Class objectClass;

  /**
   * The column configs mapped by column name.
   */
  private final Map> nameColumnConfigMap = new LinkedHashMap<>();

  /**
   * The column configs mapped by table column.
   */
  private final Map, TableColumnConfiguration> tableColumnConfigMap = new LinkedHashMap<>();

  /**
   * The column configs mapped by treetable column.
   */
  private final Map, TableColumnConfiguration> treeTableColumnConfigMap = new LinkedHashMap<>();

  /**
   * The binder.
   */
  private FxTableBinder binder;

  /**
   * Whether preferences include sorting configuration.
   */
  private boolean sortingIncluded;

  /**
   * Whether preferences include the view size.
   */
  private boolean viewSizeIncluded = true;

  /**
   * The edit mode.
   */
  private EDITMODE editMode = EDITMODE.NO;

  /**
   * The binding type.
   */
  private BINDING bindingType = BINDING.YES;

  /**
   * Creates a configuration.
   *
   * @param template a template object
   * @param name the table's name, null if basename from effective class of template
   */
  @SuppressWarnings("unchecked")
  public DefaultTableConfiguration(S template, String name) {
    this.template = template;
    this.objectClass = (Class) EffectiveClassProvider.getEffectiveClass(template);
    this.name = name == null ? ReflectionHelper.getClassBaseName(objectClass) : name;
  }

  /**
   * Creates a configuration.
   *
   * @param objectClass the object class
   * @param name the table's name, null if basename from effective class of template
   */
  public DefaultTableConfiguration(Class objectClass, String name) {
    this.template = null;
    this.objectClass = objectClass;
    this.name = name == null ? ReflectionHelper.getClassBaseName(objectClass) : name;
  }

  /**
   * Gets the template.
   *
   * @return the template, null if only class given
   */
  public S getTemplate() {
    return template;
  }


  @Override
  public String toString() {
    return name;
  }


  @Override
  public TableColumnConfiguration addColumn(String name, String displayedName) {
    TableColumnConfiguration columnConfiguration = createTableColumnConfiguration(name, displayedName);
    addColumnConfiguration(columnConfiguration);
    return columnConfiguration;
  }

  @Override
  public TableColumnConfiguration removeColumn(String name) {
    TableColumnConfiguration columnConfiguration = nameColumnConfigMap.remove(name);
    if (columnConfiguration != null) {
      tableColumnConfigMap.remove(columnConfiguration.getTableColumn());
      treeTableColumnConfigMap.remove(columnConfiguration.getTreeTableColumn());
    }
    return columnConfiguration;
  }

  @Override
  public void addColumnConfiguration(TableColumnConfiguration columnConfiguration) {
    nameColumnConfigMap.put(columnConfiguration.getName(), columnConfiguration);
    tableColumnConfigMap.put(columnConfiguration.getTableColumn(), columnConfiguration);
    treeTableColumnConfigMap.put(columnConfiguration.getTreeTableColumn(), columnConfiguration);
  }

  @Override
  public Collection> getColumnConfigurations() {
    return nameColumnConfigMap.values();
  }

  @Override
  public TableColumnConfiguration getColumnConfiguration(String name) {
    return name == null ? null : nameColumnConfigMap.get(name);
  }

  @Override
  public TableColumnConfiguration getColumnConfiguration(TableColumn column) {
    return column == null ? null : tableColumnConfigMap.get(column);
  }

  @Override
  public TableColumnConfiguration getColumnConfiguration(TreeTableColumn column) {
    return column == null ? null : treeTableColumnConfigMap.get(column);
  }

  @Override
  public String getName() {
    return name;
  }

  @Override
  public boolean isSortingIncluded() {
    return sortingIncluded;
  }

  @Override
  public void setSortingIncluded(boolean sortingIncluded) {
    this.sortingIncluded = sortingIncluded;
  }

  @Override
  public boolean isViewSizeIncluded() {
    return viewSizeIncluded;
  }

  @Override
  public void setViewSizeIncluded(boolean viewSizeIncluded) {
    this.viewSizeIncluded = viewSizeIncluded;
  }

  @Override
  public void savePreferences(FxTableView table, String suffix, boolean system) {

    StringBuilder prefName = new StringBuilder();
    prefName.append(name);
    if (suffix != null) {
      prefName.append('_').append(suffix);
    }
    // always add the locale since header sizes etc... may vary
    prefName.append('_').append(LocaleProvider.getInstance().getEffectiveLocale());

    ObservableList> sortOrder = table.getSortOrder();

    try {
      PersistedPreferences prefs = system ?
                                      PersistedPreferences.systemRoot().node(prefName.toString()) :
                                      PersistedPreferences.userRoot().node(prefName.toString());

      int viewIndex = 0;
      for (TableColumn column : table.getColumns()) {
        TableColumnConfiguration columnConfig = getColumnConfiguration(column);
        if (columnConfig != null) {
          PersistedPreferences columnPrefs = prefs.node(columnConfig.getName());
          columnPrefs.putInt(PREF_COLUMN_VISIBILITY, column.isVisible() ? 1 : 0);
          columnPrefs.putDouble(PREF_WIDTH, column.getWidth());
          columnPrefs.putInt(PREF_COLUMN_INDEX, viewIndex);
          int sortIndex = sortOrder.indexOf(column);
          if (isSortingIncluded() && sortIndex >= 0) {
            columnPrefs.putInt(PREF_COLUMN_SORT_TYPE, column.getSortType().ordinal());
            columnPrefs.putInt(PREF_COLUMN_SORT_INDEX, sortIndex);
          }
          else {
            columnPrefs.remove(PREF_COLUMN_SORT_TYPE);
            columnPrefs.remove(PREF_COLUMN_SORT_INDEX);
          }
        }
        viewIndex++;
      }

      // store also the dimensions of the table
      double height = table.getHeight();
      TotalsTableView totalsTable = TotalsTableView.getTotalsTableView(table);
      if (totalsTable != null) {
        // add the height of the totals table
        height += totalsTable.getHeight();
      }
      prefs.putDouble(PREF_TABLE_HEIGHT, height);
      prefs.putDouble(PREF_WIDTH, table.getWidth());
      prefs.flush();
    }
    catch (BackingStoreException | RuntimeException ex) {
      throw new FxRuntimeException("saving table preferences failed for '" + prefName + "'", ex);
    }
  }

  @Override
  public void loadPreferences(FxTableView table, String suffix, boolean system) {

    StringBuilder prefName = new StringBuilder();
    prefName.append(name);
    if (suffix != null) {
      prefName.append('_').append(suffix);
    }
    // always add the locale since header sizes etc... may vary
    prefName.append('_').append(LocaleProvider.getInstance().getEffectiveLocale());

    Map> sortOrder = new TreeMap<>();   // sorted by sort index

    try {
      // get the preferences
      PersistedPreferences systemPref = PersistedPreferences.systemRoot().node(prefName.toString());
      PersistedPreferences userPref = system ? null : PersistedPreferences.userRoot().node(prefName.toString());

      // map of column name to settings
      Map columnMapByName = new HashMap<>();
      for (String columnName : systemPref.childrenNames()) {
        columnMapByName.put(columnName, systemPref.node(columnName));
      }
      if (userPref != null) {
        // replace by user settings, if present
        for (String columnName : userPref.childrenNames()) {
          columnMapByName.put(columnName, userPref.node(columnName));
        }
      }

      Map> columnMapByIndex = new TreeMap<>();
      for (Map.Entry entry : columnMapByName.entrySet()) {
        TableColumnConfiguration columnConfig = getColumnConfiguration(entry.getKey());
        if (columnConfig != null) {
          TableColumn column = columnConfig.getTableColumn();
          if (column != null) {
            PersistedPreferences columnPref = entry.getValue();
            int visibility = columnPref.getInt(PREF_COLUMN_VISIBILITY, -1);
            column.setVisible(visibility != 0);
            double width = columnPref.getDouble(PREF_WIDTH, -1.0);
            if (width >= 0.0) {
              table.resizeColumn(column, width - column.getWidth());
            }
            int index = columnPref.getInt(PREF_COLUMN_INDEX, -1);
            if (index >= 0) {
              columnMapByIndex.put(index, column);
              if (isSortingIncluded()) {
                int sorting = columnPref.getInt(PREF_COLUMN_SORT_TYPE, -1);
                if (sorting >= 0) {
                  column.setSortType(TableColumn.SortType.values()[sorting]);
                  int sortIndex = columnPref.getInt(PREF_COLUMN_SORT_INDEX, -1);
                  if (sortIndex == -1) {
                    sortIndex = Integer.MAX_VALUE - sortOrder.size();
                  }
                  sortOrder.put(sortIndex, column);
                }
              }
            }
          }
        }
      }

      table.getColumns().removeAll(columnMapByIndex.values());
      table.getColumns().addAll(columnMapByIndex.values());
      table.getSortOrder().addAll(sortOrder.values());

      // read table dimensions
      if (isViewSizeIncluded()) {
        double width = -1.0;
        String key = PREF_WIDTH;
        if (!system) {
          width = userPref.getDouble(key, width);
        }
        if (width < 0.0) {
          width = systemPref.getDouble(key, width);
        }
        if (width > 0.0) {
          table.setPrefWidth(width);
        }

        double height = -1.0;
        key = PREF_TABLE_HEIGHT;
        if (!system) {
          height = userPref.getDouble(key, height);
        }
        if (height < 0.0) {
          height = systemPref.getDouble(key, height);
        }
        if (height > 0.0) {
          table.setPrefHeight(height);
        }
      }

    }
    catch (BackingStoreException | RuntimeException ex) {
      throw new FxRuntimeException("loading table preferences failed for '" + prefName + "'", ex);
    }
  }

  @Override
  public void savePreferences(FxTreeTableView treeTable, String suffix, boolean system) {

    StringBuilder prefName = new StringBuilder();
    prefName.append(name);
    if (suffix != null) {
      prefName.append('_').append(suffix);
    }
    // always add the locale since header sizes etc... may vary
    prefName.append('_').append(LocaleProvider.getInstance().getEffectiveLocale());

    ObservableList> sortOrder = treeTable.getSortOrder();

    try {
      PersistedPreferences prefs = system ?
                                   PersistedPreferences.systemRoot().node(prefName.toString()) :
                                   PersistedPreferences.userRoot().node(prefName.toString());

      int viewIndex = 0;
      for (TreeTableColumn column : treeTable.getColumns()) {
        TableColumnConfiguration columnConfig = getColumnConfiguration(column);
        if (columnConfig != null) {
          PersistedPreferences columnPrefs = prefs.node(columnConfig.getName());
          columnPrefs.putInt(PREF_COLUMN_VISIBILITY, column.isVisible() ? 1 : 0);
          columnPrefs.putDouble(PREF_WIDTH, column.getWidth());
          columnPrefs.putInt(PREF_COLUMN_INDEX, viewIndex);
          int sortIndex = sortOrder.indexOf(column);
          if (isSortingIncluded() && sortIndex >= 0) {
            columnPrefs.putInt(PREF_COLUMN_SORT_TYPE, column.getSortType().ordinal());
            columnPrefs.putInt(PREF_COLUMN_SORT_INDEX, sortIndex);
          }
          else {
            columnPrefs.remove(PREF_COLUMN_SORT_TYPE);
            columnPrefs.remove(PREF_COLUMN_SORT_INDEX);
          }
        }
        viewIndex++;
      }

      // store also the dimensions of the table
      double height = treeTable.getHeight();
      prefs.putDouble(PREF_TABLE_HEIGHT, height);
      prefs.putDouble(PREF_WIDTH, treeTable.getWidth());
      prefs.flush();
    }
    catch (BackingStoreException | RuntimeException ex) {
      throw new FxRuntimeException("saving table preferences failed for '" + prefName + "'", ex);
    }
  }

  @Override
  public void loadPreferences(FxTreeTableView treeTable, String suffix, boolean system) {

    StringBuilder prefName = new StringBuilder();
    prefName.append(name);
    if (suffix != null) {
      prefName.append('_').append(suffix);
    }
    // always add the locale since header sizes etc... may vary
    prefName.append('_').append(LocaleProvider.getInstance().getEffectiveLocale());

    Map> sortOrder = new TreeMap<>();   // sorted by sort index

    try {
      // get the preferences
      PersistedPreferences systemPref = PersistedPreferences.systemRoot().node(prefName.toString());
      PersistedPreferences userPref = system ? null : PersistedPreferences.userRoot().node(prefName.toString());

      // map of column name to settings
      Map columnMapByName = new HashMap<>();
      for (String columnName : systemPref.childrenNames()) {
        columnMapByName.put(columnName, systemPref.node(columnName));
      }
      if (userPref != null) {
        // replace by user settings, if present
        for (String columnName : userPref.childrenNames()) {
          columnMapByName.put(columnName, userPref.node(columnName));
        }
      }

      Map> columnMapByIndex = new TreeMap<>();
      for (Map.Entry entry : columnMapByName.entrySet()) {
        TableColumnConfiguration columnConfig = getColumnConfiguration(entry.getKey());
        if (columnConfig != null) {
          TreeTableColumn column = columnConfig.getTreeTableColumn();
          if (column != null) {
            PersistedPreferences columnPref = entry.getValue();
            int visibility = columnPref.getInt(PREF_COLUMN_VISIBILITY, -1);
            column.setVisible(visibility != 0);
            double width = columnPref.getDouble(PREF_WIDTH, -1.0);
            if (width >= 0.0) {
              treeTable.resizeColumn(column, width - column.getWidth());
            }
            int index = columnPref.getInt(PREF_COLUMN_INDEX, -1);
            if (index >= 0) {
              columnMapByIndex.put(index, column);
              if (isSortingIncluded()) {
                int sorting = columnPref.getInt(PREF_COLUMN_SORT_TYPE, -1);
                if (sorting >= 0) {
                  column.setSortType(TreeTableColumn.SortType.values()[sorting]);
                  int sortIndex = columnPref.getInt(PREF_COLUMN_SORT_INDEX, -1);
                  if (sortIndex == -1) {
                    sortIndex = Integer.MAX_VALUE - sortOrder.size();
                  }
                  sortOrder.put(sortIndex, column);
                }
              }
            }
          }
        }
      }

      treeTable.getColumns().removeAll(columnMapByIndex.values());
      treeTable.getColumns().addAll(columnMapByIndex.values());
      treeTable.getSortOrder().addAll(sortOrder.values());

      // read table dimensions
      if (isViewSizeIncluded()) {
        double width = -1.0;
        String key = PREF_WIDTH;
        if (!system) {
          width = userPref.getDouble(key, width);
        }
        if (width < 0.0) {
          width = systemPref.getDouble(key, width);
        }
        if (width > 0.0) {
          treeTable.setPrefWidth(width);
        }

        double height = -1.0;
        key = PREF_TABLE_HEIGHT;
        if (!system) {
          height = userPref.getDouble(key, height);
        }
        if (height < 0.0) {
          height = systemPref.getDouble(key, height);
        }
        if (height > 0.0) {
          treeTable.setPrefHeight(height);
        }
      }

    }
    catch (BackingStoreException | RuntimeException ex) {
      throw new FxRuntimeException("loading table preferences failed for '" + prefName + "'", ex);
    }
  }


  @Override
  public EDITMODE getEditMode() {
    return editMode;
  }

  @Override
  public void setEditMode(EDITMODE editMode) {
    this.editMode = editMode == null ? EDITMODE.NO : editMode;
  }

  @Override
  public void configure(FxTableView table) {
    table.getColumns().clear();
    boolean tableEditable = false;
    for (TableColumnConfiguration config: nameColumnConfigMap.values()) {
      boolean columnEditable = config.isEditable();
      tableEditable |= columnEditable;
      TableColumn column = config.getTableColumn();
      column.setEditable(columnEditable);
      table.getColumns().add(column);
    }
    table.setEditable(tableEditable);
    table.getSelectionModel().cellSelectionEnabledProperty().set(tableEditable);
    table.setConfiguration(this);
  }

  @Override
  public void configure(FxTreeTableView treeTable) {
    treeTable.getColumns().clear();
    boolean tableEditable = false;
    for (TableColumnConfiguration config: nameColumnConfigMap.values()) {
      boolean columnEditable = config.isEditable();
      tableEditable |= columnEditable;
      TreeTableColumn column = config.getTreeTableColumn();
      column.setEditable(columnEditable);
      treeTable.getColumns().add(column);
    }
    treeTable.setEditable(tableEditable);
    treeTable.getSelectionModel().cellSelectionEnabledProperty().set(tableEditable);
    treeTable.setConfiguration(this);
  }

  @Override
  public Class getObjectClass() {
    return objectClass;
  }

  @Override
  public BINDING getBindingType() {
    return bindingType;
  }

  @Override
  public void setBindingType(BINDING bindingType) {
    this.bindingType = bindingType;
  }

  @Override
  public FxTableBinder getBinder() {
    if (binder == null) {
      binder = FxBindingFactory.getInstance().createTableBinder(this);
    }
    return binder;
  }

  @Override
  public  TableCellType getTableCellType(Class type) {
    return TableCellTypeFactory.getInstance().getTableCellType(type);
  }

  @Override
  public TreeItem createTreeItem(S object) {
    return new TreeItem<>(object);
  }


  /**
   * Creates a column configuration.
   *
   * @param name the column's binding path
   * @param displayedName the displayed column name
   * @return the created column configuration
   */
  protected TableColumnConfiguration createTableColumnConfiguration(String name, String displayedName) {
    return new DefaultTableColumnConfiguration<>(this, name, displayedName);
  }

}