-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Changes from all commits
f402fd2
7bf2fed
b27f26d
8cb3a50
bd6c561
6a5f73c
b5b56bf
99d9768
bcf8004
f2fc2ab
582c9d5
198be47
00eac4e
9fc3382
90fc805
4f0db24
a8feb94
8c1e2c2
e4a4f68
00d8ef6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
|
@@ -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" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -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); | ||
} | ||
} | ||
|
||
|
@@ -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()); | ||
} | ||
} | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} |
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> |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class can be divided according to the mvvm-pattern. |
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(); | ||
} | ||
} |
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; | ||
|
@@ -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 = ""; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
private static final Logger LOGGER = LoggerFactory.getLogger(ThemeLoader.class); | ||
private final Optional<URL> additionalCssToLoad; | ||
|
@@ -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(); | ||
|
@@ -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(); | ||
|
@@ -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; | ||
} | ||
} |
There was a problem hiding this comment.
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.