Skip to content

Commit

Permalink
Add auto group colour assignment (JabRef#10521)
Browse files Browse the repository at this point in the history
* Add auto group colour assignment

* Add clickable option for auto colour assignment

* Change button to checkbox

* Start working on using the context of the current group for coloring

Co-authored-by: Carl Christian Snethlage <[email protected]>

* First short for colorful subgroups

* When creating a new group, it inhertics the icon of the parent group

* Add checkbox for coloring of groups

Co-authored-by: Carl Christian Snethlage <[email protected]>

* Help button leaves dialog opened

Co-authored-by: Carl Christian Snethlage <[email protected]>

* Fix missing "Collect by" header

Co-authored-by: Carl Christian Snethlage <[email protected]>

---------

Co-authored-by: Oliver Kopp <[email protected]>
Co-authored-by: Carl Christian Snethlage <[email protected]>
  • Loading branch information
3 people authored Dec 23, 2023
1 parent 82f1749 commit 31467be
Show file tree
Hide file tree
Showing 13 changed files with 258 additions and 41 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We added [scholar.archive.org](https://scholar.archive.org/) as a new fetcher. [#10498](https://github.com/JabRef/jabref/issues/10498)
- We integrated predatory journal checking as part of the Integrity Checker based on the [check-bib-for-predatory](https://github.com/CfKu/check-bib-for-predatory). [koppor#348](https://github.com/koppor/jabref/issues/348)
- We added a 'More options' section in the main table right click menu opening the preferences dialog. [#9432](https://github.com/JabRef/jabref/issues/9432)
- When creating a new group, it inherits the icon of the parent group. [#10521](https://github.com/JabRef/jabref/pull/10521)

### Changed

Expand Down
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ dependencies {
// Allow objects "magically" to be mapped to JSON using GSON
// implementation 'org.glassfish.jersey.media:jersey-media-json-gson:3.1.1'

// Because of GraalVM quirks, we need to ship that. See https://github.com/jspecify/jspecify/issues/389#issuecomment-1661130973 for details
implementation 'org.jspecify:jspecify:0.3.0'

testImplementation 'io.github.classgraph:classgraph:4.8.165'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
testImplementation 'org.junit.platform:junit-platform-launcher:1.10.1'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@
uses org.eclipse.jgit.transport.SshSessionFactory;
uses org.eclipse.jgit.lib.GpgSigner;

requires transitive org.jspecify;

// other libraries
requires org.antlr.antlr4.runtime;
requires org.libreoffice.uno;
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/jabref/gui/actions/StandardActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ public enum StandardActions implements Action {
DELETE_FILE(Localization.lang("Permanently delete local file"), IconTheme.JabRefIcons.DELETE_FILE, KeyBinding.DELETE_ENTRY),

HELP(Localization.lang("Online help"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP),
HELP_GROUPS(Localization.lang("Open Help page"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP),
HELP_KEY_PATTERNS(Localization.lang("Help on key patterns"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP),
HELP_REGEX_SEARCH(Localization.lang("Help on regular expression search"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP),
HELP_NAME_FORMATTER(Localization.lang("Help on Name Formatting"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP),
Expand Down
62 changes: 62 additions & 0 deletions src/main/java/org/jabref/gui/groups/GroupColorPicker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.jabref.gui.groups;

import java.util.List;

import javafx.scene.paint.Color;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public class GroupColorPicker {

// Generate color for top groups
public static Color generateColor(List<Color> siblingColors) {
return generateColor(siblingColors, null);
}

/**
* Algorithm optimized for colors, not for gray-scale (where it does not work)
*/
public static Color generateColor(List<Color> siblingColors, @Nullable Color parentColor) {
if (siblingColors.isEmpty()) {
if (parentColor == null) {
// We need something colorful to derive other colors based on the color
return Color.hsb(Math.random() * 360.0, .50, .75);
}
return generateSubGroupColor(parentColor);
}

double sumSin = 0;
double sumCos = 0;

// Calculate the mean angle
for (Color color : siblingColors) {
double hue = color.getHue();
sumSin += Math.sin(Math.toRadians(hue));
sumCos += Math.cos(Math.toRadians(hue));
}

double meanAngle = Math.toDegrees(Math.atan2(sumSin, sumCos));
meanAngle = (meanAngle + 360) % 360;

// The opposite angle is potentially the point of maximum average distance
double newHue = (meanAngle + 180) % 360;

double sumSaturation = 0;
double sumBrightness = 0;
for (Color color : siblingColors) {
sumSaturation += color.getSaturation();
sumBrightness += color.getBrightness();
}

double averageSaturation = sumSaturation / siblingColors.size();
double averageBrightness = sumBrightness / siblingColors.size();

return Color.hsb(newHue, averageSaturation, averageBrightness);
}

private static Color generateSubGroupColor(Color baseColor) {
return baseColor.deriveColor(0.0, 1.0, .9, 1.0);
}
}
37 changes: 19 additions & 18 deletions src/main/java/org/jabref/gui/groups/GroupDialog.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<TextField fx:id="descriptionField"/>
</VBox>
<HBox spacing="10.0">
<VBox HBox.hgrow="ALWAYS">
<VBox HBox.hgrow="SOMETIMES">
<Label text="%Icon"/>
<HBox>
<TextField fx:id="iconField"/>
Expand All @@ -48,27 +48,25 @@
</Button>
</HBox>
</VBox>
<VBox layoutX="10.0" layoutY="10.0" minWidth="130.0" maxWidth="130.0" prefWidth="130.0">
<Label text="%Color"/>
<ColorPicker fx:id="colorField"/>
</VBox>
<VBox>
<VBox HBox.hgrow="SOMETIMES">
<Label text="%Hierarchical context"/>
<ComboBox fx:id="hierarchicalContextCombo" minWidth="130.0" maxWidth="130.0" prefWidth="130.0"/>
</VBox>
<VBox HBox.hgrow="SOMETIMES"
layoutX="10.0" layoutY="10.0"
minWidth="130.0" maxWidth="130.0"
prefWidth="130.0" spacing="10.0">
<CheckBox fx:id="colorUseCheckbox" text="%Color"/>
<VBox disable="${!colorUseCheckbox.selected}">
<ColorPicker fx:id="colorField" minHeight="25.0"/>
<CheckBox fx:id="autoColorCheckbox" text="Auto Color"/>
<padding>
<Insets left="20.0"/>
</padding>
</VBox>
</VBox>
</HBox>
<HBox alignment="BOTTOM_LEFT">
<Label text="%Collect by" styleClass="sectionHeader"/>
<HBox HBox.hgrow="ALWAYS"/>
<Button styleClass="icon-button,narrow" onAction="#openHelp">
<graphic>
<JabRefIconView glyph="HELP"/>
</graphic>
<tooltip>
<Tooltip text="%Open Help page"/>
</tooltip>
</Button>
</HBox>
<Label text="%Collect by" styleClass="sectionHeader" style="-fx-padding: 0 10 0 0;"/>
<HBox>
<VBox spacing="10.0">
<RadioButton fx:id="explicitRadioButton" toggleGroup="$type" wrapText="true"
Expand Down Expand Up @@ -181,4 +179,7 @@
</HBox>
</VBox>
</content>
<padding>
<Insets bottom="10.0" left="15.0" right="10.0" top="10.0" />
</padding>
</DialogPane>
81 changes: 72 additions & 9 deletions src/main/java/org/jabref/gui/groups/GroupDialogView.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.EnumMap;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;

import javafx.application.Platform;
Expand All @@ -13,6 +15,7 @@
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ColorPicker;
Expand All @@ -27,15 +30,21 @@
import javafx.scene.paint.Color;

import org.jabref.gui.DialogService;
import org.jabref.gui.Globals;
import org.jabref.gui.actions.ActionFactory;
import org.jabref.gui.actions.StandardActions;
import org.jabref.gui.help.HelpAction;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.icon.JabrefIconProvider;
import org.jabref.gui.util.BaseDialog;
import org.jabref.gui.util.IconValidationDecorator;
import org.jabref.gui.util.ViewModelListCellFactory;
import org.jabref.logic.help.HelpFile;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.groups.AbstractGroup;
import org.jabref.model.groups.GroupHierarchyType;
import org.jabref.model.groups.GroupTreeNode;
import org.jabref.model.search.rules.SearchRules;
import org.jabref.model.search.rules.SearchRules.SearchFlags;
import org.jabref.model.util.FileUpdateMonitor;
Expand All @@ -48,19 +57,24 @@
import org.controlsfx.control.GridView;
import org.controlsfx.control.PopOver;
import org.controlsfx.control.textfield.CustomTextField;
import org.jspecify.annotations.Nullable;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.IkonProvider;
import org.kordamp.ikonli.javafx.FontIcon;

public class GroupDialogView extends BaseDialog<AbstractGroup> {

private static boolean useAutoColoring = false;

// Basic Settings
@FXML private TextField nameField;
@FXML private TextField descriptionField;
@FXML private TextField iconField;
@FXML private Button iconPickerButton;
@FXML private CheckBox colorUseCheckbox;
@FXML private ColorPicker colorField;
@FXML private ComboBox<GroupHierarchyType> hierarchicalContextCombo;
@FXML private CheckBox autoColorCheckbox;

// Type
@FXML private RadioButton explicitRadioButton;
Expand Down Expand Up @@ -94,16 +108,21 @@ public class GroupDialogView extends BaseDialog<AbstractGroup> {
private final ControlsFxVisualizer validationVisualizer = new ControlsFxVisualizer();

private final BibDatabaseContext currentDatabase;
private final AbstractGroup editedGroup;
private final @Nullable GroupTreeNode parentNode;
private final @Nullable AbstractGroup editedGroup;

private GroupDialogViewModel viewModel;

@Inject private FileUpdateMonitor fileUpdateMonitor;
@Inject private DialogService dialogService;
@Inject private PreferencesService preferencesService;

public GroupDialogView(BibDatabaseContext currentDatabase,
AbstractGroup editedGroup,
@Nullable GroupTreeNode parentNode,
@Nullable AbstractGroup editedGroup,
GroupDialogHeader groupDialogHeader) {
this.currentDatabase = currentDatabase;
this.parentNode = parentNode;
this.editedGroup = editedGroup;

ViewLoader.view(this)
Expand All @@ -120,17 +139,41 @@ public GroupDialogView(BibDatabaseContext currentDatabase,
this.setTitle(Localization.lang("Edit group") + " " + editedGroup.getName());
}

getDialogPane().getButtonTypes().setAll(ButtonType.OK, ButtonType.CANCEL);
ButtonType helpButtonType = new ButtonType("", ButtonBar.ButtonData.HELP_2);
getDialogPane().getButtonTypes().setAll(helpButtonType, ButtonType.OK, ButtonType.CANCEL);

final Button confirmDialogButton = (Button) getDialogPane().lookupButton(ButtonType.OK);
final Button helpButton = (Button) getDialogPane().lookupButton(helpButtonType);

ActionFactory actionFactory = new ActionFactory(Globals.getKeyPrefs());
HelpAction helpAction = new HelpAction(HelpFile.GROUPS, dialogService, preferencesService.getFilePreferences());
actionFactory.configureIconButton(
StandardActions.HELP_GROUPS,
helpAction,
helpButton);

// Consume the dialog close event, but execute the help action
helpButton.addEventFilter(ActionEvent.ACTION, event -> {
helpAction.execute();
event.consume();
});

confirmDialogButton.disableProperty().bind(viewModel.validationStatus().validProperty().not());
// handle validation before closing dialog and calling resultConverter
confirmDialogButton.addEventFilter(ActionEvent.ACTION, viewModel::validationHandler);
}

private @Nullable AbstractGroup parentGroup() {
if (parentNode == null) {
return null;
} else {
return parentNode.getGroup();
}
}

@FXML
public void initialize() {
viewModel = new GroupDialogViewModel(dialogService, currentDatabase, preferencesService, editedGroup, fileUpdateMonitor);
viewModel = new GroupDialogViewModel(dialogService, currentDatabase, preferencesService, editedGroup, parentNode, fileUpdateMonitor);

setResultConverter(viewModel::resultConverter);

Expand All @@ -144,6 +187,7 @@ public void initialize() {
nameField.textProperty().bindBidirectional(viewModel.nameProperty());
descriptionField.textProperty().bindBidirectional(viewModel.descriptionProperty());
iconField.textProperty().bindBidirectional(viewModel.iconProperty());
colorUseCheckbox.selectedProperty().bindBidirectional(viewModel.colorUseProperty());
colorField.valueProperty().bindBidirectional(viewModel.colorFieldProperty());
hierarchicalContextCombo.itemsProperty().bind(viewModel.groupHierarchyListProperty());
new ViewModelListCellFactory<GroupHierarchyType>()
Expand Down Expand Up @@ -207,18 +251,36 @@ public void initialize() {
validationVisualizer.initVisualization(viewModel.texGroupFilePathValidatonStatus(), texGroupFilePath);
nameField.requestFocus();
});

autoColorCheckbox.setSelected(useAutoColoring);
autoColorCheckbox.setOnAction(event -> {
useAutoColoring = autoColorCheckbox.isSelected();
if (!autoColorCheckbox.isSelected()) {
return;
}
if (parentNode == null) {
viewModel.colorFieldProperty().setValue(IconTheme.getDefaultGroupColor());
return;
}
List<Color> colorsOfSiblings = parentNode.getChildren().stream().map(child -> child.getGroup().getColor())
.flatMap(Optional::stream)
.toList();
Optional<Color> parentColor = parentGroup().getColor();
Color color;
if (parentColor.isEmpty()) {
color = GroupColorPicker.generateColor(colorsOfSiblings);
} else {
color = GroupColorPicker.generateColor(colorsOfSiblings, parentColor.get());
}
viewModel.colorFieldProperty().setValue(color);
});
}

@FXML
private void texGroupBrowse() {
viewModel.texGroupBrowse();
}

@FXML
private void openHelp() {
viewModel.openHelpPage();
}

@FXML
private void openIconPicker() {
ObservableList<Ikon> ikonList = FXCollections.observableArrayList();
Expand Down Expand Up @@ -249,6 +311,7 @@ private void openIconPicker() {

// Necessary because of a bug in controlsfx GridView
// https://github.com/controlsfx/controlsfx/issues/1400
// The issue is closed, but still appears here
Platform.runLater(() -> {
ikonGridView.setItems(filteredList);
});
Expand Down
Loading

0 comments on commit 31467be

Please sign in to comment.