Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let user load customized CSS #6036

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
f402fd2
feat/#1 Add CSS file type, add button in preferences to import custom…
nilsstre Feb 26, 2020
7bf2fed
feat/#1 Change so that the log messages uses format specifiers instea…
nilsstre Feb 26, 2020
b27f26d
feat/#1 Add RadioButton for toggling custom theme
nilsstre Feb 26, 2020
8cb3a50
feat/#1 Add preference for setting path to custom CSS theme
nilsstre Feb 26, 2020
bd6c561
feat/#1 Load custom CSS if toggled
nilsstre Feb 26, 2020
6a5f73c
feat/#1 Add missing language keys
nilsstre Feb 26, 2020
b5b56bf
feat/#1 Remove check if current theme is applied again, check is remo…
nilsstre Feb 26, 2020
99d9768
feat/#1 Save path to custom CSS file in program preferences
nilsstre Feb 26, 2020
bcf8004
Add functionality to let the user import custom CSS file #5790
nilsstre Feb 26, 2020
f2fc2ab
fix/#5 Add checks so that the theme change notification is only shown…
nilsstre Feb 27, 2020
582c9d5
Fix merge conflict, fixes #5
nilsstre Feb 27, 2020
198be47
Remove added stuff from merge conflict
nilsstre Feb 27, 2020
00eac4e
Add export current theme #5790
nilsstre Feb 27, 2020
9fc3382
Add missing language lines
nilsstre Feb 27, 2020
90fc805
Merge branch 'master' into addCSSCustomisation
nilsstre Feb 27, 2020
4f0db24
Add information about import/export of themes, #5790
nilsstre Feb 27, 2020
a8feb94
Fix CodaCy and checkstyle issues, #5790
nilsstre Feb 27, 2020
8c1e2c2
Add fixes from code review, #5790
nilsstre Feb 27, 2020
e4a4f68
Remove unused import #5790
nilsstre Feb 27, 2020
00d8ef6
Move the import/export buttons to the Appearance tab #5790
nilsstre Feb 27, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `#
- Filenames of external files can no longer contain curly braces. [#5926](https://github.com/JabRef/jabref/pull/5926)
- We made the filters more easily accessible in the integrity check dialog. [#5955](https://github.com/JabRef/jabref/pull/5955)
- We reimplemented and improved the dialog "Customize entry types" [#4719](https://github.com/JabRef/jabref/issues/4719)
- We made it possible to customise the look of JabRef by importing custom CSS files. We also made it possible to export both the standard _Light_ and _Dark_ themes as well as imported themes. [#5790](https://github.com/JabRef/jabref/issues/5790)

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
import javafx.scene.layout.VBox;

import org.jabref.gui.DialogService;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.gui.util.TaskExecutor;
import org.jabref.logic.l10n.Localization;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These new imports don't seem to be necessary.

import org.jabref.logic.util.StandardFileType;
import org.jabref.preferences.JabRefPreferences;

public abstract class AbstractPreferenceTabView<T extends PreferenceTabViewModel> extends VBox implements PreferencesTab {
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/jabref/gui/preferences/AppearanceTab.fxml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.TextField?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Tooltip?>
<fx:root prefWidth="650.0" spacing="10.0" type="VBox" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.jabref.gui.preferences.AppearanceTabView">
<fx:define>
Expand All @@ -29,4 +31,18 @@
<Label styleClass="sectionHeader" text="%Visual theme"/>
<RadioButton fx:id="themeLight" text="%Light theme" toggleGroup="$theme"/>
<RadioButton fx:id="themeDark" text="%Dark theme" toggleGroup="$theme"/>
<RadioButton fx:id="customTheme" text="%Custom theme" toggleGroup="$theme"/>

<Label styleClass="sectionHeader" text="%Import custom theme" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if the "Import custom CSS file" button is directly grouped with the above radio button option (since it doesn't make sense to import a custom theme but don't select it). Moreover, I would add a text field showing the currently selected custom css file. So in summary it should look like "Execute program" under "External programs" (also in the preferences).

Finally, I think "Select/Choose custom theme" is more appropriate. Import suggest that I can delete the file afterwards because JabRef imported it.

<Button maxWidth="Infinity" onAction="#importTheme" text="%Import theme">
<tooltip>
<Tooltip text="%Import custom CSS file" />
</tooltip>
</Button>
<Button maxWidth="Infinity" onAction="#exportTheme" text="%Export theme">
<tooltip>
<Tooltip text="%Export theme as CSS" />
</tooltip>
</Button>

</fx:root>
10 changes: 10 additions & 0 deletions src/main/java/org/jabref/gui/preferences/AppearanceTabView.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class AppearanceTabView extends AbstractPreferenceTabView<AppearanceTabVi
@FXML public TextField fontSize;
@FXML public RadioButton themeLight;
@FXML public RadioButton themeDark;
@FXML public RadioButton customTheme;

private final ControlsFxVisualizer validationVisualizer = new ControlsFxVisualizer();

Expand All @@ -43,8 +44,17 @@ public void initialize () {

themeLight.selectedProperty().bindBidirectional(viewModel.themeLightProperty());
themeDark.selectedProperty().bindBidirectional(viewModel.themeDarkProperty());
customTheme.selectedProperty().bindBidirectional(viewModel.customThemeProperty());

customTheme.setDisable(preferences.getPathToCustomTheme().isBlank());

validationVisualizer.setDecoration(new IconValidationDecorator());
Platform.runLater(() -> validationVisualizer.initVisualization(viewModel.fontSizeValidationStatus(), fontSize));
}

@FXML
void importTheme() { viewModel.importCSSFile();}

@FXML
void exportTheme() { viewModel.openExportThemeDialog();}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
import javafx.beans.property.StringProperty;

import org.jabref.gui.DialogService;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.gui.util.ThemeLoader;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.StandardFileType;
import org.jabref.preferences.JabRefPreferences;

import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator;
Expand All @@ -24,6 +26,7 @@ public class AppearanceTabViewModel implements PreferenceTabViewModel {
private final StringProperty fontSizeProperty = new SimpleStringProperty();
private final BooleanProperty themeLightProperty = new SimpleBooleanProperty();
private final BooleanProperty themeDarkProperty = new SimpleBooleanProperty();
private final BooleanProperty themeCustomProperty = new SimpleBooleanProperty();

private final DialogService dialogService;
private final JabRefPreferences preferences;
Expand Down Expand Up @@ -56,15 +59,20 @@ public void setValues() {
fontOverrideProperty.setValue(preferences.getBoolean(JabRefPreferences.OVERRIDE_DEFAULT_FONT_SIZE));
fontSizeProperty.setValue(String.valueOf(preferences.getInt(JabRefPreferences.MAIN_FONT_SIZE)));

switch (preferences.get(JabRefPreferences.FX_THEME)) {
case ThemeLoader.DARK_CSS:
themeLightProperty.setValue(false);
themeDarkProperty.setValue(true);
break;
case ThemeLoader.MAIN_CSS:
default:
themeLightProperty.setValue(true);
themeDarkProperty.setValue(false);
String currentTheme = preferences.get(JabRefPreferences.FX_THEME);

if (ThemeLoader.DARK_CSS.equals(currentTheme)) {
themeLightProperty.setValue(false);
themeDarkProperty.setValue(true);
themeCustomProperty.setValue(false);
} else if (ThemeLoader.MAIN_CSS.equals(currentTheme) || currentTheme.isBlank()) {
themeLightProperty.setValue(true);
themeDarkProperty.setValue(false);
themeCustomProperty.setValue(false);
} else {
themeLightProperty.setValue(false);
themeDarkProperty.setValue(false);
themeCustomProperty.setValue(true);
}
}

Expand All @@ -87,6 +95,9 @@ public void storeSettings() {
} else if (themeDarkProperty.getValue() && !preferences.get(JabRefPreferences.FX_THEME).equals(ThemeLoader.DARK_CSS)) {
restartWarnings.add(Localization.lang("Theme changed to dark theme."));
preferences.put(JabRefPreferences.FX_THEME, ThemeLoader.DARK_CSS);
} else if (themeCustomProperty.getValue() && !preferences.get(JabRefPreferences.FX_THEME).equals(ThemeLoader.getCustomCss())) {
restartWarnings.add(Localization.lang("Theme changed to a custom theme."));
preferences.put(JabRefPreferences.FX_THEME, preferences.getPathToCustomTheme());
}
}

Expand All @@ -113,4 +124,24 @@ public boolean validateSettings() {

public BooleanProperty themeDarkProperty() { return themeDarkProperty; }

public BooleanProperty customThemeProperty() { return themeCustomProperty; }

public void importCSSFile() {
FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
.addExtensionFilter(StandardFileType.CSS)
.withDefaultExtension(StandardFileType.CSS)
.withInitialDirectory(preferences.setLastPreferencesExportPath()).build();

dialogService.showFileOpenDialog(fileDialogConfiguration).ifPresent(file -> {

preferences.setPathToCustomTheme(file.toAbsolutePath().toString());

dialogService.showWarningDialogAndWait(Localization.lang("Import CSS"),
Localization.lang("You must restart JabRef for this to come into effect."));
Comment on lines +139 to +140
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't you deal with this by restartWarnings.add()?

});
}

public void openExportThemeDialog() {
new ExportThemeDialog(dialogService, preferences).showAndWait();
}
}
28 changes: 28 additions & 0 deletions src/main/java/org/jabref/gui/preferences/ExportThemeDialog.fxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<?import javafx.geometry.Insets?>
<DialogPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jabref.gui.preferences.ExportThemeDialog"
prefHeight="300.0" prefWidth="500.0">

<content>
<BorderPane>
<center>
<TableView fx:id="table">
<columns>
<TableColumn fx:id="columnName" text="%Theme name"/>
<TableColumn fx:id="columnPath" text="%Path to theme"/>
</columns>
<padding>
<Insets bottom="30.0"/>
</padding>
</TableView>
</center>
</BorderPane>
</content>
<ButtonType fx:constant="CLOSE"/>
</DialogPane>
107 changes: 107 additions & 0 deletions src/main/java/org/jabref/gui/preferences/ExportThemeDialog.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package org.jabref.gui.preferences;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;

import org.jabref.JabRefException;
import org.jabref.gui.DialogService;
import org.jabref.gui.util.BaseDialog;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.gui.util.ThemeLoader;
import org.jabref.logic.l10n.Localization;
import org.jabref.logic.util.StandardFileType;
import org.jabref.preferences.JabRefPreferences;

import com.airhacks.afterburner.views.ViewLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExportThemeDialog extends BaseDialog<Void> {

private static final Logger LOGGER = LoggerFactory.getLogger(ExportThemeDialog.class);

@FXML
private TableView<Theme> table;
@FXML
private TableColumn<Theme, String> columnName;
@FXML
private TableColumn<Theme, String> columnPath;

private JabRefPreferences preferences;
private DialogService dialogService;

public ExportThemeDialog(DialogService dialogService, JabRefPreferences preferences) {
this.dialogService = dialogService;
this.preferences = preferences;

ViewLoader
.view(this)
.load()
.setAsDialogPane(this);

this.setTitle(Localization.lang("Export Theme"));
}

@FXML
public void initialize() {
columnName.setCellValueFactory(new PropertyValueFactory<>("name"));
columnPath.setCellValueFactory(new PropertyValueFactory<>("path"));

ObservableList<Theme> data =
FXCollections.observableArrayList(new Theme("Light theme", ThemeLoader.MAIN_CSS), new Theme("Dark theme", ThemeLoader.DARK_CSS));

if (!(ThemeLoader.getCustomCss().isBlank())) {
data.add(new Theme("Custom theme", ThemeLoader.getCustomCss()));
}

table.setItems(data);

table.setOnKeyPressed(event -> {
TablePosition tablePosition;
if (event.getCode().equals(KeyCode.ENTER)) {
tablePosition = table.getFocusModel().getFocusedCell();
final int row = tablePosition.getRow();
ObservableList<Theme> list = table.getItems();
Theme theme = list.get(row);
exportCSSFile(theme.getPath());
}
});

table.setRowFactory(tv -> {
TableRow<Theme> row = new TableRow<>();
row.setOnMouseClicked(event -> handleSelectedRowEvent(row));
return row;
});
}

private void handleSelectedRowEvent(TableRow<Theme> row) {
if (!row.isEmpty()) {
exportCSSFile(row.getItem().getPath());
}
}

private void exportCSSFile(String theme) {
FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder()
.addExtensionFilter(StandardFileType.CSS)
.withDefaultExtension(StandardFileType.CSS)
.withInitialDirectory(preferences.setLastPreferencesExportPath())
.build();

dialogService.showFileSaveDialog(fileDialogConfiguration)
.ifPresent(exportFile -> {
try {
preferences.exportTheme(exportFile.getFileName(), theme);
} catch (JabRefException ex) {
LOGGER.warn(ex.getMessage(), ex);
dialogService.showErrorDialogAndWait(Localization.lang("Export theme"), ex);
}
});
}
}
Comment on lines +1 to +107
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class can be divided according to the mvvm-pattern.
This means: put exportCSSFile, handleSelectedRowEvent and the logic to initialize the list into a ViewModel class und communicate between the View and the ViewModel via Properties. Basic idea is that the ViewModel should be useable, even if no gui is loaded.
Have a look at the implementation of other PreferencesTabs or dialogs.
It's very easy.

21 changes: 21 additions & 0 deletions src/main/java/org/jabref/gui/preferences/Theme.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.jabref.gui.preferences;

import javafx.beans.property.SimpleStringProperty;

public class Theme {
private SimpleStringProperty name;
private SimpleStringProperty path;

public Theme(String name, String path) {
this.name = new SimpleStringProperty(name);
this.path = new SimpleStringProperty(path);
}

public String getName() {
return name.get();
}

public String getPath() {
return path.get();
}
}
36 changes: 29 additions & 7 deletions src/main/java/org/jabref/gui/util/ThemeLoader.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.jabref.gui.util;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
Expand Down Expand Up @@ -38,6 +39,7 @@ public class ThemeLoader {

public static final String MAIN_CSS = "Base.css";
public static final String DARK_CSS = "Dark.css";
private static String CUSTOM_CSS = "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is not really required for this class. I would prefer if only the preferences know about the current custom theme (in form of the FX_Theme field).


private static final Logger LOGGER = LoggerFactory.getLogger(ThemeLoader.class);
private final Optional<URL> additionalCssToLoad;
Expand All @@ -48,9 +50,10 @@ public ThemeLoader(FileUpdateMonitor fileUpdateMonitor, JabRefPreferences jabRef

String cssVmArgument = System.getProperty("jabref.theme.css");
String cssPreferences = jabRefPreferences.get(JabRefPreferences.FX_THEME);

if (StringUtil.isNotBlank(cssVmArgument)) {
// First priority: VM argument
LOGGER.info("Using css from VM option: " + cssVmArgument);
LOGGER.info("Using css from VM option: {}", cssVmArgument);
URL cssVmUrl = null;
try {
cssVmUrl = Paths.get(cssVmArgument).toUri().toURL();
Expand All @@ -60,13 +63,24 @@ public ThemeLoader(FileUpdateMonitor fileUpdateMonitor, JabRefPreferences jabRef
additionalCssToLoad = Optional.ofNullable(cssVmUrl);
} else if (StringUtil.isNotBlank(cssPreferences) && !MAIN_CSS.equalsIgnoreCase(cssPreferences)) {
// Otherwise load css from preference
URL cssResource = JabRefFrame.class.getResource(cssPreferences);
if (cssResource != null) {
LOGGER.debug("Using css " + cssResource);
additionalCssToLoad = Optional.of(cssResource);
Optional<URL> cssResource = Optional.empty();
if (DARK_CSS.equals(cssPreferences)) {
cssResource = Optional.ofNullable(JabRefFrame.class.getResource(cssPreferences));
} else {
try {
cssResource = Optional.of(new File(cssPreferences).toURI().toURL());
setCustomCss(cssPreferences);
} catch (MalformedURLException e) {
LOGGER.warn("Cannot load css {}", cssPreferences);
}
}

if (cssResource.isPresent()) {
LOGGER.debug("Using css {}", cssResource);
additionalCssToLoad = cssResource;
} else {
additionalCssToLoad = Optional.empty();
LOGGER.warn("Cannot load css " + cssPreferences);
LOGGER.warn("Cannot load css {}", cssPreferences);
}
} else {
additionalCssToLoad = Optional.empty();
Expand Down Expand Up @@ -104,7 +118,15 @@ private void addAndWatchForChanges(Scene scene, URL cssFile, int index) {
});
}
} catch (IOException | URISyntaxException | UnsupportedOperationException e) {
LOGGER.error("Could not watch css file for changes " + cssFile, e);
LOGGER.error("Could not watch css file for changes {}", cssFile, e);
}
}

public static String getCustomCss() {
return CUSTOM_CSS;
}

public static void setCustomCss(String customCss) {
CUSTOM_CSS = customCss;
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/jabref/logic/util/StandardFileType.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public enum StandardFileType implements FileType {
XML("xml"),
JSON("json"),
XMP("xmp"),
ZIP("zip");
ZIP("zip"),
CSS("css");

private final List<String> extensions;

Expand Down
Loading