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

Live reloading when switching themes #7336

Merged
merged 55 commits into from
Jan 3, 2022
Merged

Live reloading when switching themes #7336

merged 55 commits into from
Jan 3, 2022

Conversation

docrjp
Copy link
Contributor

@docrjp docrjp commented Jan 12, 2021

This resolves enhancement #7335 ( fixes #7335 )

Theme preferences can be changed without reloading. Supporting the enhancement is a refactoring of Theme which lays the groundwork for further enhancements #7322 and #7323.

This is a draft PR, for visibility and Continuous Integration purposes. Some code changes are outstanding:

  • Live reload when changing between light/dark/custom
  • Review live reloading for web engine (Preview Viewer)
  • If all live reloading is fully effective, remove reload warning
  • Rework ThemeTest now that the Theme interface has changed for web engine stylesheets
  • Review JavaDoc and comments to explain solution
  • Summarise solution here to assist review

Followed by standard PR tasks:

  • Change in CHANGELOG.md described (if applicable)
  • Tests created for changes (if applicable)
  • Manually tested changed features in running JabRef (always required)
  • Screenshots added in PR description (for UI changes)
  • Checked documentation: Is the information available and up to date? If not created an issue at https://github.com/JabRef/user-documentation/issues or, even better, submitted a pull request to the documentation repository.

Copy link
Member

@tobiasdiez tobiasdiez left a comment

Choose a reason for hiding this comment

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

Thanks a lot, this already looks extremely nice. The new Stylesheet classes are a good idea, they make things a lot clearer.

I'm not sure if it works, but it would be nice to profit even more from the introduction of these classes and further simplify the Theme / ThemeImpl / ThemePreference classes. The following would be my idea:

  • Add Optional<Stylesheet> getAdditonialStylesheet in AppearencePreference. The preference class uses Stylesheet.create(...) to create one based on the string stored in the preferences.
  • The Theme class uses this method to determine the additional stylesheets to install.
  • You can now remove ThemeState, ThemeImpl and ThemePreferences.

What do you think?

src/main/java/org/jabref/gui/theme/StyleSheetFile.java Outdated Show resolved Hide resolved

private final ThemePreference themePreference;

private final StyleSheet additionalStylesheet;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe make this an Optional<StyleSheet>? Than you would not need to introduce the "EmptyStylesheet" class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea here is to have an invariant: "there is always an additional stylesheet".

This is so that it is much simpler to transition between states where an additional stylesheet is available, and states where it is not (either because it is missing, or because the theme does not require one).

Copy link
Member

Choose a reason for hiding this comment

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

ok, but "there is always an additional stylesheet" doesn't seem to be valid from a business logic (the light theme doesn't have additional css) as well as from the current implementation (EmptyStylesheet is not a a stylesheet, it's something that pretends to be one).

On the other hand, the current solution works and is fine codewise so take my comment as a suggestion that doesn't necessarily need to be implemented.

Copy link
Contributor Author

@docrjp docrjp Jan 17, 2021

Choose a reason for hiding this comment

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

Ah, perhaps it wasn't clear! EmptyStylesheet loaded a real resource called Empty.css so it wasn't pretending!

However, given it is confusing, I've updated to remove StyleSheetEmpty and rename Empty.css to Light.css, which has a comment in it explaining that it doesn't have to add anything over Base.css

This is meaningful, because we could add something specific to Light.css. A practical example would be theming for Preview. That can't be done in Base.css, because WebEngine (which preview uses) can only take one style sheet and that is the additional one. So Light.css would be the place for that.

So hopefully that clears things up (and removes a class) while keeping the useful invariant.

src/main/java/org/jabref/gui/theme/ThemeImpl.java Outdated Show resolved Hide resolved
@docrjp
Copy link
Contributor Author

docrjp commented Jan 17, 2021

Thanks a lot, this already looks extremely nice. The new Stylesheet classes are a good idea, they make things a lot clearer.

I'm not sure if it works, but it would be nice to profit even more from the introduction of these classes and further simplify the Theme / ThemeImpl / ThemePreference classes. The following would be my idea:

  • Add Optional<Stylesheet> getAdditonialStylesheet in AppearencePreference. The preference class uses Stylesheet.create(...) to create one based on the string stored in the preferences.
  • The Theme class uses this method to determine the additional stylesheets to install.
  • You can now remove ThemeState, ThemeImpl and ThemePreferences.

What do you think?

I have gone somewhat in this direction, just pushed a change that cleans up the AppearancePreferences and eliminates ThemeState, by having the Stylesheet created by the ThemePreference. Theme no longer has a circular relationship with the PreferenceService. Instead it just needs AppearancePreferences and this is the state object.

It might be a bit cleaner still if I can move AppreancePreferences into the theme package, at the moment all the preference objects sit together but we really want it to live with ThemePreference so it can have some package-private methods to help ThemeImpl.

Not sure I could go further with removing the added classes without compromising the Single Responsibility Principle which are the rationale behind them:

  • Theme (ThemeImpl) is responsible for live updating changes to appearance. It is a singleton so that it can transition between old and new preferences
  • Stylesheet and its implementations abstract the different places that a stylesheet can be stored, and the complexities of WebEngine and Scene having different URL handling
  • AppearancePreferences hold all the chosen preferences at a point in time that can be transitioned by Theme
  • ThemePreference is the specific appearance preference that determines the chosen additional stylesheet. It is responsible for determining the correct additional Stylesheet for a chosen theme. This is the class that will be refactored as part of Add some more built in themes, especially high contrast dark #7322

If we for example combined Stylesheet and ThemePreference into a single class, then Stylesheet would no longer be useable for the base stylesheet, it would be coupled to the additional stylesheet. The general stylesheet handling doesn't need to be impacted when we get to #7322, which really only concerns how ThemePreference interacts with the appearance tab and settings storage. Stylesheet shouldn't have to care about such things, especially as it is also used by the base CSS.

@tobiasdiez
Copy link
Member

tobiasdiez commented Jan 17, 2021

That looks really good. Thanks! Based on your description of the responsibilities of the classes, one might consider to rename Theme to ̀ThemeManager (and ThemePreferences to Theme). Also maybe it's possible to merge ThemePreference with AppearencePreference since the former is never used as a standalone anyway.

@calixtus
Copy link
Member

It might be a bit cleaner still if I can move AppreancePreferences into the theme package, at the moment all the preference objects sit together but we really want it to live with ThemePreference so it can have some package-private methods to help ThemeImpl.

Great work so far, however, please keep in mind that the XxxPreferences objects should be plain stupid (immutable) data holder objects, that are created by a getter method by a PreferencesService. One of our goals is to clean up the whole codebase of JabRef, which has grown in almost 20 years, modularize it and implement design patterns. I know, this is still a very long road to walk...

@calixtus
Copy link
Member

calixtus commented Feb 1, 2021

Hi @docrjp , do you have any updates on this PR? We are so impressed of your work you already contributed in such a short period of time in this and the other PR.
Tonight is devcall, and we normally walk through the PRs to check for their status.

@docrjp
Copy link
Contributor Author

docrjp commented Feb 1, 2021

Hi @docrjp , do you have any updates on this PR? We are so impressed of your work you already contributed in such a short period of time in this and the other PR.
Tonight is devcall, and we normally walk through the PRs to check for their status.

Hi! So, I'm somewhat undecided about how to finish up the PR.

Eliminating ThemeState, and moving the Stylesheet static factory to the XxxPreferences, conflicts to some extent with the principle that the preferences are just data holder objects. Since now, the static factory Stylesheet.create() is called from the preferences. On the other hand, maybe that's taking the principle too literally. It's not too much different from java NIO Path. If we used Path in a preference class, then we're using static factory methods i.e. Path.of() and these static factories have a lot of file system logic behind them, with Path itself being an interface.

I think it depends on the goal behind the preference classes being data holders.

If it is so that we can deserialize by reflection with a library (e.g. Jackson), where it helps to not have complex factory or constructor logic, then I think it safest to move this logic back out of ThemePreference and reintroduce ThemeState as per the initial PR commit. Doing so eliminates the static factory from the ThemePreference and will keep that class really plain and simple.

On the other hand, if the purpose of keeping the preferences as data holder objects is to encourage loose coupling -- i.e. avoid the preferences needing application services injecting into them -- then the Stylesheet.create() static factory itself is harmless. It doesn't need to depend upon any other services, less so even than Path.of() which depends on file system providers.

However, what remains not so great is that Stylesheet is in the theme package but the factory is used from the preference package -- and the preference is in turn used by the ThemeManager in the .theme package. So that's a cyclic dependency of packages.

My instinct is to keep ThemePreference, and keep Stylesheet.create() as being called by ThemePreference, and then move ThemePreference into its own sub-package of the preferences package along with Stylesheet. But I don't want to do that if it messes up other plans with preferences.

@calixtus
Copy link
Member

calixtus commented Feb 1, 2021

Thanks for your very quick response!
The big plans were to uncouple the current implementation of JabRefPreferences from the other code and to clean up the project structure, so we reach someday a clean distinction between model logic and gui (and in the far far future in a galaxy far far away maybe someday provide a webbased interface for jabref).

My idea was to collect the xxxPreferences classes in the model package unbound to any concrete implementation either of the preferences or the gui. On the other hand I think that the theme preferences definitely are part of the gui implementation.
I think we'll discuss this in the devcall tonight.

@tobiasdiez
Copy link
Member

the purpose of keeping the preferences as data holder objects is to encourage loose coupling -- i.e. avoid the preferences needing application services injecting into them

That's their main goal as of now. Personally, I don't mind if a bit of logic is needed to construct a preference object.

However, what remains not so great is that Stylesheet is in the theme package but the factory is used from the preference package -- and the preference is in turn used by the ThemeManager in the .theme package. So that's a cyclic dependency of packages.

I don't think this is a big deal. For unit tests, you can mock the preferences and still create a ThemeManager. I think, this is the most important thing that one desires from the architecture.

@docrjp
Copy link
Contributor Author

docrjp commented Feb 2, 2021

Did anything come out of the devcall that is worth taking into consideration?

If not, then based on discussion above, I think we can just stick with what we've got for now, and I finish up the outstanding work of fixing the unit tests. I'll also update the tracked master to make sure that the PR is clean.

With the related follow up issues #7322 and #7323. there will be some more natural opportunities to refactor and discuss :)

@calixtus
Copy link
Member

calixtus commented Feb 2, 2021

Sorry, I was a bit buried in work today.

We had a brief discussion yesterday, but no quick response, so it's more or less up to me, since I'm currently refactoring the whole preferences complex.

I believe there was a misunderstanding on my side: I thought that ThemePreference is a preferences class just like MainTablePreferences, but it rather seems to be the representation of the actual theme. So please rename it to Theme, like @tobiasdiez mentioned earlier. I'd like to ask you to provide the ThemeManager in JabRefGui or in Globals, but not to construct it in JabRefPreferences, since there we only want to return values from the low level preferences repository there. That way we can avoid bloating the JabRefPrefences too much with logic and gui stuff that does not belong there. That's because my future target is to abstract the getXXXPrefrences and storeXXXPreferences methods in JabRefPreferences to be able to provide multiple implementations of a PreferencesService. So JabRefPreferences should only implement an adapter for the java prefs.

Please keep the naming convention (getAppearancePreferences instead of getCurrentAppearancePreferences). Also please use these methods only to store preferences or return from the java prefs or from a cache variable.

I know, JabRefPreferences is still a bit a messy place, but I'm working on it. It has a code history of almost 20 years now.

Thank you for your patience and all your impressive work!

As soon as you are ready, we are doing the regular double review and can then merge.

# Conflicts:
#	src/main/java/org/jabref/gui/preferences/appearance/AppearanceTabViewModel.java
#	src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
@tobiasdiez
Copy link
Member

@docrjp could you please implement the last remaining changes so that we can get this merged? Thanks!

@tobiasdiez tobiasdiez added the status: changes required Pull requests that are not yet complete label Mar 10, 2021
@docrjp
Copy link
Contributor Author

docrjp commented Mar 10, 2021 via email

@calixtus
Copy link
Member

Hi, I took the liberty to dive into this PR a bit last week and made some commits regarding the remaining issues. If you don't mind I'll push them on your branch?

@Siedlerchr
Copy link
Member

@calixtus What's the status here? You still had the OutOfBounds Exception?
Since someone in the forum asked https://discourse.jabref.org/t/new-feature-change-light-dark-theme-without-restarting/3041

@calixtus
Copy link
Member

There still seems to be a concurrency bug producing the exception, and the ThemeManagerTest not completely working. Had to put work on this pr on hold because of more pressing matters, both private and jabref-related
(observable prefs project)

# Conflicts:
#	src/main/java/org/jabref/gui/LibraryTab.java
#	src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java
#	src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java
#	src/main/java/org/jabref/gui/preferences/preview/PreviewTab.java
#	src/main/java/org/jabref/gui/preview/PreviewViewer.java
#	src/main/java/org/jabref/gui/slr/ExistingStudySearchAction.java
#	src/main/java/org/jabref/preferences/JabRefPreferences.java
# Conflicts:
#	src/main/java/org/jabref/gui/entryeditor/DeprecatedFieldsTab.java
#	src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
#	src/main/java/org/jabref/gui/entryeditor/FieldsEditorTab.java
#	src/main/java/org/jabref/gui/entryeditor/OptionalFields2Tab.java
#	src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTab.java
#	src/main/java/org/jabref/gui/entryeditor/OptionalFieldsTabBase.java
#	src/main/java/org/jabref/gui/entryeditor/OtherFieldsTab.java
#	src/main/java/org/jabref/gui/entryeditor/PreviewTab.java
#	src/main/java/org/jabref/gui/entryeditor/RequiredFieldsTab.java
#	src/main/java/org/jabref/gui/entryeditor/UserDefinedFieldsTab.java
#	src/main/java/org/jabref/gui/preview/PreviewPanel.java
@calixtus calixtus changed the title live reloading when switching themes, refs #7335 (WIP) Live reloading when switching themes Jan 1, 2022
@calixtus calixtus marked this pull request as ready for review January 1, 2022 17:57
@calixtus calixtus added status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers type: code-quality Issues related to code or architecture decisions ui labels Jan 1, 2022
@koppor koppor merged commit 222d214 into JabRef:main Jan 3, 2022
Siedlerchr added a commit that referenced this pull request Jan 4, 2022
* upstream/main:
  Observable Preferences J (Preview) and minor refactor (#8370)
  quickfix (#8383)
  Live reloading when switching themes (#7336)
  FIX NPE on Merge Dialog (#8380)
  Bump org.eclipse.jgit from 5.13.0.202109080827-r to 6.0.0.202111291000-r (#8378)
  Bump jackson-dataformat-yaml from 2.13.0 to 2.13.1 (#8379)
  Bump tika-core from 2.2.0 to 2.2.1 (#8377)
  Update turabian-fullnote-bibliography-no-ibid.csl
  Squashed 'buildres/csl/csl-locales/' changes from d5ee85de8e..c38205618f
  Squashed 'buildres/csl/csl-styles/' changes from 60bf7d5..f78c707
Siedlerchr added a commit that referenced this pull request Jan 4, 2022
* upstream/main:
  Observable Preferences J (Preview) and minor refactor (#8370)
  quickfix (#8383)
  Live reloading when switching themes (#7336)
@ghost
Copy link

ghost commented Aug 15, 2023

Is this feature ready now? I'm trying to make themes and would like to have this.
It works in 5.9 when selecting a new theme file but I just wanted somebody to confirm it's officially working always :)

@calixtus
Copy link
Member

Well, live reloading worked once when it was implemented, and I believe there are some test ensuring it works. Be aware that currently the css is a bit of a mess, as there are some styles still hidden in the source, especially measurements are not yet scaling. But most of jabref should be customizeable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers type: code-quality Issues related to code or architecture decisions ui
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Change theme preferences without requiring a restart
5 participants