diff --git a/CHANGELOG.md b/CHANGELOG.md index f72c67a4a53..e778e8fdaab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,6 +174,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve ### Changed +- We changed the resize behavior of table columns to have smart fit into the table if there is enough space. [#967](https://github.com/JabRef/jabref/issues/967) - The export to MS Office XML now exports the author field as `Inventor` if the bibtex entry type is `patent` [#7830](https://github.com/JabRef/jabref/issues/7830) - We changed the EndNote importer to import the field `label` to the corresponding bibtex field `endnote-label` [forum#2734](https://discourse.jabref.org/t/importing-endnote-label-field-to-jabref-from-xml-file/2734) - The keywords added via "Manage content selectors" are now displayed in alphabetical order. [#3791](https://github.com/JabRef/jabref/issues/3791) @@ -303,6 +304,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We changed the way JabRef displays the title of a tab and of the window. [4161](https://github.com/JabRef/jabref/issues/4161) - We changed connect timeouts for server requests to 30 seconds in general and 5 seconds for GROBID server (special) and improved user notifications on connection issues. [7026](https://github.com/JabRef/jabref/pull/7026) - We changed the order of the library tab context menu items. [#7171](https://github.com/JabRef/jabref/issues/7171) +- We renamed "Show extra columns" to "Show dedicated file columns". [#7181](https://github.com/JabRef/jabref/pull/7181) - We changed the way linked files are opened on Linux to use the native openFile method, compatible with confined packages. [7037](https://github.com/JabRef/jabref/pull/7037) - We refined the entry preview to show the full names of authors and editors, to list the editor only if no author is present, have the year earlier. [#7083](https://github.com/JabRef/jabref/issues/7083) diff --git a/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java b/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java index d79b3a6857b..662923e0731 100644 --- a/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java +++ b/src/main/java/org/jabref/gui/maintable/ColumnPreferences.java @@ -4,15 +4,20 @@ public class ColumnPreferences { + public static final double DEFAULT_COLUMN_MIN_WIDTH = 80; public static final double DEFAULT_COLUMN_WIDTH = 100; public static final double ICON_COLUMN_WIDTH = 16 + 12; // add some additional space to improve appearance private final List columns; + private final List columnSortOrder; - public ColumnPreferences(List columns, List columnSortOrder) { + private final boolean dedicatedFileColumnsEnabled; + + public ColumnPreferences(List columns, List columnSortOrder, boolean dedicatedFileColumnsEnabled) { this.columns = columns; this.columnSortOrder = columnSortOrder; + this.dedicatedFileColumnsEnabled = dedicatedFileColumnsEnabled; } public List getColumns() { @@ -22,4 +27,9 @@ public List getColumns() { public List getColumnSortOrder() { return columnSortOrder; } + + public boolean isDedicatedFileColumnsEnabled() { + return dedicatedFileColumnsEnabled; + } + } diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java index 130faad49fc..c2f15dc9de9 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -83,7 +83,7 @@ public MainTable(MainTableDataModel model, this.database = Objects.requireNonNull(database); this.model = model; UndoManager undoManager = libraryTab.getUndoManager(); - MainTablePreferences mainTablePreferences = preferencesService.getMainTablePreferences(); + ColumnPreferences columnPreferences = preferencesService.getColumnPreferences(); importHandler = new ImportHandler( database, externalFileTypes, @@ -141,17 +141,14 @@ public MainTable(MainTableDataModel model, } } */ - - mainTablePreferences.getColumnPreferences().getColumnSortOrder().forEach(columnModel -> + columnPreferences.getColumnSortOrder().forEach(columnModel -> this.getColumns().stream() .map(column -> (MainTableColumn) column) .filter(column -> column.getModel().equals(columnModel)) .findFirst() .ifPresent(column -> this.getSortOrder().add(column))); - if (mainTablePreferences.getResizeColumnsToFit()) { - this.setColumnResizePolicy(new SmartConstrainedResizePolicy()); - } + this.setColumnResizePolicy(new SmartConstrainedResizePolicy()); this.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); @@ -344,7 +341,7 @@ private void handleOnDragDetected(TableRow row, BibEntry List entries = getSelectionModel().getSelectedItems().stream().map(BibEntryTableViewModel::getEntry).collect(Collectors.toList()); - // The following is necesary to initiate the drag and drop in javafx, although we don't need the contents + // The following is necessary to initiate the drag and drop in javafx, although we don't need the contents // It doesn't work without ClipboardContent content = new ClipboardContent(); Dragboard dragboard = startDragAndDrop(TransferMode.MOVE); diff --git a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java index b1295a48fdc..de0e1c02402 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java @@ -37,7 +37,9 @@ import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; +import org.jabref.model.entry.field.InternalField; import org.jabref.model.entry.field.SpecialField; +import org.jabref.model.entry.field.StandardField; import org.jabref.model.groups.AbstractGroup; import org.jabref.model.util.OptionalUtil; import org.jabref.preferences.PreferencesService; @@ -80,7 +82,6 @@ public MainTableColumnFactory(BibDatabaseContext database, List> columns = new ArrayList<>(); columnPreferences.getColumns().forEach(column -> { - switch (column.getType()) { case INDEX: columns.add(createIndexColumn(column)); @@ -113,7 +114,19 @@ public MainTableColumnFactory(BibDatabaseContext database, default: case NORMALFIELD: if (!column.getQualifier().isBlank()) { - columns.add(createFieldColumn(column)); + TableColumn fieldColumn = createFieldColumn(column); + columns.add(fieldColumn); + fieldColumn.setPrefWidth(ColumnPreferences.DEFAULT_COLUMN_WIDTH); + if (column.getQualifier().equalsIgnoreCase(StandardField.YEAR.getName())) { + // 60 is chosen, because of the optimal width of a four digit number + fieldColumn.setPrefWidth(60); + } else if (column.getQualifier().equalsIgnoreCase(InternalField.TYPE_HEADER.getName())) { + // 90 is chosen, because of the optimal width of the entry type + fieldColumn.setPrefWidth(90); + } else { + fieldColumn.setMinWidth(ColumnPreferences.DEFAULT_COLUMN_MIN_WIDTH); + fieldColumn.setPrefWidth(ColumnPreferences.DEFAULT_COLUMN_WIDTH); + } } break; } @@ -129,7 +142,7 @@ public static void setExactWidth(TableColumn column, double width) { } /** - * Creates a column with a continous number + * Creates a column with a continuous number */ private TableColumn createIndexColumn(MainTableColumnModel columnModel) { TableColumn column = new MainTableColumn<>(columnModel); diff --git a/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java b/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java index e82af036482..aee48142d5d 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java @@ -124,6 +124,9 @@ public Type getType() { return typeProperty.getValue(); } + /** + * Returns the field name of the column (e.g., year) + */ public String getQualifier() { return qualifierProperty.getValue(); } diff --git a/src/main/java/org/jabref/gui/maintable/MainTablePreferences.java b/src/main/java/org/jabref/gui/maintable/MainTablePreferences.java deleted file mode 100644 index 7698893b9dd..00000000000 --- a/src/main/java/org/jabref/gui/maintable/MainTablePreferences.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.jabref.gui.maintable; - -public class MainTablePreferences { - private final ColumnPreferences columnPreferences; - private final boolean resizeColumnsToFit; - private final boolean extraFileColumnsEnabled; - - public MainTablePreferences(ColumnPreferences columnPreferences, boolean resizeColumnsToFit, boolean extraFileColumnsEnabled) { - this.columnPreferences = columnPreferences; - this.resizeColumnsToFit = resizeColumnsToFit; - this.extraFileColumnsEnabled = extraFileColumnsEnabled; - } - - public ColumnPreferences getColumnPreferences() { - return columnPreferences; - } - - public boolean getResizeColumnsToFit() { - return resizeColumnsToFit; - } - - public boolean getExtraFileColumnsEnabled() { - return extraFileColumnsEnabled; - } -} diff --git a/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java b/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java index 092f5c02820..21361da8b70 100644 --- a/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java +++ b/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java @@ -38,18 +38,20 @@ private void updateColumns() { mainTable.getColumns().stream() .map(column -> ((MainTableColumn) column).getModel()) .collect(Collectors.toList()), - preferences.getColumnPreferences().getColumnSortOrder())); + preferences.getColumnPreferences().getColumnSortOrder(), + preferences.getColumnPreferences().isDedicatedFileColumnsEnabled())); } /** - * Stores the SortOrder of the the Table in the preferences. Cannot be combined with updateColumns, because JavaFX + * Stores the SortOrder of the Table in the preferences. Cannot be combined with updateColumns, because JavaFX * would provide just an empty list for the sort order on other changes. */ private void updateSortOrder() { preferences.storeColumnPreferences(new ColumnPreferences( - preferences.getColumnPreferences().getColumns(), - mainTable.getSortOrder().stream() - .map(column -> ((MainTableColumn) column).getModel()) - .collect(Collectors.toList()))); + preferences.getColumnPreferences().getColumns(), + mainTable.getSortOrder().stream() + .map(column -> ((MainTableColumn) column).getModel()) + .collect(Collectors.toList()), + preferences.getColumnPreferences().isDedicatedFileColumnsEnabled())); } } diff --git a/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java b/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java index 4096455c765..7fe1e2b3a78 100644 --- a/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java +++ b/src/main/java/org/jabref/gui/maintable/SmartConstrainedResizePolicy.java @@ -1,12 +1,13 @@ package org.jabref.gui.maintable; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; -import javafx.scene.control.ResizeFeaturesBase; -import javafx.scene.control.TableColumnBase; +import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.util.Callback; @@ -14,96 +15,467 @@ import org.slf4j.LoggerFactory; /** - * This resize policy is almost the same as {@link TableView#CONSTRAINED_RESIZE_POLICY} - * We make sure that the width of all columns sums up to the total width of the table. - * However, in contrast to {@link TableView#CONSTRAINED_RESIZE_POLICY} we size the columns initially by their preferred width. + * This resize policy supports following properties – preferably user-configurable: + * + *
    + *
  • Honoring the minimal and maximal width for each column
  • + *
  • Honoring a desired width of each column (if available). It is used at initial table rendering (e.g., program startup) and for the threshold (see below). It is not used in other cases.
  • + *
  • Honoring non-resizable columns (i.e., that a column should not be scaled)
  • + *
  • Desired width derivation (e.g., 80% of the desired width) is a point from which on the column should not be shrunk any more. This is called the threshold.
  • + *
+ *

+ * This resize policy supports following properties (non user-configurable): + * + *

    + *
  • Between each resizing, the properties of each column (minimal width, resizable, ...) can change.
  • + *
  • Automatic sizing of the columns: + *
      + *
    • Each column should have at least its minimal size.
    • + *
    • If the space honoring the minimal width is not sufficient, a horizontal scrollbar should be used.
    • + *
    • Columns with fixed size should use those fixed sizes
    • + *
    • If there is additional space left the remaining columns should be broadened to use this space
    • + *
    + *
  • + *
  • We balance between a complete fit into the table space and the desired width. A "huge" derivation from the desired width is not accepted.
  • + *
  • Ideally, the columns should have the desired width
  • + *
  • If a user changes the size of a column, the new size of that column should be set.
  • + *
  • No behavior toggle buttons by the user --> this policy is a smart one
  • + *
  • We distinguish between a column being resized by the user and a column automatically resized.
  • + *
  • In case a user changes a column manually: + *
      + *
    • The column shrinks/enlarges by the requested delta.
    • + *
    • The column must not shrink below the minimum width / enlarge above the maximum width.
    • + *
    • Thereby, the desired width is not changed.
    • + *
    • The other columns adapt "smartly": The other columns according to their current ratio. This way, the column proportions are respected.
    • + *
    + *
  • + *
  • The ratio used for enlargement of columns respects the current column width. The ratio for shrinkage is constant for all columns.
  • + *
+ *

+ * The implementation is driven by following factors: + * + *

    + *
  • Minimal width is an "absolute" minimal making the content nearly barely visible
  • + *
  • Maximal width is an "absolute" maximal width making the content too much visible
  • + *
  • The preferred width holds the current active width of a column (due to JavaFX design decisions).
  • + *
  • Desired width is set to a reasonable value. The desired width is a "globally" preferred width.
  • + *
+ * + *

I0 - Initial table rendering

+ *

+ * Decision to take: a) Use last setting or b) rerender with desired widths. We opt for b), because we assume that user-configuration of desired widths is easy. This leads to I3 being inconsistent to RWS2 (there is no previous width in I3) + * + *

    + *
  • I1 - Initially, all columns takes the desired width.
  • + *
  • I2 - If content fits into table, distribute remaining delta to table space according to the shares. No scrollbar.
  • + *
  • I3 - If content does not fit into table, all columns are shrunk (all equally) until all columns hit the threshold. In case no column can shrink any more (they are smaller or equal the threshold), they are not shrunk. Thus, the table gets wider than the table space. This leads to a scrollbar.
  • + *
+ * + *

RW0 - Resizing of window

+ * + *
    + *
  • RWE0 - If enlarged + *
      + *
    • RWE1 - Content has fit into table. Then, content still has to fit into table. Thus, enlarge all columns respecting the ratio (the content is enlarged proportional to actual width). Scrollbar not present.
    • + *
    • RWE2 - Content has not fit into table. Case: Content still not fits into table. No resize action. Scrollbar shrinks.
    • + *
    • RWE3 - Content has not fit into table. Case: Content fits into table. Calculate the delta and distribute across all resizable columns using the ratio. Scrollbar not present any more.
    • + *
    + *
  • + *
  • RWS0 - If shrunk + *
      + *
    • RWS1 - Content has fit into table. Content does not fit into table anymore (because of shrinkage). Delta is absorbed by columns until threshold is reached. If not complete delta can be absorbed, scrollbar appears.
    • + *
    • RWS2 - Content has not fit into table. Content still does not fit into table (because of shrinkage). Scrollbar enlarges.
    • + *
    + *
  • + *
+ * + *

RC0 - Resizing of column

+ * + *
    + *
  • RCE0 - If enlarged + *
      + *
    • RCE1 - Content has fit into table. Then, content does not fit into table (because of delta). Column gets enlarged by the delta. Other columns should not below a certain "reasonable" size ("threshold"). All columns are shrunk (all equally) until all columns hit the threshold. In case no column can shrink any more (they are smaller or equal the threshold), they are not shrunk. Thus, the table gets wider than the table space. This leads to a scrollbar.
    • + *
    • RCE2 - Content has not fit into table. Then, content still does not fit into table (because of delta). Column gets enlarged by the delta. Other columns are not changed (consistency to RCS3). Scrollbar gets wider.
    • + *
    + *
  • + *
  • RCS0 - If shrunk + *
      + *
    • RCS1 - Content has fit into table. Then, content still fits into table (because of delta). Column must not shrink below minimum width. If minimum is reached, nothing happens. Remaining delta is distributed among other columns. No scrollbar present.
    • + *
    • RCS2 - Content has not fit into table. If the delta is applied, the content fits into the table. Delta is applied to the column. Remaining delta splits up of delta-in-bounds and delta-out-of-bounds. Delta-in-bounds is distributed among other columns. Scrollbar disappears.
    • + *
    • RCS3 - Content has not fit into table. If the delta is applied, the content still does not fit into table. Column is shrunk respecting the delta. Other columns are not changed (consistency to RCE2). Scrollbar shrinks.
    • + *
    + *
  • + *
+ * + *

Notes

+ * + *
    + *
  • Design goal of this class is to be self-contained and not dependent on other non-JavaFX classes.
  • + *
  • TODO: In case the desired column width is updated, the SmartConstrainedResizePolicy needs to be reinstantiated. A more advanced code would use JavaFX Properties for that.
  • + *
+ * + *

Related Work

+ *
    + *
  • TableView SmartResize Policy
  • + *
  • CONSTRAINED_RESIZE_POLICY: This policy a) initially adjust the column widths of all columns in a way that the table fits the whole table space, b) adjusts the width of the columns right to the current column to have the whole table fit into the table space. The policy starts with the minimum width, not with the preferred width. The policy does not support the case if the content does not fit into the table space.
  • + *
  • UNCONSTRAINED_RESIZE_POLICY: This policy just resizes the specified column by the provided delta and shifts all other columns (to the right of the given column) further to the right (when the delta is positive) or to the left (when the delta is negative).
  • + *
  • HypnosResizePolicy: Similar to CONSTRAINED_RESIZE_POLICY. However, at resize extra space is given to columns that aren't at their pref width and need that type of space (negative or positive) on a proportional basis first.
  • + *
*/ public class SmartConstrainedResizePolicy implements Callback { private static final Logger LOGGER = LoggerFactory.getLogger(SmartConstrainedResizePolicy.class); + // The delta which is "OK" the column width being smaller than the table width without any action taken + public static final double EPSILON_MARGIN = 20d; + + private List> resizableColumns; + + private TableColumn lastModifiedColumn = null; + + private Map desiredColumnWidths; + + private Double thresholdPercent; + + // Required for RW0 + private Double previousTableWidth; + + private boolean firstTimeRun = true; + + public SmartConstrainedResizePolicy() { + this.desiredColumnWidths = new HashMap<>(); + // default is 80% + this.thresholdPercent = .8; + } + + /** + * Sets an desired column width for a set of columns. A "desired" width is a width which is wished by the user + * + * @param desiredColumnWidths + * @param thresholdPercent Value between 0 and 1 (normal percentage calculation: 1 is 100%, .8 is 80%, ...) + */ + public SmartConstrainedResizePolicy(Map desiredColumnWidths, Double thresholdPercent) { + this.desiredColumnWidths = desiredColumnWidths; + this.thresholdPercent = thresholdPercent; + } + + public Double getMinWidthThreshold(TableColumn column) { + return getDesiredColumnWidth(column) * thresholdPercent; + } + + /** + * SIDE EFFECT: Stores computed desired width if not present in HashMap. This leads to a constant desired width. + */ + private Double getDesiredColumnWidth(TableColumn column) { + desiredColumnWidths.putIfAbsent(column, column.getPrefWidth()); + Double result = desiredColumnWidths.get(column); + LOGGER.trace("Desired column width for {}: {}", column.getText(), result); + return result; + } + + /** + * @return false if surely no changes happened, true if some change happened + */ @Override public Boolean call(TableView.ResizeFeatures prop) { - if (prop.getColumn() == null) { - return initColumnSize(prop.getTable()); + TableView table = prop.getTable(); + if (firstTimeRun) { + if (table.getWidth() == 0.0d) { + LOGGER.debug("Table width is 0. Returning false"); + return false; + } + + firstTimeGlobalVariablesInitializations(table); + + LOGGER.debug("I0"); + doInitialTableRendering(table); + firstTimeRun = false; + LOGGER.debug("First time rendering completed."); + previousTableWidth = table.getWidth(); + LOGGER.debug("Storing current table width {} as \"previous table width\"", previousTableWidth); + return true; + } + + LOGGER.debug("RC0, RW0"); + + TableColumn column = prop.getColumn(); + Boolean result; + if (column == null) { + // happens at window resize + LOGGER.debug("RW0 - Table is fully rendered"); + if (previousTableWidth == table.getWidth()) { + LOGGER.debug("Table has same size as in last run. Nothing to do"); + return false; + } + result = doFullTableRendering(table); } else { - return constrainedResize(prop); + LOGGER.debug("RC0 - Column width changed"); + result = doColumnChange(table, prop.getColumn(), prop.getDelta()); } + LOGGER.debug("Result: {}", result); + previousTableWidth = table.getWidth(); + LOGGER.debug("Storing current table width {} as \"previous table width\"", previousTableWidth); + return result; } - private Boolean initColumnSize(TableView table) { - double tableWidth = getContentWidth(table); - List> visibleLeafColumns = table.getVisibleLeafColumns(); - double totalWidth = visibleLeafColumns.stream().mapToDouble(TableColumnBase::getWidth).sum(); - - if (Math.abs(totalWidth - tableWidth) > 1) { - double totalPrefWidth = visibleLeafColumns.stream().mapToDouble(TableColumnBase::getPrefWidth).sum(); - double currPrefWidth = 0; - if (totalPrefWidth > 0) { - for (TableColumnBase col : visibleLeafColumns) { - double share = col.getPrefWidth() / totalPrefWidth; - double newSize = tableWidth * share; - - // Just to make sure that we are staying under the total table width (due to rounding errors) - currPrefWidth += newSize; - if (currPrefWidth > tableWidth) { - newSize -= currPrefWidth - tableWidth; - currPrefWidth -= tableWidth; - } + private void firstTimeGlobalVariablesInitializations(TableView table) { + resizableColumns = table.getVisibleLeafColumns().stream() + .filter(TableColumn::isResizable) + .collect(Collectors.toList()); + } + + /** + * Case I0 + */ + private void doInitialTableRendering(TableView table) { + if (table.getWidth() == 0.0d) { + LOGGER.error("Table width is 0. Returning false"); + } + + LOGGER.debug("I1"); + resizableColumns.forEach(column -> column.setPrefWidth(getDesiredColumnWidth(column))); + + if (contentFitsIntoTable(table)) { + LOGGER.debug("I2"); + Map, Double> expansionRatio = determineExpansionRatio(resizableColumns); + rearrangeColumns(table, resizableColumns, expansionRatio); + } else { + LOGGER.debug("I3"); + Map, Double> shrinkageRatio = determineShrinkageRatio(resizableColumns); + rearrangeColumns(table, resizableColumns, shrinkageRatio); + } + } + + /** + * Case RW0 + */ + private Boolean doFullTableRendering(TableView table) { + if (table.getWidth() == 0.0d) { + LOGGER.error("Table width is 0. Returning false"); + return false; + } + if (contentFitsIntoTable(table)) { + LOGGER.debug("RWE1"); + Map, Double> expansionRatio = determineExpansionRatio(resizableColumns); + return rearrangeColumns(table, resizableColumns, expansionRatio); + } else { + LOGGER.debug("RWE2, RWE3, RWS1, RWS2"); + boolean contentHasFitIntoTable = getContentWidth(table) - EPSILON_MARGIN <= previousTableWidth; + LOGGER.debug("contentHasFitIntoTable {} = getContentWidth(table) {} <= previousTableWidth {}", contentHasFitIntoTable, getContentWidth(table), previousTableWidth); + if (!contentHasFitIntoTable) { + LOGGER.debug("RWS2"); + return false; + } + Map, Double> shrinkageRatio = determineShrinkageRatio(resizableColumns); + return rearrangeColumns(table, resizableColumns, shrinkageRatio); + } + } - resize(col, newSize - col.getWidth()); + /** + * case RC0 + */ + private boolean doColumnChange(TableView table, TableColumn userChangedColumn, Double sizeChange) { + LOGGER.debug("Start RC0 with column {} sizeChange {}", userChangedColumn.getText(), sizeChange); + if (table.getWidth() == 0.0d) { + LOGGER.error("Table width is 0. Returning false"); + return false; + } + + return determineNewWidth(userChangedColumn, sizeChange).map(newWidth -> { + LOGGER.debug("Checking if content has fit into table before"); + boolean contentHasFitIntoTableBefore = contentFitsIntoTable(table); + LOGGER.debug("Result: contentHasFitIntoTableBefore: {}", contentHasFitIntoTableBefore); + userChangedColumn.setPrefWidth(newWidth); + LOGGER.trace("New width set"); + List> otherResizableColumns = resizableColumns.stream().filter(column -> !column.equals(userChangedColumn)).collect(Collectors.toList()); + if (contentHasFitIntoTableBefore) { + LOGGER.debug("RCE1, RCS1"); + Map, Double> shrinkageRatio = determineShrinkageRatio(otherResizableColumns); + rearrangeColumns(table, otherResizableColumns, shrinkageRatio); + } else { + if (sizeChange >= 0) { + LOGGER.debug("RCE2"); + // no more action, because other columns are not changed. + } else { + if (contentFitsIntoTable(table)) { + LOGGER.debug("RCS2"); + Map, Double> expansionRatio = determineExpansionRatio(otherResizableColumns); + rearrangeColumns(table, otherResizableColumns, expansionRatio); + } else { + LOGGER.debug("RCS3"); + // no more action, because other columns are not changed. + } } } + return true; + }).orElse(false); + } + + private boolean contentFitsIntoTable(TableView table) { + double tableWidth = table.getWidth(); + Double contentWidth = getContentWidth(table); + boolean comparisonResult = tableWidth >= contentWidth; + LOGGER.debug("tableWidth {} >= contentWidth {}: {}", tableWidth, contentWidth, comparisonResult); + return comparisonResult; + } + + /** + * This way, the proportions of the columns are kept during resize of the window + */ + private Map, Double> determineExpansionRatioWithoutColumn(TableColumn excludedVisibleColumn) { + List> columns = resizableColumns.stream().filter(column -> !column.equals(excludedVisibleColumn)).collect(Collectors.toList()); + return determineExpansionRatio(columns); + } + + /** + * Determines the share of the given columns + * Invariant: sum(shares) = 100% + */ + private Map, Double> determineExpansionRatio(List> columns) { + Map, Double> expansionRatio = new HashMap<>(); + + // We need to store the initial preferred width, because "setWidth()" does not exist + // There is only "setMinWidth", "setMaxWidth", and "setPrefWidth + Double allColumnsWidth = columns.stream().mapToDouble(TableColumn::getWidth).sum(); + LOGGER.debug("allColumnsWidth: {}", allColumnsWidth); + for (TableColumn column : columns) { + double share = column.getWidth() / allColumnsWidth; + LOGGER.debug("share of {}: {}", column.getText(), share); + expansionRatio.put(column, share); + } + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Sum of shares: {}", expansionRatio.values().stream().mapToDouble(Double::doubleValue).sum()); } + return expansionRatio; + } - return false; + /** + * Determines the shrinkage ratio. It is a uniform distribution. + */ + private Map, Double> determineShrinkageRatio(List> resizableColumns) { + Map, Double> shrinkageRatio = new HashMap<>(); + Double share = 1.0 / resizableColumns.size(); + resizableColumns.forEach(tableColumn -> shrinkageRatio.put(tableColumn, share)); + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("Sum of shares: {}", shrinkageRatio.values().stream().mapToDouble(Double::doubleValue).sum()); + } + return shrinkageRatio; } - private void resize(TableColumnBase column, double delta) { - // We have to use reflection since TableUtil is not visible to us - try { - // TODO: reflective access, should be removed - Class clazz = Class.forName("javafx.scene.control.TableUtil"); - Method constrainedResize = clazz.getDeclaredMethod("resize", TableColumnBase.class, double.class); - constrainedResize.setAccessible(true); - constrainedResize.invoke(null, column, delta); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) { - LOGGER.error("Could not invoke resize in TableUtil", e); + /** + * Determines the new width of the column based on the requested delta. It respects the min and max width of the column. + *
+ * This method is also called if the table content is wider than the window. Thus, it does not respect the overall table width; + * "just" the width constraints of the given column + * + * @param column The column the resize is requested + * @param delta The delta requested + * @return the new size, Optional.empty() if no resize is possible + */ + private Optional determineNewWidth(TableColumn column, Double delta) { + // This is com.sun.javafx.scene.control.skin.Utils.boundedSize with more comments and Optionals + + LOGGER.trace("Column {}", column.getText()); + LOGGER.trace("Requested delta {}", delta); + + // Calculate newWidth based on delta and constraint of the column + double oldWidth = column.getWidth(); + double newWidth; + if (delta < 0) { + double minWidth = getMinWidthThreshold(column); + LOGGER.trace("getMinWidthThreshold {}", minWidth); + newWidth = Math.max(minWidth, oldWidth + delta); + LOGGER.trace("newWidth {} = Math.max(minWidth {}, oldWidth {} + delta {})", newWidth, minWidth, oldWidth, delta); + } else { + double maxWidth = column.getMaxWidth(); + LOGGER.trace("MaxWidth {}", maxWidth); + newWidth = Math.min(maxWidth, oldWidth + delta); + LOGGER.trace("newWidth {} = Math.min(maxWidth {}, oldWidth {} + delta {})", newWidth, maxWidth, oldWidth, delta); + } + LOGGER.trace("Truncating width"); + newWidth = Math.floor(newWidth * 1.0E10) / 1.0E10; + LOGGER.trace("Size proposal: {} -> {}", oldWidth, newWidth); + if (oldWidth == newWidth) { + return Optional.empty(); } + return Optional.of(newWidth); } - private Boolean constrainedResize(TableView.ResizeFeatures prop) { - TableView table = prop.getTable(); - List> visibleLeafColumns = table.getVisibleLeafColumns(); - return constrainedResize(prop, - false, - getContentWidth(table) - 2, - visibleLeafColumns); - } - - private Boolean constrainedResize(TableView.ResizeFeatures prop, Boolean isFirstRun, Double contentWidth, List> visibleLeafColumns) { - // We have to use reflection since TableUtil is not visible to us - try { - // TODO: reflective access, should be removed - Class clazz = Class.forName("javafx.scene.control.TableUtil"); - Method constrainedResize = clazz.getDeclaredMethod("constrainedResize", ResizeFeaturesBase.class, Boolean.TYPE, Double.TYPE, List.class); - constrainedResize.setAccessible(true); - Object returnValue = constrainedResize.invoke(null, prop, isFirstRun, contentWidth, visibleLeafColumns); - return (Boolean) returnValue; - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) { - LOGGER.error("Could not invoke constrainedResize in TableUtil", e); - return false; + /** + * Completely rearranges the given columnsToResize so that the complete table content fits into the table space (in case threshold of columns is not hit) + *

+ * Handles cases I2, I3, RWE1 to RWE3, and RWS1 and RWS2 + * + * @param table The table to handle + * @param columnsToResize The columns allowed to change the widths + * @param ratio The expansion/shrinkage ratio of the columns + */ + private boolean rearrangeColumns(TableView table, List> columnsToResize, Map, Double> ratio) { + Double tableWidth = table.getWidth(); + Double newContentWidth; + boolean aWidthChanged = false; + + // We need a second in case the EPSILON_MARGIN condition cannot be fulfilled + // This is for instance the case: + // - New table width: 830 + // - 9 Columns à 100 threshold + // - old table width: 900 + // - --> 70 pixels should be shrunk + // - Columns should be shrunk + // - Columns cannot be shrunk because of threshold + // Therefore, we introduce an iterations counter to have a safety addition to the termination condition. + int iterations = 0; + + double remainingPixels; + do + { + iterations++; + // in case the userChosenColumnToResize got bigger, the remaining available width will get below 0 --> other columns need to be shrunk + LOGGER.debug("tableWidth {}", tableWidth); + Double contentWidth = getContentWidth(table); + LOGGER.debug("contentWidth {}", contentWidth); + Double remainingAvailableWidth = tableWidth - contentWidth; + // Double remainingAvailableWidth = Math.max(0, tableWidth - contentWidth); + LOGGER.debug("Distributing remainingAvailableWidth {}", remainingAvailableWidth); + for (TableColumn column : columnsToResize) { + LOGGER.debug("Column {}", column.getText()); + double share = ratio.get(column); + // Precondition in our case: column has to have minimum width + double delta = share * remainingAvailableWidth; + LOGGER.debug("share {} * remainingAvailableWidth {} = delta {}", share, remainingAvailableWidth, delta); + Optional newWidth = determineNewWidth(column, delta); + if (newWidth.isPresent()) { + // in case we can do something, do it + // otherwise, the next loop iteration will distribute it + aWidthChanged = true; + column.setPrefWidth(newWidth.get()); + } + } + + newContentWidth = getContentWidth(table); + LOGGER.debug("newContentWidth {}", newContentWidth); + + remainingPixels = Math.abs(tableWidth - newContentWidth); + LOGGER.debug("|tableWidth - newContentWidth| = {}", remainingPixels); + } while (remainingPixels > EPSILON_MARGIN && iterations <= 2); + + // Special case if due to rounding errors contentWidth is still larger than tableWidth + // E.g., contentWidth - tableWidth = 0.0000000000002 + if (remainingPixels < 1) { + columnsToResize.stream().filter(column -> column.getWidth() > column.getMinWidth() + 1).findFirst(); + TableColumn columnToAbsorbEpsilon = columnsToResize.get(0); + columnToAbsorbEpsilon.setPrefWidth(columnToAbsorbEpsilon.getWidth() - remainingPixels); } + + LOGGER.debug("aWidthChanged is {}", aWidthChanged); + return aWidthChanged; } + /** + * Computes and returns the width required by the content of the table + */ private Double getContentWidth(TableView table) { - try { - // TODO: reflective access, should be removed - Field privateStringField = TableView.class.getDeclaredField("contentWidth"); - privateStringField.setAccessible(true); - return (Double) privateStringField.get(table); - } catch (IllegalAccessException | NoSuchFieldException e) { - return 0d; - } + // The current table content width contains all visible columns: the resizable ones and the non-resizable ones + return table.getVisibleLeafColumns().stream().mapToDouble(TableColumn::getWidth).sum(); } } diff --git a/src/main/java/org/jabref/gui/preferences/table/TableTab.fxml b/src/main/java/org/jabref/gui/preferences/table/TableTab.fxml index c65b263263f..5d5c482f97b 100644 --- a/src/main/java/org/jabref/gui/preferences/table/TableTab.fxml +++ b/src/main/java/org/jabref/gui/preferences/table/TableTab.fxml @@ -99,8 +99,8 @@ text="%Write values of special fields as separate fields to BibTeX" toggleGroup="$specialFieldsStoreMode"/> - - + diff --git a/src/main/java/org/jabref/gui/preferences/table/TableTab.java b/src/main/java/org/jabref/gui/preferences/table/TableTab.java index 4ce286ccd4d..f7ffee93709 100644 --- a/src/main/java/org/jabref/gui/preferences/table/TableTab.java +++ b/src/main/java/org/jabref/gui/preferences/table/TableTab.java @@ -37,8 +37,7 @@ public class TableTab extends AbstractPreferenceTabView imple @FXML private Button specialFieldsHelp; @FXML private RadioButton specialFieldsSyncKeywords; @FXML private RadioButton specialFieldsSerialize; - @FXML private CheckBox extraFileColumnsEnable; - @FXML private CheckBox autoResizeColumns; + @FXML private CheckBox dedicatedFileColumnsEnable; @FXML private RadioButton namesNatbib; @FXML private RadioButton nameAsIs; @@ -120,8 +119,7 @@ private void setupBindings() { specialFieldsEnable.selectedProperty().bindBidirectional(viewModel.specialFieldsEnabledProperty()); specialFieldsSyncKeywords.selectedProperty().bindBidirectional(viewModel.specialFieldsSyncKeywordsProperty()); specialFieldsSerialize.selectedProperty().bindBidirectional(viewModel.specialFieldsSerializeProperty()); - extraFileColumnsEnable.selectedProperty().bindBidirectional(viewModel.extraFileColumnsEnabledProperty()); - autoResizeColumns.selectedProperty().bindBidirectional(viewModel.autoResizeColumnsProperty()); + dedicatedFileColumnsEnable.selectedProperty().bindBidirectional(viewModel.extraFileColumnsEnabledProperty()); namesNatbib.selectedProperty().bindBidirectional(viewModel.namesNatbibProperty()); nameAsIs.selectedProperty().bindBidirectional(viewModel.nameAsIsProperty()); diff --git a/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java b/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java index 1ab98f3905b..4edc4e19ee0 100644 --- a/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/table/TableTabViewModel.java @@ -20,7 +20,6 @@ import org.jabref.gui.maintable.ColumnPreferences; import org.jabref.gui.maintable.MainTableColumnModel; import org.jabref.gui.maintable.MainTableNameFormatPreferences; -import org.jabref.gui.maintable.MainTablePreferences; import org.jabref.gui.preferences.PreferenceTabViewModel; import org.jabref.gui.specialfields.SpecialFieldsPreferences; import org.jabref.gui.util.NoSelectionModel; @@ -64,8 +63,7 @@ public MainTableColumnModel fromString(String string) { private final BooleanProperty specialFieldsEnabledProperty = new SimpleBooleanProperty(); private final BooleanProperty specialFieldsSyncKeywordsProperty = new SimpleBooleanProperty(); private final BooleanProperty specialFieldsSerializeProperty = new SimpleBooleanProperty(); - private final BooleanProperty extraFileColumnsEnabledProperty = new SimpleBooleanProperty(); - private final BooleanProperty autoResizeColumnsProperty = new SimpleBooleanProperty(); + private final BooleanProperty dedicatedFileColumnsEnabledProperty = new SimpleBooleanProperty(); private final BooleanProperty namesNatbibProperty = new SimpleBooleanProperty(); private final BooleanProperty nameAsIsProperty = new SimpleBooleanProperty(); @@ -97,7 +95,7 @@ public TableTabViewModel(DialogService dialogService, PreferencesService prefere } }); - extraFileColumnsEnabledProperty.addListener((observable, oldValue, newValue) -> { + dedicatedFileColumnsEnabledProperty.addListener((observable, oldValue, newValue) -> { if (newValue) { insertExtraFileColumns(); } else { @@ -116,16 +114,14 @@ public TableTabViewModel(DialogService dialogService, PreferencesService prefere @Override public void setValues() { - MainTablePreferences initialMainTablePreferences = preferences.getMainTablePreferences(); - initialColumnPreferences = initialMainTablePreferences.getColumnPreferences(); + initialColumnPreferences = preferences.getColumnPreferences(); initialSpecialFieldsPreferences = preferences.getSpecialFieldsPreferences(); MainTableNameFormatPreferences initialNameFormatPreferences = preferences.getMainTableNameFormatPreferences(); specialFieldsEnabledProperty.setValue(initialSpecialFieldsPreferences.isSpecialFieldsEnabled()); specialFieldsSyncKeywordsProperty.setValue(initialSpecialFieldsPreferences.shouldAutoSyncSpecialFieldsToKeyWords()); specialFieldsSerializeProperty.setValue(initialSpecialFieldsPreferences.shouldSerializeSpecialFields()); - extraFileColumnsEnabledProperty.setValue(initialMainTablePreferences.getExtraFileColumnsEnabled()); - autoResizeColumnsProperty.setValue(initialMainTablePreferences.getResizeColumnsToFit()); + dedicatedFileColumnsEnabledProperty.setValue(initialColumnPreferences.isDedicatedFileColumnsEnabled()); fillColumnList(); @@ -151,7 +147,7 @@ public void setValues() { insertSpecialFieldColumns(); } - if (extraFileColumnsEnabledProperty.getValue()) { + if (dedicatedFileColumnsEnabledProperty.getValue()) { insertExtraFileColumns(); } @@ -241,13 +237,11 @@ public void moveColumnDown() { @Override public void storeSettings() { - MainTablePreferences newMainTablePreferences = preferences.getMainTablePreferences(); - preferences.storeMainTablePreferences(new MainTablePreferences( + preferences.storeColumnPreferences( new ColumnPreferences( columnsListProperty.getValue(), - newMainTablePreferences.getColumnPreferences().getColumnSortOrder()), - autoResizeColumnsProperty.getValue(), - extraFileColumnsEnabledProperty.getValue() + initialColumnPreferences.getColumnSortOrder(), + dedicatedFileColumnsEnabledProperty.getValue() )); SpecialFieldsPreferences newSpecialFieldsPreferences = new SpecialFieldsPreferences( @@ -328,11 +322,7 @@ public BooleanProperty specialFieldsSerializeProperty() { } public BooleanProperty extraFileColumnsEnabledProperty() { - return this.extraFileColumnsEnabledProperty; - } - - public BooleanProperty autoResizeColumnsProperty() { - return autoResizeColumnsProperty; + return this.dedicatedFileColumnsEnabledProperty; } public BooleanProperty namesNatbibProperty() { diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 325898a86c8..4464ee576f9 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -50,7 +50,6 @@ import org.jabref.gui.maintable.MainTableNameFormatPreferences; import org.jabref.gui.maintable.MainTableNameFormatPreferences.AbbreviationStyle; import org.jabref.gui.maintable.MainTableNameFormatPreferences.DisplayStyle; -import org.jabref.gui.maintable.MainTablePreferences; import org.jabref.gui.mergeentries.MergeEntries; import org.jabref.gui.search.SearchDisplayMode; import org.jabref.gui.specialfields.SpecialFieldsPreferences; @@ -191,7 +190,10 @@ public class JabRefPreferences implements PreferencesService { public static final String KEYWORD_SEPARATOR = "groupKeywordSeparator"; public static final String AUTO_ASSIGN_GROUP = "autoAssignGroup"; public static final String DISPLAY_GROUP_COUNT = "displayGroupCount"; - public static final String EXTRA_FILE_COLUMNS = "extraFileColumns"; + + // different content of the constant because of backwards compatibility + public static final String DEDICATED_FILE_COLUMNS = "extraFileColumns"; + public static final String OVERRIDE_DEFAULT_FONT_SIZE = "overrideDefaultFontSize"; public static final String MAIN_FONT_SIZE = "mainFontSize"; @@ -577,7 +579,7 @@ private JabRefPreferences() { defaults.put(MEMORY_STICK_MODE, Boolean.FALSE); defaults.put(SHOW_ADVANCED_HINTS, Boolean.TRUE); - defaults.put(EXTRA_FILE_COLUMNS, Boolean.FALSE); + defaults.put(DEDICATED_FILE_COLUMNS, Boolean.FALSE); defaults.put(PROTECTED_TERMS_ENABLED_INTERNAL, convertListToString(ProtectedTermsLoader.getInternalLists())); defaults.put(PROTECTED_TERMS_DISABLED_INTERNAL, ""); @@ -1913,7 +1915,8 @@ private void updateColumnSortOrder() { public ColumnPreferences getColumnPreferences() { return new ColumnPreferences( createMainTableColumns(), - createMainTableColumnSortOrder()); + createMainTableColumnSortOrder(), + getBoolean(DEDICATED_FILE_COLUMNS, false)); } /** @@ -1940,24 +1943,12 @@ public void storeColumnPreferences(ColumnPreferences columnPreferences) { .map(MainTableColumnModel::getName) .collect(Collectors.toList())); + putBoolean(DEDICATED_FILE_COLUMNS, columnPreferences.isDedicatedFileColumnsEnabled()); + // Update cache mainTableColumns = columnPreferences.getColumns(); } - @Override - public MainTablePreferences getMainTablePreferences() { - return new MainTablePreferences(getColumnPreferences(), - getBoolean(AUTO_RESIZE_MODE), - getBoolean(EXTRA_FILE_COLUMNS)); - } - - @Override - public void storeMainTablePreferences(MainTablePreferences mainTablePreferences) { - storeColumnPreferences(mainTablePreferences.getColumnPreferences()); - putBoolean(AUTO_RESIZE_MODE, mainTablePreferences.getResizeColumnsToFit()); - putBoolean(EXTRA_FILE_COLUMNS, mainTablePreferences.getExtraFileColumnsEnabled()); - } - @Override public MainTableNameFormatPreferences getMainTableNameFormatPreferences() { DisplayStyle displayStyle = DisplayStyle.LASTNAME_FIRSTNAME; // default diff --git a/src/main/java/org/jabref/preferences/PreferencesService.java b/src/main/java/org/jabref/preferences/PreferencesService.java index 869e87515b6..de8c8d48d5a 100644 --- a/src/main/java/org/jabref/preferences/PreferencesService.java +++ b/src/main/java/org/jabref/preferences/PreferencesService.java @@ -15,7 +15,6 @@ import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.maintable.ColumnPreferences; import org.jabref.gui.maintable.MainTableNameFormatPreferences; -import org.jabref.gui.maintable.MainTablePreferences; import org.jabref.gui.specialfields.SpecialFieldsPreferences; import org.jabref.gui.util.Theme; import org.jabref.logic.JabRefException; @@ -223,10 +222,6 @@ public interface PreferencesService { void storeColumnPreferences(ColumnPreferences columnPreferences); - MainTablePreferences getMainTablePreferences(); - - void storeMainTablePreferences(MainTablePreferences mainTablePreferences); - MainTableNameFormatPreferences getMainTableNameFormatPreferences(); void storeMainTableNameFormatPreferences(MainTableNameFormatPreferences preferences); diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index fa058688e00..50d6455da44 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -350,7 +350,7 @@ Finished\ writing\ metadata\ for\ %0\ file\ (%1\ skipped,\ %2\ errors).=Finished First\ select\ the\ entries\ you\ want\ keys\ to\ be\ generated\ for.=First select the entries you want keys to be generated for. -Fit\ table\ horizontally\ on\ screen=Fit table horizontally on screen +Show\ dedicated\ file\ columns=Show dedicated file columns Float=Float Format\:\ Tab\:field;field;...\ (e.g.\ General\:url;pdf;note...)=Format\: Tab\:field;field;... (e.g. General\:url;pdf;note...) diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index f9e6c0bf60b..b2bce92f4d2 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -2,10 +2,13 @@ - + + + + @@ -15,6 +18,9 @@ + + +