diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 526fdf67..536726fd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,9 @@ jobs: - name: Checkout uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install dependencies run: | dnf -y install docker diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..321f0eef --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "data/submodules"] + path = data/submodules + url = https://github.com/GradienceTeam/Submodules diff --git a/HACKING.md b/HACKING.md index c3e2c91e..f0941cea 100644 --- a/HACKING.md +++ b/HACKING.md @@ -46,6 +46,7 @@ flatpak install org.gnome.Sdk//44 org.gnome.Platform//44 ```shell git clone https://github.com/GradienceTeam/Gradience.git cd Gradience +git submodule update --init --recursive flatpak-builder --install --user --force-clean repo/ build-aux/flatpak/com.github.GradienceTeam.Gradience.json ``` @@ -53,6 +54,7 @@ flatpak-builder --install --user --force-clean repo/ build-aux/flatpak/com.githu ```shell git clone https://github.com/GradienceTeam/Gradience.git cd Gradience +git submodule update --init --recursive flatpak-builder --install --system --force-clean repo/ build-aux/flatpak/com.github.GradienceTeam.Gradience.json ``` @@ -84,6 +86,7 @@ pip install -r requirements.txt ```shell git clone https://github.com/GradienceTeam/Gradience.git cd Gradience +git submodule update --init --recursive meson setup builddir meson configure builddir -Dprefix=/usr/local sudo ninja -C builddir install @@ -94,6 +97,7 @@ sudo ninja -C builddir install ```shell git clone https://github.com/GradienceTeam/Gradience.git cd Gradience +git submodule update --init --recursive meson setup builddir meson configure builddir -Dprefix="$(pwd)/builddir" ninja -C builddir install diff --git a/README.md b/README.md index aef7d5eb..417a3f15 100644 --- a/README.md +++ b/README.md @@ -112,10 +112,15 @@ Use [this guide](https://github.com/lassekongo83/adw-gtk3/blob/main/gtk4.md) to ## 🔄 Revert Theming +1. Open Preferences window -> **Note** -> You can press on the menu button in the headerbar and press `Reset Applied Color Scheme` -> ![Main Gradience menu](https://raw.githubusercontent.com/GradienceTeam/Design/main/Screenshots/hamburger_menu.png) +![Main Gradience Menu](https://i.imgur.com/bJMNX6d.png) + +2. Go to Theming tab + +3. In _Reset & Restore Presets_ group, click Reset button for either GTK 3 or Libadwaita applications + +![Reset & Restore Presets Group](https://i.imgur.com/SynxTJT.png)
🪛️ Manual revert diff --git a/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json b/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json index 086ff085..a99dab72 100644 --- a/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json +++ b/build-aux/flatpak/com.github.GradienceTeam.Gradience.Devel.json @@ -10,7 +10,11 @@ "--device=dri", "--socket=fallback-x11", "--socket=wayland", + "--talk-name=org.freedesktop.Flatpak", + "--filesystem=~/.local/share/gnome-shell/extensions", "--filesystem=xdg-data/flatpak/overrides:create", + "--filesystem=xdg-cache/gradience:create", + "--filesystem=xdg-data/themes:create", "--filesystem=xdg-config/gtk-3.0", "--filesystem=xdg-config/gtk-4.0", "--filesystem=xdg-run/gvfsd", diff --git a/build-aux/flatpak/com.github.GradienceTeam.Gradience.json b/build-aux/flatpak/com.github.GradienceTeam.Gradience.json index d74b44ee..a021f039 100644 --- a/build-aux/flatpak/com.github.GradienceTeam.Gradience.json +++ b/build-aux/flatpak/com.github.GradienceTeam.Gradience.json @@ -10,7 +10,11 @@ "--device=dri", "--socket=fallback-x11", "--socket=wayland", + "--talk-name=org.freedesktop.Flatpak", + "--filesystem=~/.local/share/gnome-shell/extensions", "--filesystem=xdg-data/flatpak/overrides:create", + "--filesystem=xdg-cache/gradience:create", + "--filesystem=xdg-data/themes:create", "--filesystem=xdg-config/gtk-3.0", "--filesystem=xdg-config/gtk-4.0", "--filesystem=xdg-run/gvfsd", diff --git a/build-aux/flatpak/pypi-dependencies.json b/build-aux/flatpak/pypi-dependencies.json index 233dfe33..1cdda572 100644 --- a/build-aux/flatpak/pypi-dependencies.json +++ b/build-aux/flatpak/pypi-dependencies.json @@ -12,8 +12,8 @@ "sources": [ { "type": "file", - "url": "https://files.pythonhosted.org/packages/39/f6/7c1e3a2a54f18b67c5bd092c25ac7327083d4b3b15731b98a9c193df2db9/anyascii-0.3.1-py3-none-any.whl", - "sha256": "8707d3185017435933360462a65e2c70a4818490745804f38a5ca55e59eb56a0" + "url": "https://files.pythonhosted.org/packages/4f/7b/a9a747e0632271d855da379532b05a62c58e979813814a57fa3b3afeb3a4/anyascii-0.3.2-py3-none-any.whl", + "sha256": "3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4" } ] }, @@ -24,6 +24,11 @@ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"material-color-utilities-python\" --no-build-isolation" ], "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/d8/29/bd8de07107bc952e0e2783243024e1c125e787fd685725a622e4ac7aeb3c/regex-2023.3.23.tar.gz", + "sha256": "dc80df325b43ffea5cdea2e3eaa97a44f3dd298262b1c7fe9dbb2a9522b956a7" + }, { "type": "file", "url": "https://files.pythonhosted.org/packages/bc/07/830784e061fb94d67649f3e438ff63cfb902dec6d48ac75aeaaac7c7c30e/Pillow-9.4.0.tar.gz", @@ -33,11 +38,6 @@ "type": "file", "url": "https://files.pythonhosted.org/packages/31/65/a8e0f3e2bad0d4eabeb1931b22cdae08344a955f28022dc83420a128683c/material_color_utilities_python-0.1.5-py2.py3-none-any.whl", "sha256": "48abd8695a1355ab3ad43fe314ca8664c66282a86fbf94a717571273bf422bdf" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/27/b5/92d404279fd5f4f0a17235211bb0f5ae7a0d9afb7f439086ec247441ed28/regex-2022.10.31.tar.gz", - "sha256": "a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83" } ] }, @@ -48,21 +48,16 @@ "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"svglib\" --no-build-isolation" ], "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", + "sha256": "a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78" + }, { "type": "file", "url": "https://files.pythonhosted.org/packages/bc/07/830784e061fb94d67649f3e438ff63cfb902dec6d48ac75aeaaac7c7c30e/Pillow-9.4.0.tar.gz", "sha256": "a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e" }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/9d/3a/e39436efe51894243ff145a37c4f9a030839b97779ebcc4f13b3ba21c54e/cssselect2-0.7.0-py3-none-any.whl", - "sha256": "fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969" - }, - { - "type": "file", - "url": "https://files.pythonhosted.org/packages/06/5a/e11cad7b79f2cf3dd2ff8f81fa8ca667e7591d3d8451768589996b65dec1/lxml-4.9.2.tar.gz", - "sha256": "2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67" - }, { "type": "file", "url": "https://files.pythonhosted.org/packages/b8/ac/10d68a650b321bd8c4d8cbefd9994e7727d57b381c9bdb0a013273011e62/reportlab-3.6.12.tar.gz", @@ -70,8 +65,8 @@ }, { "type": "file", - "url": "https://files.pythonhosted.org/packages/ee/cd/8b19f6299541862deea19b26a9efe269ff843fa15a9fd833ff0fec2ac22c/svglib-1.4.1.tar.gz", - "sha256": "48c24706c23bb4262173b6fa49eabb10afa15b8412f14283120549517ccfa314" + "url": "https://files.pythonhosted.org/packages/06/5a/e11cad7b79f2cf3dd2ff8f81fa8ca667e7591d3d8451768589996b65dec1/lxml-4.9.2.tar.gz", + "sha256": "2455cfaeb7ac70338b3257f41e21f0724f4b5b0c0e7702da67ee6c3640835b67" }, { "type": "file", @@ -80,8 +75,13 @@ }, { "type": "file", - "url": "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", - "sha256": "a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78" + "url": "https://files.pythonhosted.org/packages/9d/3a/e39436efe51894243ff145a37c4f9a030839b97779ebcc4f13b3ba21c54e/cssselect2-0.7.0-py3-none-any.whl", + "sha256": "fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969" + }, + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/56/5b/53ca0fd447f73423c7dc59d34e523530ef434481a3d18808ff7537ad33ec/svglib-1.5.1.tar.gz", + "sha256": "3ae765d3a9409ee60c0fb4d24c2deb6a80617aa927054f5bcd7fc98f0695e587" } ] }, @@ -108,13 +108,27 @@ "sources": [ { "type": "file", - "url": "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", - "sha256": "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + "url": "https://files.pythonhosted.org/packages/95/7e/68018b70268fb4a2a605e2be44ab7b4dd7ce7808adae6c5ef32e34f4b55a/MarkupSafe-2.1.2.tar.gz", + "sha256": "abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d" }, { "type": "file", - "url": "https://files.pythonhosted.org/packages/1d/97/2288fe498044284f39ab8950703e88abbac2abbdf65524d576157af70556/MarkupSafe-2.1.1.tar.gz", - "sha256": "7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b" + "url": "https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl", + "sha256": "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" + } + ] + }, + { + "name": "python3-libsass", + "buildsystem": "simple", + "build-commands": [ + "pip3 install --verbose --exists-action=i --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"libsass\" --no-build-isolation" + ], + "sources": [ + { + "type": "file", + "url": "https://files.pythonhosted.org/packages/55/14/f1d9578dce39f890ae3c0f93db8a23e89d2a1403da81d307ffb429df7c3b/libsass-0.22.0.tar.gz", + "sha256": "3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425" } ] } diff --git a/data/com.github.GradienceTeam.Gradience.gschema.xml.in b/data/com.github.GradienceTeam.Gradience.gschema.xml.in index d6bbd9bd..7383d9d5 100644 --- a/data/com.github.GradienceTeam.Gradience.gschema.xml.in +++ b/data/com.github.GradienceTeam.Gradience.gschema.xml.in @@ -22,6 +22,9 @@ [] + + ['shell', 'monet'] + false diff --git a/data/gradience.gresource.xml b/data/gradience.gresource.xml index 905fcb94..8f6a18e9 100644 --- a/data/gradience.gresource.xml +++ b/data/gradience.gresource.xml @@ -10,6 +10,7 @@ ui/error_list_row.ui ui/explore_preset_row.ui ui/log_out_dialog.ui + ui/monet_theming_group.ui ui/no_plugin_window.ui ui/option_row.ui ui/palette_shades.ui @@ -18,8 +19,12 @@ ui/preset_row.ui ui/presets_manager_window.ui ui/repo_row.ui + ui/reset_preset_group.ui ui/save_dialog.ui ui/share_window.ui + ui/shell_prefs_window.ui + ui/shell_theming_group.ui + ui/theming_empty_group.ui ui/welcome_window.ui ui/window.ui images/welcome-dark.svg diff --git a/data/meson.build b/data/meson.build index dd1c63fd..7ef966f8 100644 --- a/data/meson.build +++ b/data/meson.build @@ -73,3 +73,6 @@ endif subdir('icons') subdir('plugins') + +subdir('shell') +subdir('submodules/gnome-shell') diff --git a/data/presets/adwaita-dark.json b/data/presets/adwaita-dark.json index aaaa5383..b6dba7ff 100644 --- a/data/presets/adwaita-dark.json +++ b/data/presets/adwaita-dark.json @@ -33,7 +33,9 @@ "popover_bg_color": "#383838", "popover_fg_color": "#ffffff", "shade_color": "rgba(0, 0, 0, 0.36)", - "scrollbar_outline_color": "rgba(0, 0, 0, 0.5)" + "scrollbar_outline_color": "rgba(0, 0, 0, 0.5)", + "thumbnail_bg_color": "#383838", + "thumbnail_fg_color": "#ffffff" }, "palette": { "blue_": { @@ -100,4 +102,4 @@ "5": "#000000" } } -} \ No newline at end of file +} diff --git a/data/presets/adwaita.json b/data/presets/adwaita.json index ae851a22..11867a7b 100644 --- a/data/presets/adwaita.json +++ b/data/presets/adwaita.json @@ -33,7 +33,9 @@ "popover_bg_color": "#ffffff", "popover_fg_color": "rgba(0, 0, 0, 0.8)", "shade_color": "rgba(0, 0, 0, 0.07)", - "scrollbar_outline_color": "#ffffff" + "scrollbar_outline_color": "#ffffff", + "thumbnail_bg_color": "#ffffff", + "thumbnail_fg_color": "rgba(0, 0, 0, 0.8)" }, "palette": { "blue_": { @@ -100,4 +102,4 @@ "5": "#000000" } } -} \ No newline at end of file +} diff --git a/data/presets/pretty-purple.json b/data/presets/pretty-purple.json index bbead689..bb1d4e7b 100644 --- a/data/presets/pretty-purple.json +++ b/data/presets/pretty-purple.json @@ -33,7 +33,9 @@ "popover_bg_color": "#241f31", "popover_fg_color": "#ffffff", "shade_color": "rgba(0, 0, 0, 0.36)", - "scrollbar_outline_color": "rgba(0, 0, 0, 0.5)" + "scrollbar_outline_color": "rgba(0, 0, 0, 0.5)", + "thumbnail_bg_color": "#241f31", + "thumbnail_fg_color": "#ffffff" }, "palette": { "blue_": { @@ -100,4 +102,4 @@ "5": "#000000" } } -} \ No newline at end of file +} diff --git a/data/shell/meson.build b/data/shell/meson.build new file mode 100644 index 00000000..c9f76b11 --- /dev/null +++ b/data/shell/meson.build @@ -0,0 +1,5 @@ +install_subdir('templates', + install_dir: join_paths(get_option('datadir'), 'gradience', 'shell'), + exclude_files: 'meson.build', + strip_directory : false +) diff --git a/data/shell/templates/42/check-box.template b/data/shell/templates/42/check-box.template new file mode 100644 index 00000000..1480ade2 --- /dev/null +++ b/data/shell/templates/42/check-box.template @@ -0,0 +1,18 @@ +/* Check Boxes */ + +// these are equal to the size of the SVG assets +$check_height: 24px; +$check_width: 24px; + + +.check-box { + StBoxLayout { spacing: .8em; } + StBin { + width: $check_width; + height: $check_height; + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/checkbox-off-light.svg"), url("resource:///org/gnome/shell/theme/checkbox-off.svg")); + } + &:focus StBin { background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/checkbox-off-focused-light.svg"), url("resource:///org/gnome/shell/theme/checkbox-off-focused.svg"));; } + &:checked StBin { background-image: url("resource:///org/gnome/shell/theme/checkbox.svg"); } + &:focus:checked StBin { background-image: url("resource:///org/gnome/shell/theme/checkbox-focused.svg"); } +} diff --git a/data/shell/templates/42/colors.template b/data/shell/templates/42/colors.template new file mode 100644 index 00000000..d105273a --- /dev/null +++ b/data/shell/templates/42/colors.template @@ -0,0 +1,71 @@ +// When color definition differs for dark and light variant, +// it gets @if-ed depending on $variant + +@import '_palette.scss'; + +$_dark_base_color: darken(desaturate({{bg_color}}, 2%), 2%); + +$base_color: $_dark_base_color; +$bg_color: if($variant == 'light', darken($base_color, 5%), lighten($base_color, 5%)); +$fg_color: if($variant == 'light', transparentize(black, .2), {{fg_color}}); + +$accent_fg_color: {{accent_fg_color}}; +$selected_fg_color: if($variant == 'light', $accent_fg_color, {{selected_fg_color}}); +$selected_bg_color: {{selected_bg_color}}; +$selected_borders_color: if($variant == 'light', darken($selected_bg_color, 15%), darken($selected_bg_color, 30%)); // NOTE: Unused in GNOME Shell 42 + +$borders_color: if($variant == 'light', transparentize($fg_color, .5), transparentize($fg_color, .9)); +$borders_edge: if($variant == 'light', rgba(255,255,255,0.8), lighten($bg_color, 5%)); + +$link_color: if($variant == 'light', darken($selected_bg_color, 10%), lighten($selected_bg_color, 20%)); +$link_visited_color: if($variant == 'light', darken($selected_bg_color, 20%), lighten($selected_bg_color, 10%)); // NOTE: Unused in GNOME Shell 42 + +$warning_color: {{warning_bg_color}}; +$error_color: {{error_bg_color}}; +$success_color: {{success_bg_color}}; // NOTE: Unused in GNOME Shell 42 +$destructive_color: {{destructive_bg_color}}; + +// NOTE: Used also in overview for folder colors, in search results, partially in text and for indicators below app icons +$osd_fg_color: {{osd_fg_color}}; +$osd_bg_color: $_dark_base_color; // hardcoded for both light & dark +$osd_insensitive_bg_color: transparentize(mix($osd_fg_color, opacify($osd_bg_color, 1), 10%), 0.5); // NOTE: Unused in GNOME Shell 42 +$osd_insensitive_fg_color: if($variant == 'light', mix($osd_fg_color, $osd_bg_color, 80%), mix($osd_fg_color, $osd_bg_color, 70%)); +$osd_borders_color: transparentize(black, 0.3); +$osd_outer_borders_color: transparentize($osd_fg_color, 0.98); + +$shadow_color: if($variant == 'light', rgba(0,0,0,0.1), rgba(0,0,0,0.2)); + +// cards +$card_bg_color: if($variant == 'light', darken($bg_color, 5%), lighten($bg_color, 2%)); // TODO: Allow to modify this value + +// notifications +$bubble_buttons_color: if($variant == 'light', darken($bg_color, 12%), lighten($bg_color, 10%)); + +// overview background color +$system_bg_color: darken(desaturate({{system_bg_color}}, 2%), 2%); + +//insensitive state derived colors +$insensitive_fg_color: mix($fg_color, $bg_color, 50%); +$insensitive_bg_color: mix($bg_color, $base_color, 60%); +$insensitive_borders_color: mix($borders_color, $base_color, 60%); // NOTE: Unused in GNOME Shell 42 + +//colors for the backdrop state, derived from the main colors. +// NOTE: This entire section doesn't seem to be used anywhere in GNOME Shell 42 +$backdrop_base_color: if($variant =='light', darken($base_color,1%), lighten($base_color,1%)); +$backdrop_bg_color: $bg_color; +$backdrop_fg_color: mix($fg_color, $backdrop_bg_color, 80%); +$backdrop_insensitive_color: if($variant =='light', darken($backdrop_bg_color,15%), lighten($backdrop_bg_color,15%)); +$backdrop_borders_color: mix($borders_color, $bg_color, 90%); +$backdrop_dark_fill: mix($backdrop_borders_color,$backdrop_bg_color, 35%); + +// derived checked colors +$checked_bg_color: if($variant=='light', darken($bg_color, 7%), lighten($bg_color, 7%)); +$checked_fg_color: if($variant=='light', darken($fg_color, 7%), lighten($fg_color, 7%)); // NOTE: Unused in GNOME Shell 42 + +// derived hover colors +$hover_bg_color: if($variant=='light', darken($bg_color, 3%), lighten($bg_color, 10%)); +$hover_fg_color: if($variant=='light', darken($fg_color, 5%), lighten($fg_color, 10%)); + +// derived active colors +$active_bg_color: if($variant=='light', darken($bg_color, 5%), lighten($bg_color, 12%)); +$active_fg_color: if($variant=='light', darken($fg_color, 5%), lighten($fg_color, 12%)); diff --git a/data/shell/templates/42/gnome-shell.template b/data/shell/templates/42/gnome-shell.template new file mode 100644 index 00000000..79e9cc58 --- /dev/null +++ b/data/shell/templates/42/gnome-shell.template @@ -0,0 +1,15 @@ +$variant: {{theme_variant}}; + +/* Generated with Gradience + * + * Issues caused by theming should be reported to Gradience repository, and not upstream + * + * https://github.com/GradienceTeam/Gradience + */ + +@import "gnome-shell-sass/_colors"; //use gtk colors +@import "gnome-shell-sass/_drawing"; +@import "gnome-shell-sass/_common"; +@import "gnome-shell-sass/_widgets"; + +{{custom_css}} diff --git a/data/shell/templates/42/palette.template b/data/shell/templates/42/palette.template new file mode 100644 index 00000000..2ad9aa04 --- /dev/null +++ b/data/shell/templates/42/palette.template @@ -0,0 +1,46 @@ +//GNOME Color Palette +$blue_1: {{blue_1}}; +$blue_2: {{blue_2}}; +$blue_3: {{blue_3}}; +$blue_4: {{blue_4}}; +$blue_5: {{blue_5}}; +$green_1: {{green_1}}; +$green_2: {{green_2}}; +$green_3: {{green_3}}; +$green_4: {{green_4}}; +$green_5: {{green_5}}; +$yellow_1: {{yellow_1}}; +$yellow_2: {{yellow_2}}; +$yellow_3: {{yellow_3}}; +$yellow_4: {{yellow_4}}; +$yellow_5: {{yellow_5}}; +$orange_1: {{orange_1}}; +$orange_2: {{orange_2}}; +$orange_3: {{orange_3}}; +$orange_4: {{orange_4}}; +$orange_5: {{orange_5}}; +$red_1: {{red_1}}; +$red_2: {{red_2}}; +$red_3: {{red_3}}; +$red_4: {{red_4}}; +$red_5: {{red_5}}; +$purple_1: {{purple_1}}; +$purple_2: {{purple_2}}; +$purple_3: {{purple_3}}; +$purple_4: {{purple_4}}; +$purple_5: {{purple_5}}; +$brown_1: {{brown_1}}; +$brown_2: {{brown_2}}; +$brown_3: {{brown_3}}; +$brown_4: {{brown_4}}; +$brown_5: {{brown_5}}; +$light_1: {{light_1}}; +$light_2: {{light_2}}; +$light_3: {{light_3}}; +$light_4: {{light_4}}; +$light_5: {{light_5}}; +$dark_1: {{dark_1}}; +$dark_2: {{dark_2}}; +$dark_3: {{dark_3}}; +$dark_4: {{dark_4}}; +$dark_5: {{dark_5}}; diff --git a/data/shell/templates/42/panel.template b/data/shell/templates/42/panel.template new file mode 100644 index 00000000..ed975f85 --- /dev/null +++ b/data/shell/templates/42/panel.template @@ -0,0 +1,208 @@ +/* Top Bar */ +// a.k.a. the panel + +$panel_bg_color: #000; // TODO: Allow to modify this value +$panel_fg_color: if($variant == 'light', lighten($bg_color, 10%), darken($fg_color, 5%)); // TODO: Allow to modify this value +$panel_height: 2.2em; // TODO: Allow to modify this value +$panel_transition_duration: 250ms; // same as the overview transition duration + +#panel { + background-color: $panel_bg_color; + font-weight: bold; + height: $panel_height; + @extend %numeric; + transition-duration: $panel_transition_duration; + + // transparent panel on lock & login screens + &.unlock-screen, + &.login-screen, + &:overview { + background-color: transparent; + } + + // panel menus + .panel-button { + font-weight: bold; + color: $panel_fg_color; + -natural-hpadding: $base_padding * 2; + -minimum-hpadding: $base_padding; + transition-duration: 150ms; + border: 3px solid transparent; + border-radius: 99px; + + &.clock-display { + .clock { + transition-duration: 150ms; + border: 3px solid transparent; + border-radius: 99px; + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px $screenshot_ui_button_red; + + StBoxLayout { + spacing: $base_padding; + } + + StIcon { + icon-size: $base_icon_size; + } + } + + &:active, &:overview, &:focus, &:checked { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.8); + + // The clock display needs to have the background on .clock because + // we want to exclude the do-not-disturb indicator from the background + &.clock-display { + box-shadow: none; + + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.8); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.15); + } + } + + &:hover { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.85); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.85); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.1); + } + } + + &:active:hover, &:overview:hover, &:focus:hover, &:checked:hover { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.75); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.75); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.2); + } + } + + // status area icons + .system-status-icon { + icon-size: $base_icon_size; + padding: $base_padding - 1px; + margin: 0 $base_margin; + } + + .panel-status-indicators-box .system-status-icon, + .panel-status-menu-box .system-status-icon { + margin: 0; + } + + // app menu icon + .app-menu-icon { + -st-icon-style: symbolic; + // dimensions of the icon are hardcoded + } + + &#panelActivities { + -natural-hpadding: $base_padding * 3; + } + } + + &.unlock-screen, + &.login-screen, + &:overview { + .panel-button { + &:active, &:overview, &:focus, &:checked { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.15); + + &.clock-display { + box-shadow: none; + + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.15); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.15); + } + } + + &:hover { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.10); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.10); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.1); + } + } + + &:active:hover, &:overview:hover, &:focus:hover, &:checked:hover { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.2); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.2); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.2); + } + } + } + } + + .panel-status-indicators-box, + .panel-status-menu-box { + spacing: 2px; + } + + // spacing between power icon and (optional) percentage label + .power-status.panel-status-indicators-box { + spacing: 0; + } + + // indicator for active + .screencast-indicator, + .remote-access-indicator { color: $warning_color; } +} + +// App Menu +#appMenu { + spacing: $base_padding; + .label-shadow { color: transparent; } +} + +#appMenu .panel-status-menu-box { + padding: 0 $base_padding; + spacing: $base_padding; +} + + +// Clock + +.clock-display-box { + spacing: 2px; + + .clock { + padding-left: $base_padding * 2; + padding-right: $base_padding * 2; + } +} diff --git a/data/shell/templates/42/switches.template b/data/shell/templates/42/switches.template new file mode 100644 index 00000000..97eeca57 --- /dev/null +++ b/data/shell/templates/42/switches.template @@ -0,0 +1,16 @@ +/* Switches */ + +// these are equal to the size of the SVG assets +$switch_height: 26px; +$switch_width: 48px; + +.toggle-switch { + color: $fg_color; + height: $switch_height; + width: $switch_width; + background-size: contain; + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/toggle-off-light.svg"), url("resource:///org/gnome/shell/theme/toggle-off.svg")); + &:checked { + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/toggle-on-light.svg"), url("asstets/toggle-on.svg")); + } +} diff --git a/data/shell/templates/43/check-box.template b/data/shell/templates/43/check-box.template new file mode 100644 index 00000000..1480ade2 --- /dev/null +++ b/data/shell/templates/43/check-box.template @@ -0,0 +1,18 @@ +/* Check Boxes */ + +// these are equal to the size of the SVG assets +$check_height: 24px; +$check_width: 24px; + + +.check-box { + StBoxLayout { spacing: .8em; } + StBin { + width: $check_width; + height: $check_height; + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/checkbox-off-light.svg"), url("resource:///org/gnome/shell/theme/checkbox-off.svg")); + } + &:focus StBin { background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/checkbox-off-focused-light.svg"), url("resource:///org/gnome/shell/theme/checkbox-off-focused.svg"));; } + &:checked StBin { background-image: url("resource:///org/gnome/shell/theme/checkbox.svg"); } + &:focus:checked StBin { background-image: url("resource:///org/gnome/shell/theme/checkbox-focused.svg"); } +} diff --git a/data/shell/templates/43/colors.template b/data/shell/templates/43/colors.template new file mode 100644 index 00000000..16aade1e --- /dev/null +++ b/data/shell/templates/43/colors.template @@ -0,0 +1,77 @@ +// When color definition differs for dark and light variant, +// it gets @if-ed depending on $variant + +@import '_palette.scss'; + +$is_highcontrast: "false"; + +$_dark_base_color: darken(desaturate({{bg_color}}, 2%), 2%); + +$base_color: $_dark_base_color; +$bg_color: if($variant == 'light', darken($base_color, 5%), lighten($base_color, 5%)); +$fg_color: if($variant == 'light', transparentize(black, .2), {{fg_color}}); + +$accent_fg_color: {{accent_fg_color}}; +$selected_fg_color: if($variant == 'light', $accent_fg_color, {{selected_fg_color}}); +$selected_bg_color: {{selected_bg_color}}; +$selected_borders_color: if($variant == 'light', darken($selected_bg_color, 15%), darken($selected_bg_color, 30%)); // NOTE: Unused in GNOME Shell 43 + +$borders_color: if($variant == 'light', transparentize($fg_color, .5), transparentize($fg_color, .9)); +$borders_edge: if($variant == 'light', rgba(255,255,255,0.8), lighten($bg_color, 5%)); + +$link_color: if($variant == 'light', darken($selected_bg_color, 10%), lighten($selected_bg_color, 20%)); +$link_visited_color: if($variant == 'light', darken($selected_bg_color, 20%), lighten($selected_bg_color, 10%)); // NOTE: Unused in GNOME Shell 43 + +$warning_color: {{warning_bg_color}}; +$error_color: {{error_bg_color}}; +$success_color: {{success_bg_color}}; // NOTE: Unused in GNOME Shell 43 +$destructive_color: {{destructive_bg_color}}; + +// NOTE: Used also in overview for folder colors, in search results, partially in text and for indicators below app icons +$osd_fg_color: {{osd_fg_color}}; +$osd_bg_color: $_dark_base_color; // hardcoded for both light & dark +$osd_insensitive_bg_color: transparentize(mix($osd_fg_color, opacify($osd_bg_color, 1), 10%), 0.5); // NOTE: Unused in GNOME Shell 43 +$osd_insensitive_fg_color: if($variant == 'light', mix($osd_fg_color, $osd_bg_color, 80%), mix($osd_fg_color, $osd_bg_color, 70%)); +$osd_borders_color: transparentize(black, 0.3); +$osd_outer_borders_color: transparentize($osd_fg_color, 0.98); + +$shadow_color: if($variant == 'light', rgba(0,0,0,0.1), rgba(0,0,0,0.2)); + +// button +$button_mix_factor: 5%; + +// cards +$card_bg_color: if($variant == 'light', darken($bg_color, 5%), lighten($bg_color, 2%)); // TODO: Allow to modify this value +$card_outer_borders_color: transparentize($fg_color, 0.98); + +// notifications +$bubble_buttons_color: if($variant == 'light', darken($bg_color, 12%), lighten($bg_color, 10%)); + +// overview background color +$system_bg_color: darken(desaturate({{system_bg_color}}, 2%), 2%); + +//insensitive state derived colors +$insensitive_fg_color: mix($fg_color, $bg_color, 50%); +$insensitive_bg_color: mix($bg_color, $base_color, 60%); +$insensitive_borders_color: mix($borders_color, $base_color, 60%); // NOTE: Unused in GNOME Shell 43 + +//colors for the backdrop state, derived from the main colors. +// NOTE: This entire section doesn't seem to be used anywhere in GNOME Shell 43 +$backdrop_base_color: if($variant =='light', darken($base_color,1%), lighten($base_color,1%)); +$backdrop_bg_color: $bg_color; +$backdrop_fg_color: mix($fg_color, $backdrop_bg_color, 80%); +$backdrop_insensitive_color: if($variant =='light', darken($backdrop_bg_color,15%), lighten($backdrop_bg_color,15%)); +$backdrop_borders_color: mix($borders_color, $bg_color, 90%); +$backdrop_dark_fill: mix($backdrop_borders_color,$backdrop_bg_color, 35%); + +// derived checked colors +$checked_bg_color: if($variant=='light', darken($bg_color, 7%), lighten($bg_color, 7%)); +$checked_fg_color: if($variant=='light', darken($fg_color, 7%), lighten($fg_color, 7%)); // NOTE: Unused in GNOME Shell 43 + +// derived hover colors +$hover_bg_color: if($variant=='light', darken($bg_color, 3%), lighten($bg_color, 10%)); +$hover_fg_color: if($variant=='light', darken($fg_color, 5%), lighten($fg_color, 10%)); + +// derived active colors +$active_bg_color: if($variant=='light', darken($bg_color, 5%), lighten($bg_color, 12%)); +$active_fg_color: if($variant=='light', darken($fg_color, 5%), lighten($fg_color, 12%)); diff --git a/data/shell/templates/43/gnome-shell.template b/data/shell/templates/43/gnome-shell.template new file mode 100644 index 00000000..62eb8603 --- /dev/null +++ b/data/shell/templates/43/gnome-shell.template @@ -0,0 +1,15 @@ +$variant: {{theme_variant}}; + +/* Generated with Gradience + * + * Issues caused by theming should be reported to Gradience repository, and not upstream + * + * https://github.com/GradienceTeam/Gradience + */ + +@import "gnome-shell-sass/_colors"; //use gtk colors +@import "gnome-shell-sass/_drawing"; +@import "gnome-shell-sass/_common"; +@import "gnome-shell-sass/_widgets"; + +{{custom_css}} diff --git a/data/shell/templates/43/palette.template b/data/shell/templates/43/palette.template new file mode 100644 index 00000000..2ad9aa04 --- /dev/null +++ b/data/shell/templates/43/palette.template @@ -0,0 +1,46 @@ +//GNOME Color Palette +$blue_1: {{blue_1}}; +$blue_2: {{blue_2}}; +$blue_3: {{blue_3}}; +$blue_4: {{blue_4}}; +$blue_5: {{blue_5}}; +$green_1: {{green_1}}; +$green_2: {{green_2}}; +$green_3: {{green_3}}; +$green_4: {{green_4}}; +$green_5: {{green_5}}; +$yellow_1: {{yellow_1}}; +$yellow_2: {{yellow_2}}; +$yellow_3: {{yellow_3}}; +$yellow_4: {{yellow_4}}; +$yellow_5: {{yellow_5}}; +$orange_1: {{orange_1}}; +$orange_2: {{orange_2}}; +$orange_3: {{orange_3}}; +$orange_4: {{orange_4}}; +$orange_5: {{orange_5}}; +$red_1: {{red_1}}; +$red_2: {{red_2}}; +$red_3: {{red_3}}; +$red_4: {{red_4}}; +$red_5: {{red_5}}; +$purple_1: {{purple_1}}; +$purple_2: {{purple_2}}; +$purple_3: {{purple_3}}; +$purple_4: {{purple_4}}; +$purple_5: {{purple_5}}; +$brown_1: {{brown_1}}; +$brown_2: {{brown_2}}; +$brown_3: {{brown_3}}; +$brown_4: {{brown_4}}; +$brown_5: {{brown_5}}; +$light_1: {{light_1}}; +$light_2: {{light_2}}; +$light_3: {{light_3}}; +$light_4: {{light_4}}; +$light_5: {{light_5}}; +$dark_1: {{dark_1}}; +$dark_2: {{dark_2}}; +$dark_3: {{dark_3}}; +$dark_4: {{dark_4}}; +$dark_5: {{dark_5}}; diff --git a/data/shell/templates/43/panel.template b/data/shell/templates/43/panel.template new file mode 100644 index 00000000..02ff303c --- /dev/null +++ b/data/shell/templates/43/panel.template @@ -0,0 +1,233 @@ +/* Top Bar */ +// a.k.a. the panel + +$panel_bg_color: #000; // TODO: Allow to modify this value +$panel_fg_color: if($variant == 'light', lighten($bg_color, 10%), darken($fg_color, 5%)); // TODO: Allow to modify this value +$panel_height: 2.2em; // TODO: Allow to modify this value +$panel_transition_duration: 250ms; // same as the overview transition duration + +#panel { + background-color: $panel_bg_color; + font-weight: bold; + height: $panel_height; + @extend %numeric; + transition-duration: $panel_transition_duration; + + // transparent panel on lock & login screens + &.unlock-screen, + &.login-screen, + &:overview { + background-color: transparent; + } + + // panel menus + .panel-button { + font-weight: bold; + color: $panel_fg_color; + -natural-hpadding: $base_padding * 2; + -minimum-hpadding: $base_padding; + transition-duration: 150ms; + border: 3px solid transparent; + border-radius: 99px; + + &.clock-display { + .clock { + transition-duration: 150ms; + border: 3px solid transparent; + border-radius: 99px; + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px $screenshot_ui_button_red; + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px $warning_color; + StBoxLayout { margin: 0 $base_padding; } + } + + &.screen-recording-indicator, + &.screen-sharing-indicator { + StBoxLayout { + spacing: $base_padding; + } + + StIcon { + icon-size: $base_icon_size; + } + } + + &:active, &:overview, &:focus, &:checked { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.8); + + // The clock display needs to have the background on .clock because + // we want to exclude the do-not-disturb indicator from the background + &.clock-display { + box-shadow: none; + + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.8); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.15); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.15); + } + } + + &:hover { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.85); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.85); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.1); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.1); + } + } + + &:active:hover, &:overview:hover, &:focus:hover, &:checked:hover { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.75); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.75); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.2); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.2); + } + } + + // status area icons + .system-status-icon { + icon-size: $base_icon_size; + padding: $base_padding - 1px; + margin: 0 $base_margin; + } + + .panel-status-indicators-box .system-status-icon, + .panel-status-menu-box .system-status-icon { + margin: 0; + } + + // app menu icon + .app-menu-icon { + -st-icon-style: symbolic; + // dimensions of the icon are hardcoded + } + + &#panelActivities { + -natural-hpadding: $base_padding * 3; + } + } + + &.unlock-screen, + &.login-screen, + &:overview { + .panel-button { + &:active, &:overview, &:focus, &:checked { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.15); + + &.clock-display { + box-shadow: none; + + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.15); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.15); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.15); + } + } + + &:hover { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.10); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.10); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.1); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.1); + } + } + + &:active:hover, &:overview:hover, &:focus:hover, &:checked:hover { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.2); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.2); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.2); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.2); + } + } + } + } + + .panel-status-indicators-box, + .panel-status-menu-box { + spacing: 2px; + } + + // spacing between power icon and (optional) percentage label + .power-status.panel-status-indicators-box { + spacing: 0; + } + + // indicator for active + .screencast-indicator, + .remote-access-indicator { color: $warning_color; } +} + +// App Menu +#appMenu { + spacing: $base_padding; + .label-shadow { color: transparent; } +} + +#appMenu .panel-status-menu-box { + padding: 0 $base_padding; + spacing: $base_padding; +} + + +// Clock + +.clock-display-box { + spacing: 2px; + + .clock { + padding-left: $base_padding * 2; + padding-right: $base_padding * 2; + } +} diff --git a/data/shell/templates/43/switches.template b/data/shell/templates/43/switches.template new file mode 100644 index 00000000..9772a594 --- /dev/null +++ b/data/shell/templates/43/switches.template @@ -0,0 +1,16 @@ +/* Switches */ + +// these are equal to the size of the SVG assets +$switch_height: 26px; +$switch_width: 48px; + +.toggle-switch { + color: $fg_color; + height: $switch_height; + width: $switch_width; + background-size: contain; + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/toggle-off-light.svg"), url("resource:///org/gnome/shell/theme/toggle-off.svg")); + &:checked { + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/toggle-on-light.svg"), url("assets/toggle-on.svg")); + } +} diff --git a/data/shell/templates/44/check-box.template b/data/shell/templates/44/check-box.template new file mode 100644 index 00000000..1480ade2 --- /dev/null +++ b/data/shell/templates/44/check-box.template @@ -0,0 +1,18 @@ +/* Check Boxes */ + +// these are equal to the size of the SVG assets +$check_height: 24px; +$check_width: 24px; + + +.check-box { + StBoxLayout { spacing: .8em; } + StBin { + width: $check_width; + height: $check_height; + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/checkbox-off-light.svg"), url("resource:///org/gnome/shell/theme/checkbox-off.svg")); + } + &:focus StBin { background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/checkbox-off-focused-light.svg"), url("resource:///org/gnome/shell/theme/checkbox-off-focused.svg"));; } + &:checked StBin { background-image: url("resource:///org/gnome/shell/theme/checkbox.svg"); } + &:focus:checked StBin { background-image: url("resource:///org/gnome/shell/theme/checkbox-focused.svg"); } +} diff --git a/data/shell/templates/44/colors.template b/data/shell/templates/44/colors.template new file mode 100644 index 00000000..ee8db91f --- /dev/null +++ b/data/shell/templates/44/colors.template @@ -0,0 +1,73 @@ +// When color definition differs for dark and light variant, +// it gets @if-ed depending on $variant + +@import '_palette.scss'; + +$is_highcontrast: false; + +$_dark_base_color: darken(desaturate({{bg_color}}, 2%), 2%); + +$base_color: $_dark_base_color; +$bg_color: if($variant == 'light', darken($base_color, 5%), lighten($base_color, 5%)); +$fg_color: if($variant == 'light', transparentize(black, .2), {{fg_color}}); + +$accent_fg_color: {{accent_fg_color}}; +$selected_fg_color: if($variant == 'light', $accent_fg_color, {{selected_fg_color}}); +$selected_bg_color: {{selected_bg_color}}; +$selected_borders_color: if($variant == 'light', darken($selected_bg_color, 15%), darken($selected_bg_color, 30%)); + +$borders_color: if($variant == 'light', transparentize($fg_color, .5), transparentize($fg_color, .9)); +$outer_borders_color: if($variant == 'light', rgba(255,255,255,0.8), lighten($bg_color, 5%)); + +$link_color: if($variant == 'light', darken($selected_bg_color, 10%), lighten($selected_bg_color, 20%)); +$link_visited_color: if($variant == 'light', darken($selected_bg_color, 20%), lighten($selected_bg_color, 10%)); // NOTE: Unused in GNOME Shell 44 + +$warning_color: {{warning_bg_color}}; +$error_color: {{error_bg_color}}; +$success_color: {{success_bg_color}}; // NOTE: Unused in GNOME Shell 44 +$destructive_color: {{destructive_bg_color}}; + +// NOTE: Used also in overview for folder colors, in search results, partially in text and for indicators below app icons +$osd_fg_color: {{osd_fg_color}}; +$osd_bg_color: $_dark_base_color; // hardcoded for both light & dark +$osd_insensitive_bg_color: transparentize(mix($osd_fg_color, opacify($osd_bg_color, 1), 10%), 0.5); // NOTE: Unused in GNOME Shell 44 +$osd_insensitive_fg_color: if($variant == 'light', mix($osd_fg_color, $osd_bg_color, 80%), mix($osd_fg_color, $osd_bg_color, 70%)); +$osd_borders_color: transparentize(black, 0.3); +$osd_outer_borders_color: transparentize($osd_fg_color, 0.9); + +$shadow_color: if($variant == 'light', rgba(0,0,0,0.1), rgba(0,0,0,0.2)); + +// button +$button_mix_factor: 9%; + +// notifications +$bubble_buttons_color: if($variant == 'light', darken($bg_color, 7%), lighten($bg_color, 5%)); + +// overview background color +$system_bg_color: darken(desaturate({{system_bg_color}}, 2%), 2%); + +//insensitive state derived colors +$insensitive_fg_color: mix($fg_color, $bg_color, 50%); +$insensitive_bg_color: mix($bg_color, $base_color, 60%); +$insensitive_borders_color: mix($borders_color, $base_color, 60%); // NOTE: Unused in GNOME Shell 44 + +//colors for the backdrop state, derived from the main colors. +// NOTE: This entire section doesn't seem to be used anywhere in GNOME Shell 44 +$backdrop_base_color: if($variant =='light', darken($base_color,1%), lighten($base_color,1%)); +$backdrop_bg_color: $bg_color; +$backdrop_fg_color: mix($fg_color, $backdrop_bg_color, 80%); +$backdrop_insensitive_color: if($variant =='light', darken($backdrop_bg_color,15%), lighten($backdrop_bg_color,15%)); +$backdrop_borders_color: mix($borders_color, $bg_color, 90%); +$backdrop_dark_fill: mix($backdrop_borders_color,$backdrop_bg_color, 35%); + +// derived checked colors +$checked_bg_color: if($variant=='light', darken($bg_color, 7%), lighten($bg_color, 7%)); +$checked_fg_color: if($variant=='light', darken($fg_color, 7%), lighten($fg_color, 7%)); // NOTE: Unused in GNOME Shell 44 + +// derived hover colors +$hover_bg_color: if($variant=='light', darken($bg_color, 3%), lighten($bg_color, 10%)); +$hover_fg_color: if($variant=='light', darken($fg_color, 5%), lighten($fg_color, 10%)); + +// derived active colors +$active_bg_color: if($variant=='light', darken($bg_color, 5%), lighten($bg_color, 12%)); +$active_fg_color: if($variant=='light', darken($fg_color, 5%), lighten($fg_color, 12%)); diff --git a/data/shell/templates/44/gnome-shell.template b/data/shell/templates/44/gnome-shell.template new file mode 100644 index 00000000..62eb8603 --- /dev/null +++ b/data/shell/templates/44/gnome-shell.template @@ -0,0 +1,15 @@ +$variant: {{theme_variant}}; + +/* Generated with Gradience + * + * Issues caused by theming should be reported to Gradience repository, and not upstream + * + * https://github.com/GradienceTeam/Gradience + */ + +@import "gnome-shell-sass/_colors"; //use gtk colors +@import "gnome-shell-sass/_drawing"; +@import "gnome-shell-sass/_common"; +@import "gnome-shell-sass/_widgets"; + +{{custom_css}} diff --git a/data/shell/templates/44/palette.template b/data/shell/templates/44/palette.template new file mode 100644 index 00000000..2ad9aa04 --- /dev/null +++ b/data/shell/templates/44/palette.template @@ -0,0 +1,46 @@ +//GNOME Color Palette +$blue_1: {{blue_1}}; +$blue_2: {{blue_2}}; +$blue_3: {{blue_3}}; +$blue_4: {{blue_4}}; +$blue_5: {{blue_5}}; +$green_1: {{green_1}}; +$green_2: {{green_2}}; +$green_3: {{green_3}}; +$green_4: {{green_4}}; +$green_5: {{green_5}}; +$yellow_1: {{yellow_1}}; +$yellow_2: {{yellow_2}}; +$yellow_3: {{yellow_3}}; +$yellow_4: {{yellow_4}}; +$yellow_5: {{yellow_5}}; +$orange_1: {{orange_1}}; +$orange_2: {{orange_2}}; +$orange_3: {{orange_3}}; +$orange_4: {{orange_4}}; +$orange_5: {{orange_5}}; +$red_1: {{red_1}}; +$red_2: {{red_2}}; +$red_3: {{red_3}}; +$red_4: {{red_4}}; +$red_5: {{red_5}}; +$purple_1: {{purple_1}}; +$purple_2: {{purple_2}}; +$purple_3: {{purple_3}}; +$purple_4: {{purple_4}}; +$purple_5: {{purple_5}}; +$brown_1: {{brown_1}}; +$brown_2: {{brown_2}}; +$brown_3: {{brown_3}}; +$brown_4: {{brown_4}}; +$brown_5: {{brown_5}}; +$light_1: {{light_1}}; +$light_2: {{light_2}}; +$light_3: {{light_3}}; +$light_4: {{light_4}}; +$light_5: {{light_5}}; +$dark_1: {{dark_1}}; +$dark_2: {{dark_2}}; +$dark_3: {{dark_3}}; +$dark_4: {{dark_4}}; +$dark_5: {{dark_5}}; diff --git a/data/shell/templates/44/panel.template b/data/shell/templates/44/panel.template new file mode 100644 index 00000000..876871b9 --- /dev/null +++ b/data/shell/templates/44/panel.template @@ -0,0 +1,232 @@ +/* Top Bar */ +// a.k.a. the panel + +$panel_height: 2.2em; +$panel_transition_duration: 250ms; // same as the overview transition duration + +#panel { + background-color: $panel_bg_color; + font-weight: bold; + height: $panel_height; + @extend %numeric; + transition-duration: $panel_transition_duration; + + // transparent panel on lock & login screens + &.unlock-screen, + &.login-screen, + &:overview { + background-color: transparent; + } + + // panel menus + .panel-button { + font-weight: bold; + color: $panel_fg_color; + -natural-hpadding: $base_padding * 2; + -minimum-hpadding: $base_padding; + transition-duration: 150ms; + border: 3px solid transparent; + border-radius: 99px; + + &.clock-display { + .clock { + transition-duration: 150ms; + border: 3px solid transparent; + border-radius: 99px; + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px $screenshot_ui_button_red; + } + + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px $warning_color; + StBoxLayout { margin: 0 $base_padding; } + } + + &.screen-recording-indicator, + &.screen-sharing-indicator { + StBoxLayout { + spacing: $base_padding; + } + + StIcon { + icon-size: $base_icon_size; + } + } + + &:active, &:overview, &:focus, &:checked { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.8); + + // The clock display needs to have the background on .clock because + // we want to exclude the do-not-disturb indicator from the background + &.clock-display { + box-shadow: none; + + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.8); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.15); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.15); + } + } + + &:hover { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.85); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.85); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.1); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.1); + } + } + + &:active:hover, &:overview:hover, &:focus:hover, &:checked:hover { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.75); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px transparentize($panel_fg_color, 0.75); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.2); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.2); + } + } + + // status area icons + .system-status-icon { + icon-size: $base_icon_size; + padding: $base_padding - 1px; + margin: 0 $base_margin; + } + + .panel-status-indicators-box .system-status-icon, + .panel-status-menu-box .system-status-icon { + margin: 0; + } + + // app menu icon + .app-menu-icon { + -st-icon-style: symbolic; + // dimensions of the icon are hardcoded + } + + &#panelActivities { + -natural-hpadding: $base_padding * 3; + } + } + + &.unlock-screen, + &.login-screen, + &:overview { + .panel-button { + &:active, &:overview, &:focus, &:checked { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.15); + + &.clock-display { + box-shadow: none; + + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.15); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.15); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.15); + } + } + + &:hover { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.10); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.10); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.1); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.1); + } + } + + &:active:hover, &:overview:hover, &:focus:hover, &:checked:hover { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.2); + &.clock-display { + box-shadow: none; + .clock { + box-shadow: inset 0 0 0 100px rgba(255,255,255, 0.2); + } + } + + &.screen-recording-indicator { + box-shadow: inset 0 0 0 100px transparentize($screenshot_ui_button_red, 0.2); + } + &.screen-sharing-indicator { + box-shadow: inset 0 0 0 100px transparentize($warning_color, 0.2); + } + } + } + } + + .panel-status-indicators-box, + .panel-status-menu-box { + spacing: 2px; + } + + // spacing between power icon and (optional) percentage label + .power-status.panel-status-indicators-box { + spacing: 0; + } + + // indicator for active + .screencast-indicator, + .remote-access-indicator { color: $warning_color; } +} + +// App Menu +#appMenu { + spacing: $base_padding; + .label-shadow { color: transparent; } +} + +#appMenu .panel-status-menu-box { + padding: 0 $base_padding; + spacing: $base_padding; +} + + +// Clock + +.clock-display-box { + spacing: 2px; + + .clock { + padding-left: $base_padding * 2; + padding-right: $base_padding * 2; + } +} diff --git a/data/shell/templates/44/switches.template b/data/shell/templates/44/switches.template new file mode 100644 index 00000000..9772a594 --- /dev/null +++ b/data/shell/templates/44/switches.template @@ -0,0 +1,16 @@ +/* Switches */ + +// these are equal to the size of the SVG assets +$switch_height: 26px; +$switch_width: 48px; + +.toggle-switch { + color: $fg_color; + height: $switch_height; + width: $switch_width; + background-size: contain; + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/toggle-off-light.svg"), url("resource:///org/gnome/shell/theme/toggle-off.svg")); + &:checked { + background-image: if($variant == 'light', url("resource:///org/gnome/shell/theme/toggle-on-light.svg"), url("assets/toggle-on.svg")); + } +} diff --git a/data/submodules b/data/submodules new file mode 160000 index 00000000..49a44283 --- /dev/null +++ b/data/submodules @@ -0,0 +1 @@ +Subproject commit 49a44283b22d831abc7898c2dd52313e3b951be3 diff --git a/data/ui/meson.build b/data/ui/meson.build index c7c423f0..529e3ad5 100644 --- a/data/ui/meson.build +++ b/data/ui/meson.build @@ -5,9 +5,11 @@ blueprints = custom_target('blueprints', 'option_row.blp', 'window.blp', 'log_out_dialog.blp', + 'monet_theming_group.blp', 'app_type_dialog.blp', 'custom_css_group.blp', 'presets_manager_window.blp', + 'reset_preset_group.blp', 'preferences_window.blp', 'plugin_row.blp', 'welcome_window.blp', @@ -15,9 +17,12 @@ blueprints = custom_target('blueprints', 'builtin_preset_row.blp', 'explore_preset_row.blp', 'save_dialog.blp', + 'shell_prefs_window.blp', + 'shell_theming_group.blp', 'repo_row.blp', 'no_plugin_window.blp', 'share_window.blp', + 'theming_empty_group.blp', ), output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'] diff --git a/data/ui/monet_theming_group.blp b/data/ui/monet_theming_group.blp new file mode 100644 index 00000000..e0a41dc6 --- /dev/null +++ b/data/ui/monet_theming_group.blp @@ -0,0 +1,44 @@ +using Gtk 4.0; +using Adw 1; + +template GradienceMonetThemingGroup : Adw.PreferencesGroup { + title: _("Monet Engine"); + description: _("Monet is an engine that generates a Material Design 3 palette from extracting image's colors."); + + Adw.ExpanderRow monet-theming-expander { + title: _("Monet Engine Options"); + subtitle: _("Choose an image, and change the parameters of a generated Monet palette"); + expanded: true; + + [action] + Button monet-apply-button { + valign: center; + label: _("Apply"); + tooltip-text: _("Apply a palette"); + clicked => on_apply_button_clicked(); + styles ["suggested-action"] + } + + Adw.ActionRow file-chooser-row { + title: _("Select an Image"); + + [suffix] + Button file-chooser-button { + valign: center; + clicked => on_file_chooser_button_clicked(); + + Adw.ButtonContent { + icon-name: "folder-pictures-symbolic"; + label: _("Choose a File"); + use-underline: true; + } + } + } + } +} + +Gtk.FileChooserNative monet-file-chooser { + title: _("Choose a Image File"); + modal: true; + //response => on_monet_file_chooser_response(); +} diff --git a/data/ui/option_row.blp b/data/ui/option_row.blp index 137dcb92..8edee639 100644 --- a/data/ui/option_row.blp +++ b/data/ui/option_row.blp @@ -30,11 +30,9 @@ template GradienceOptionRow : Adw.ActionRow { ColorButton color-value { rgba: "#00000000"; use-alpha: true; - color-set => on_color_value_changed(); } Entry text-value { text: "#00000000"; - changed => on_text_value_changed(); } } diff --git a/data/ui/preferences_window.blp b/data/ui/preferences_window.blp index b5c8cc9f..c86d47ca 100644 --- a/data/ui/preferences_window.blp +++ b/data/ui/preferences_window.blp @@ -8,6 +8,9 @@ template GradiencePreferencesWindow : Adw.PreferencesWindow { modal: true; Adw.PreferencesPage general_page { + title: _("General"); + icon-name: "applications-system-symbolic"; + Adw.PreferencesGroup flatpak_group { title: _("GTK 4 Flatpak Applications"); @@ -79,4 +82,32 @@ template GradiencePreferencesWindow : Adw.PreferencesWindow { } } } + + Adw.PreferencesPage theming_page { + title: _("Theming"); + icon-name: "larger-brush-symbolic"; + + Adw.PreferencesGroup preset_group { + title: _("Theme Engines"); + description: _("Theme Engines are the built-in theme generators for various customizable programs/frameworks."); + + Adw.ActionRow { + title: _("Monet Engine"); + subtitle: _("Monet Engine generates a Material Design 3 palette from extracting image's colors."); + activatable-widget: monet_engine_switch; + Switch monet_engine_switch { + valign: center; + } + } + + Adw.ActionRow { + title: _("Shell Engine"); + subtitle: _("Shell Engine generates a custom GNOME Shell theme based of a currently chosen preset."); + activatable-widget: gnome_shell_engine_switch; + Switch gnome_shell_engine_switch { + valign: center; + } + } + } + } } diff --git a/data/ui/reset_preset_group.blp b/data/ui/reset_preset_group.blp new file mode 100644 index 00000000..4fd6123f --- /dev/null +++ b/data/ui/reset_preset_group.blp @@ -0,0 +1,47 @@ +using Gtk 4.0; +using Adw 1; + +template GradienceResetPresetGroup : Adw.PreferencesGroup { + title: _("Reset & Restore Presets"); + description: _("This section allows you to reset an currently applied preset or restore the previous one."); + + Adw.ActionRow { + title: _("Libadwaita and GTK 4 Applications"); + + Button restore_libadw_button { + valign: center; + icon-name: "edit-undo-symbolic"; + tooltip-text: _("Restore Previous Preset"); + clicked => on_libadw_restore_button_clicked(); + styles ["flat"] + } + + Button reset_libadw_button { + valign: center; + label: _("Reset"); + tooltip-text: _("Reset Applied Preset"); + clicked => on_libadw_reset_button_clicked(); + styles ["destructive-action"] + } + } + + Adw.ActionRow { + title: _("GTK 3 Applications"); + + Button restore_gtk3_button { + valign: center; + icon-name: "edit-undo-symbolic"; + tooltip-text: _("Restore Previous Preset"); + clicked => on_gtk3_restore_button_clicked(); + styles ["flat"] + } + + Button reset_gtk3_button { + valign: center; + label: _("Reset"); + tooltip-text: _("Reset Applied Preset"); + clicked => on_gtk3_reset_button_clicked(); + styles ["destructive-action"] + } + } +} diff --git a/data/ui/shell_prefs_window.blp b/data/ui/shell_prefs_window.blp new file mode 100644 index 00000000..a0179b0b --- /dev/null +++ b/data/ui/shell_prefs_window.blp @@ -0,0 +1,17 @@ +using Gtk 4.0; +using Adw 1; + +template GradienceShellPrefsWindow : Adw.PreferencesWindow { + title: _("Shell Engine Preferences"); + search-enabled: false; + default-height: 620; + default-width: 500; + modal: true; + + Adw.PreferencesPage { + Adw.PreferencesGroup custom-colors-group { + title: _("Custom Shell Colors"); + description: _("This section allows you to customize colors that will be used in Shell theme."); + } + } +} diff --git a/data/ui/shell_theming_group.blp b/data/ui/shell_theming_group.blp new file mode 100644 index 00000000..a7d5ecef --- /dev/null +++ b/data/ui/shell_theming_group.blp @@ -0,0 +1,60 @@ +using Gtk 4.0; +using Adw 1; + +template GradienceShellThemingGroup : Adw.PreferencesGroup { + title: _("Shell Engine"); + description: _("Shell Engine generates a custom GNOME Shell theme based on the colors of a currently selected preset.\nWARNING: Extensions that modify Shell stylesheet can cause issues with themes."); + + Adw.ExpanderRow shell-theming-expander { + title: _("Shell Engine Options"); + subtitle: _("Change the parameters of a generated GNOME Shell theme"); + expanded: true; + + [action] + Button shell-apply-button { + valign: center; + label: _("Apply"); + tooltip-text: _("Apply a Shell theme"); + clicked => on_apply_button_clicked(); + styles ["suggested-action"] + } + + Adw.ActionRow custom-colors-row { + title: _("Customize Shell Theme"); + + [suffix] + Button custom-colors-button { + valign: center; + label: _("Open Shell Preferences"); + clicked => on_custom_colors_button_clicked(); + } + } + + Adw.ComboRow variant-row { + title: _("Preset Variant"); + subtitle: _("Select which preset variant you have currently applied"); + } + } +} + +Adw.ActionRow other-options-row { + [prefix] + Button restore_libadw_button { + valign: center; + icon-name: "edit-undo-symbolic"; + sensitive: false; + //tooltip-text: _("Restore Previous Theme"); + tooltip-text: _("Currently unavailable"); + clicked => on_restore_button_clicked(); + styles ["flat"] + } + + [suffix] + Button reset_theme_button { + valign: center; + label: _("Reset Theme"); + tooltip-text: _("Reset an applied theme"); + clicked => on_reset_theme_clicked(); + styles ["destructive-action"] + } +} diff --git a/data/ui/theming_empty_group.blp b/data/ui/theming_empty_group.blp new file mode 100644 index 00000000..68f266ab --- /dev/null +++ b/data/ui/theming_empty_group.blp @@ -0,0 +1,19 @@ +using Gtk 4.0; +using Adw 1; + +template GradienceEmptyThemingGroup : Adw.PreferencesGroup { + title: _("No Theme Engines"); + description: _("Theme Engines extends the functionality of Gradience. They can be enabled in the Preferences."); + + Adw.ActionRow open-preferences { + title: _("Open Preferences to manage Theme Engines"); + + [suffix] + Button open { + valign: center; + label: _("Open Preferences"); + tooltip-text: _("Open Preferences"); + action-name: "app.preferences"; + } + } +} diff --git a/data/ui/welcome_window.blp b/data/ui/welcome_window.blp index df529897..e96590a0 100644 --- a/data/ui/welcome_window.blp +++ b/data/ui/welcome_window.blp @@ -68,8 +68,8 @@ template GradienceWelcomeWindow: Adw.Window { Adw.StatusPage page_release { icon-name: "software-update-available-symbolic"; - title: _("What's new in 0.3.2"); - description: _("In this release, we fixed the Firefox GNOME theme plugin, issues with presets always being saved with the same name, as well as some UX polish, and more."); + title: _("What's new in 0.8.0"); + description: _("In this release, we added GNOME Shell theming support and reworked how Gradience work internally."); } Adw.StatusPage page_agreement { diff --git a/data/ui/window.blp b/data/ui/window.blp index 09e95c09..90fccfa2 100644 --- a/data/ui/window.blp +++ b/data/ui/window.blp @@ -88,16 +88,16 @@ template GradienceMainWindow : Adw.ApplicationWindow { title: _("_Colors"); icon-name: "larger-brush-symbolic"; - child: Adw.PreferencesPage content { }; + child: Adw.PreferencesPage content-colors { }; use-underline: true; } Adw.ViewStackPage { - name: "monet"; - title: _("_Monet"); + name: "theming"; + title: _("_Theming"); icon-name: "color-picker-symbolic"; - child: Adw.PreferencesPage content_monet { }; + child: Adw.PreferencesPage content-theming { }; use-underline: true; } @@ -106,7 +106,7 @@ template GradienceMainWindow : Adw.ApplicationWindow { title: _("_Advanced"); icon-name: "settings-symbolic"; - child: Adw.PreferencesPage content_plugins { }; + child: Adw.PreferencesPage content-plugins { }; use-underline: true; } } @@ -123,17 +123,6 @@ template GradienceMainWindow : Adw.ApplicationWindow { menu main-menu { - section { - item { - label: _("Restore Applied Color Scheme"); - action: "app.restore_color_scheme"; - } - - item { - label: _("Reset Applied Color Scheme"); - action: "app.reset_color_scheme"; - } - } section { item { label: _("Preferences"); diff --git a/gradience/backend/css_parser.py b/gradience/backend/css_parser.py index a114db37..369128ae 100644 --- a/gradience/backend/css_parser.py +++ b/gradience/backend/css_parser.py @@ -18,19 +18,8 @@ import re +from gradience.backend.globals import adw_palette_prefixes -# Adwaita named palette colors dict -adw_colors = [ - "blue_", - "green_", - "yellow_", - "orange_", - "red_", - "purple_", - "brown_", - "light_", - "dark_", -] # Regular expressions define_color = re.compile(r"(@define-color .*[^\s])") @@ -41,7 +30,7 @@ def parse_css(path): variables = {} palette = {} - for color in adw_colors: + for color in adw_palette_prefixes: palette[color] = {} with open(path, "r", encoding="utf-8") as sheet: @@ -51,7 +40,7 @@ def parse_css(path): if cdefine_match != None: # If @define-color variable declarations were found palette_part = cdefine_match.__getitem__(1) # Get the second item of the re.Match object name, color = palette_part.split(" ", 1)[1].split(" ", 1) - if name.startswith(tuple(adw_colors)): # Palette colors + if name.startswith(tuple(adw_palette_prefixes)): # Palette colors palette[name[:-1]][name[-1:]] = color[:-1] else: # Other color variables variables[name] = color[:-1] diff --git a/gradience/backend/exceptions.py b/gradience/backend/exceptions.py new file mode 100644 index 00000000..a4c87544 --- /dev/null +++ b/gradience/backend/exceptions.py @@ -0,0 +1,28 @@ +# css_parser.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +class GradienceError(Exception): + """ Base class for all other exceptions in Gradience. """ + pass + + +# TODO: Move this module somewhere else later +class UnsupportedShellVersion(GradienceError): + """ Exception raised when the shell version is not supported. """ + pass diff --git a/gradience/backend/globals.py b/gradience/backend/globals.py index 5411b89e..b07ecdab 100644 --- a/gradience/backend/globals.py +++ b/gradience/backend/globals.py @@ -23,40 +23,67 @@ from gi.repository import Xdp from gradience.backend import constants -presets_dir = os.path.join( - os.environ.get("XDG_CONFIG_HOME", os.environ["HOME"] + "/.config"), - "presets" +user_config_dir = os.environ.get( + "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" ) +user_data_dir = os.environ.get( + "XDG_DATA_HOME", os.environ["HOME"] + "/.local/share" +) + +user_cache_dir = os.environ.get( + "XDG_CACHE_HOME", os.environ["HOME"] + "/.cache" +) + +presets_dir = os.path.join(user_config_dir, "presets") + +user_plugin_dir = os.path.join(user_data_dir, "gradience", "plugins") +system_plugin_dir = os.path.join(constants.pkgdatadir, "plugins") + preset_repos = { "Official": "https://github.com/GradienceTeam/Community/raw/next/official.json", "Curated": "https://github.com/GradienceTeam/Community/raw/next/curated.json" } -user_plugin_dir = os.path.join( - os.environ.get("XDG_DATA_HOME", os.environ["HOME"] + "/.local/share"), - "gradience", - "plugins" -) +# Adwaita named UI colors prefixes list +# NOTE: Remember to update this list if new libadwaita version brings up new variables +adw_variables_prefixes = [ + "accent_", + "destructive_", + "success_", + "warning_", + "error_", + "window_", + "view_", + "headerbar_", + "card_", + "dialog_", + "popover_", + "shade_", + "scrollbar_", + "borders" +] -system_plugin_dir = os.path.join( - constants.pkgdatadir, - "plugins" -) +# Adwaita named palette colors prefixes list +# NOTE: Remember to update this list if new libadwaita version brings up new variables +adw_palette_prefixes = [ + "blue_", + "green_", + "yellow_", + "orange_", + "red_", + "purple_", + "brown_", + "light_", + "dark_" +] -def get_gtk_theme_dir(app_type): +def get_gtk_theme_dir(app_type: str): if app_type == "gtk4": - theme_dir = os.path.join( - os.environ.get("XDG_CONFIG_HOME", - os.environ["HOME"] + "/.config"), - "gtk-4.0" - ) - elif app_type == "gtk3": - theme_dir = os.path.join( - os.environ.get("XDG_CONFIG_HOME", - os.environ["HOME"] + "/.config"), - "gtk-3.0" - ) + theme_dir = os.path.join(user_config_dir, "gtk-4.0") + + if app_type == "gtk3": + theme_dir = os.path.join(user_config_dir, "gtk-3.0") return theme_dir @@ -66,6 +93,3 @@ def is_sandboxed(): is_sandboxed = portal.running_under_sandbox() return is_sandboxed - -def get_available_sassc(): - pass diff --git a/gradience/backend/meson.build b/gradience/backend/meson.build index 7f60bd4d..db50f198 100644 --- a/gradience/backend/meson.build +++ b/gradience/backend/meson.build @@ -30,6 +30,7 @@ gradience_sources = [ 'flatpak_overrides.py', 'globals.py', 'logger.py', - 'preset_downloader.py' + 'preset_downloader.py', + 'exceptions.py' ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: backenddir) diff --git a/gradience/backend/models/preset.py b/gradience/backend/models/preset.py index 80883c6c..eca52649 100644 --- a/gradience/backend/models/preset.py +++ b/gradience/backend/models/preset.py @@ -1,7 +1,7 @@ # preset.py # # Change the look of Adwaita, with ease -# Copyright (C) 2022 Gradience Team +# Copyright (C) 2022-2023, Gradience Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,7 +27,7 @@ from gradience.backend.logger import Logger logging = Logger() -# Adwaita default colors palette +# Adwaita default colors palette dict adw_palette = { "blue_": { "1": "#99c1f1", @@ -94,23 +94,25 @@ adw_palette = { } } -# Supported app types that can utilize custom CSS -custom_css_app_types = [ +# Supported GTK versions that can utilize custom CSS +custom_css_gtk_versions = [ "gtk4", "gtk3" ] class Preset: + display_name = "New Preset" + preset_path = "new_preset" + variables = {} palette = adw_palette custom_css = { "gtk4": "", - "gtk3": "", + "gtk3": "" } + plugins = {} - display_name = "New Preset" - preset_path = "new_preset" plugins_list = {} badges = {} @@ -122,10 +124,13 @@ class Preset: if display_name: self.display_name = display_name + if palette: self.palette = palette + if custom_css: self.custom_css = custom_css + if badges: self.badges = badges @@ -182,7 +187,7 @@ class Preset: if "custom_css" in preset: self.custom_css = preset["custom_css"] else: - for app_type in custom_css_app_types: + for app_type in custom_css_gtk_versions: self.custom_css[app_type] = "" except Exception as e: logging.error("Failed to create a new preset object.", exc=e) @@ -193,8 +198,8 @@ class Preset: self.display_name = name old_path = self.preset_path self.preset_path = os.path.join( - os.path.dirname(self.preset_path), - to_slug_case(name) + ".json") + os.path.dirname(self.preset_path), to_slug_case(name) + ".json" + ) self.save_to_file(to=self.preset_path) os.remove(old_path) @@ -207,6 +212,7 @@ class Preset: "custom_css": self.custom_css, "plugins": self.plugins_list } + json_output = json.dumps(preset_dict, indent=indent) return json_output @@ -218,23 +224,14 @@ class Preset: if to is None: filename = to_slug_case(name) if name else to_slug_case(self.display_name) self.preset_path = os.path.join( - presets_dir, "user", filename + ".json") + presets_dir, "user", filename + ".json" + ) else: self.preset_path = to - if not os.path.exists( - os.path.join( - presets_dir, - "user", - ) - ): + if not os.path.exists(os.path.join(presets_dir, "user")): try: - os.makedirs( - os.path.join( - presets_dir, - "user", - ) - ) + os.makedirs(os.path.join(presets_dir, "user")) except OSError as e: logging.error("Failed to create a new preset directory.", exc=e) raise diff --git a/gradience/backend/theming/meson.build b/gradience/backend/theming/meson.build index 8b40abbc..936b43f8 100644 --- a/gradience/backend/theming/meson.build +++ b/gradience/backend/theming/meson.build @@ -3,6 +3,7 @@ themingdir = 'gradience/backend/theming' gradience_sources = [ '__init__.py', 'monet.py', - 'preset_utils.py' + 'preset.py', + 'shell.py' ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: themingdir) diff --git a/gradience/backend/theming/monet.py b/gradience/backend/theming/monet.py index 6d60d704..845aefb7 100644 --- a/gradience/backend/theming/monet.py +++ b/gradience/backend/theming/monet.py @@ -18,9 +18,13 @@ import os +import material_color_utilities_python as monet + from svglib.svglib import svg2rlg from reportlab.graphics import renderPM -import material_color_utilities_python as monet + +from gradience.backend.models.preset import Preset +from gradience.backend.utils.colors import argb_to_color_code from gradience.backend.logger import Logger @@ -31,7 +35,7 @@ class Monet: def __init__(self): self.palette = None - def generate_from_image(self, image_path: str) -> dict: + def generate_palette_from_image(self, image_path: str) -> dict: if image_path.endswith(".svg"): drawing = svg2rlg(image_path) image_path = os.path.join( @@ -52,6 +56,7 @@ class Monet: basewidth = 64 wpercent = basewidth / float(monet_img.size[0]) hsize = int((float(monet_img.size[1]) * float(wpercent))) + monet_img = monet_img.resize( (basewidth, hsize), monet.Image.Resampling.LANCZOS ) @@ -59,3 +64,114 @@ class Monet: self.palette = monet.themeFromImage(monet_img) return self.palette + + def new_preset_from_monet(self, name=None, monet_palette=None, props=None, obj_only=False) -> Preset or None: + preset = Preset() + + if props: + tone = props[0] + theme = props[1] + else: + raise AttributeError("Properties 'tone' and/or 'theme' missing") + + if not monet_palette: + raise AttributeError("Property 'monet_palette' missing") + + if theme == "light": + light_theme = monet_palette["schemes"]["light"] + variable = { + "accent_color": argb_to_color_code(light_theme.primary), + "accent_bg_color": argb_to_color_code(light_theme.primary), + "accent_fg_color": argb_to_color_code(light_theme.onPrimary), + "destructive_color": argb_to_color_code(light_theme.error), + "destructive_bg_color": argb_to_color_code(light_theme.errorContainer), + # Avoid using .onError as it causes contrast issues + "destructive_fg_color": argb_to_color_code(light_theme.onErrorContainer), + "success_color": argb_to_color_code(light_theme.tertiary), + "success_bg_color": argb_to_color_code(light_theme.tertiaryContainer), + "success_fg_color": argb_to_color_code(light_theme.onTertiaryContainer), + "warning_color": argb_to_color_code(light_theme.secondary), + "warning_bg_color": argb_to_color_code(light_theme.secondaryContainer), + "warning_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), + "error_color": argb_to_color_code(light_theme.error), + "error_bg_color": argb_to_color_code(light_theme.errorContainer), + # Avoid using .onError as it causes contrast issues + "error_fg_color": argb_to_color_code(light_theme.onErrorContainer), + "window_bg_color": argb_to_color_code(light_theme.surface), + "window_fg_color": argb_to_color_code(light_theme.onSurface), + "view_bg_color": argb_to_color_code(light_theme.secondaryContainer), + "view_fg_color": argb_to_color_code(light_theme.onSurface), + "headerbar_bg_color": argb_to_color_code(light_theme.primary, "0.08"), + "headerbar_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), + "headerbar_border_color": argb_to_color_code(light_theme.onSurface, "0.8"), + "headerbar_backdrop_color": "@window_bg_color", + "headerbar_shade_color": argb_to_color_code(light_theme.onSurface, "0.07"), + "card_bg_color": argb_to_color_code(light_theme.primary, "0.05"), + "card_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), + "card_shade_color": argb_to_color_code(light_theme.shadow, "0.07"), + "dialog_bg_color": argb_to_color_code(light_theme.secondaryContainer), + "dialog_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), + "popover_bg_color": argb_to_color_code(light_theme.secondaryContainer), + "popover_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), + "shade_color": argb_to_color_code(light_theme.shadow, "0.07"), + "scrollbar_outline_color": argb_to_color_code(light_theme.outline), + } + elif theme == "dark": + dark_theme = monet_palette["schemes"]["dark"] + variable = { + "accent_color": argb_to_color_code(dark_theme.primary), + "accent_bg_color": argb_to_color_code(dark_theme.primary), + "accent_fg_color": argb_to_color_code(dark_theme.onPrimary), + "destructive_color": argb_to_color_code(dark_theme.error), + "destructive_bg_color": argb_to_color_code(dark_theme.errorContainer), + # Avoid using .onError as it causes contrast issues + "destructive_fg_color": argb_to_color_code(dark_theme.onErrorContainer), + "success_color": argb_to_color_code(dark_theme.tertiary), + "success_bg_color": argb_to_color_code(dark_theme.tertiaryContainer), + "success_fg_color": argb_to_color_code(dark_theme.onTertiaryContainer), + "warning_color": argb_to_color_code(dark_theme.secondary), + "warning_bg_color": argb_to_color_code(dark_theme.secondaryContainer), + "warning_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), + "error_color": argb_to_color_code(dark_theme.error), + "error_bg_color": argb_to_color_code(dark_theme.errorContainer), + # Avoid using .onError as it causes contrast issues + "error_fg_color": argb_to_color_code(dark_theme.onErrorContainer), + "window_bg_color": argb_to_color_code(dark_theme.surface), + "window_fg_color": argb_to_color_code(dark_theme.onSurface), + "view_bg_color": argb_to_color_code(dark_theme.secondaryContainer), + "view_fg_color": argb_to_color_code(dark_theme.onSurface), + "headerbar_bg_color": argb_to_color_code(dark_theme.primary, "0.08"), + "headerbar_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), + "headerbar_border_color": argb_to_color_code(dark_theme.onSurface, "0.8"), + "headerbar_backdrop_color": "@window_bg_color", + "headerbar_shade_color": argb_to_color_code(dark_theme.onSurface, "0.07"), + "card_bg_color": argb_to_color_code(dark_theme.primary, "0.05"), + "card_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), + "card_shade_color": argb_to_color_code(dark_theme.shadow, "0.07"), + "dialog_bg_color": argb_to_color_code(dark_theme.secondaryContainer), + "dialog_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), + "popover_bg_color": argb_to_color_code(dark_theme.secondaryContainer), + "popover_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), + "shade_color": argb_to_color_code(dark_theme.shadow, "0.36"), + "scrollbar_outline_color": argb_to_color_code(dark_theme.outline, "0.5"), + } + else: + raise AttributeError("Unknown theme variant selected") + + if obj_only == False and not name: + raise AttributeError("You either need to set 'obj_only' property to True, or add value to 'name' property") + + if obj_only: + if name: + preset.new(variables=variable, display_name=name) + else: + preset.new(variables=variable) + return preset + + if obj_only == False: + preset.new(variables=variable, display_name=name) + + try: + preset.save_to_file() + except OSError: + raise diff --git a/gradience/backend/theming/preset.py b/gradience/backend/theming/preset.py new file mode 100644 index 00000000..5c9a1b86 --- /dev/null +++ b/gradience/backend/theming/preset.py @@ -0,0 +1,158 @@ +# preset.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022-2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import json + +from pathlib import Path + +from gi.repository import GLib, Gio + +from gradience.backend.models.preset import Preset + +from gradience.backend.utils.theming import generate_gtk_css +from gradience.backend.globals import user_config_dir, presets_dir, get_gtk_theme_dir + +from gradience.backend.logger import Logger + +logging = Logger() + + +class PresetUtils: + def __init__(self): + pass + + def get_presets_list(self, repo=None, full_list=False) -> dict: + presets_list = {} + + def __get_repo_presets(repo): + if repo.is_dir(): + for file_name in repo.iterdir(): + file_name = str(file_name) + if file_name.endswith(".json"): + try: + with open(os.path.join(presets_dir, file_name), "r", encoding="utf-8") as file: + preset_text = file.read() + file.close() + except (OSError, KeyError) as e: + logging.error("Failed to load preset information.", exc=e) + raise + else: + preset = json.loads(preset_text) + + if preset.get("variables") is None: + raise KeyError("'variables' section missing in loaded preset file") + + if preset.get("palette") is None: + raise KeyError("'palette' section missing in loaded preset file") + + presets_list[file_name] = preset["name"] + elif repo.is_file(): + # this exists to keep compatibility with old preset structure + if repo.name.endswith(".json"): + logging.warning("Legacy preset structure found. Moving to a new structure.") + + try: + if not os.path.isdir(os.path.join(presets_dir, "user")): + os.mkdir(os.path.join(presets_dir, "user")) + + os.rename(repo, os.path.join( + presets_dir, "user", repo.name)) + + with open(os.path.join(presets_dir, "user", repo), "r", encoding="utf-8") as file: + preset_text = file.read() + file.close() + except (OSError, KeyError) as e: + logging.error("Failed to load preset information.", exc=e) + raise + else: + preset = json.loads(preset_text) + + if preset.get("variables") is None: + raise KeyError("'variables' section missing in loaded preset file") + + if preset.get("palette") is None: + raise KeyError("'palette' section missing in loaded preset file") + + presets_list["user"][file_name] = preset["name"] + + if full_list: + for repo in Path(presets_dir).iterdir(): + logging.debug(f"presets_dir.iterdir: {repo}") + __get_repo_presets(repo) + + return presets_list + elif repo: + __get_repo_presets(repo) + + return presets_list + else: + raise AttributeError("You either need to set 'repo' property, or change 'full_list' property to True") + + def apply_preset(self, app_type: str, preset: Preset) -> None: + theme_dir = get_gtk_theme_dir(app_type) + gtk_css_path = os.path.join(theme_dir, "gtk.css") + + if not os.path.exists(theme_dir): + os.makedirs(theme_dir) + + try: + with open(gtk_css_path, "r", encoding="utf-8") as css_file: + contents = css_file.read() + css_file.close() + except FileNotFoundError: + logging.warning(f"gtk.css file not found in {gtk_css_path}. Generating new stylesheet.") + else: + with open(gtk_css_path + ".bak", "w", encoding="utf-8") as backup: + backup.write(contents) + backup.close() + finally: + with open(gtk_css_path, "w", encoding="utf-8") as css_file: + css_file.write(generate_gtk_css(app_type, preset)) + css_file.close() + + def restore_preset(self, app_type: str) -> None: + theme_dir = get_gtk_theme_dir(app_type) + gtk_css_path = os.path.join(theme_dir, "gtk.css") + + try: + with open(gtk_css_path + ".bak", "r", encoding="utf-8") as backup: + contents = backup.read() + backup.close() + + with open(gtk_css_path, "w", encoding="utf-8") as css_file: + css_file.write(contents) + css_file.close() + except OSError as e: + logging.error(f"Unable to restore {app_type.capitalize()} preset backup.", exc=e) + raise + + def reset_preset(self, app_type: str) -> None: + theme_dir = get_gtk_theme_dir(app_type) + gtk_css_path = os.path.join(theme_dir, "gtk.css") + + file = Gio.File.new_for_path(gtk_css_path) + + try: + file.delete() + except GLib.GError as e: + if e.code == 1: + return + + logging.error("Unable to delete current preset.", exc=e) + raise diff --git a/gradience/backend/theming/preset_utils.py b/gradience/backend/theming/preset_utils.py deleted file mode 100644 index 8c57f7aa..00000000 --- a/gradience/backend/theming/preset_utils.py +++ /dev/null @@ -1,356 +0,0 @@ -# preset_utils.py -# -# Change the look of Adwaita, with ease -# Copyright (C) 2022 Gradience Team -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import json - -from pathlib import Path - -from gi.repository import GLib, Gio - -from gradience.backend.models.preset import Preset -from gradience.backend.utils.colors import argb_to_color_code - -from gradience.backend.globals import presets_dir, get_gtk_theme_dir - -from gradience.backend.logger import Logger - -logging = Logger() - - -class PresetUtils: - def __init__(self): - self.preset = Preset() - - def generate_gtk_css(self, app_type: str, preset: Preset) -> str: - variables = preset.variables - palette = preset.palette - custom_css = preset.custom_css - - final_css = "" - - for key in variables.keys(): - final_css += f"@define-color {key} {variables[key]};\n" - - for prefix_key in palette.keys(): - for key in palette[prefix_key].keys(): - final_css += f"@define-color {prefix_key + key} {palette[prefix_key][key]};\n" - - final_css += custom_css.get(app_type, "") - - return final_css - - def new_preset_from_monet(self, name=None, monet_palette=None, props=None, obj_only=False) -> Preset or None: - if props: - tone = props[0] - theme = props[1] - else: - raise AttributeError("Properties 'tone' and/or 'theme' missing") - - if not monet_palette: - raise AttributeError("Property 'monet_palette' missing") - - if theme == "light": - light_theme = monet_palette["schemes"]["light"] - variable = { - "accent_color": argb_to_color_code(light_theme.primary), - "accent_bg_color": argb_to_color_code(light_theme.primary), - "accent_fg_color": argb_to_color_code(light_theme.onPrimary), - "destructive_color": argb_to_color_code(light_theme.error), - "destructive_bg_color": argb_to_color_code(light_theme.errorContainer), - # Avoid using .onError as it causes contrast issues - "destructive_fg_color": argb_to_color_code(light_theme.onErrorContainer), - "success_color": argb_to_color_code(light_theme.tertiary), - "success_bg_color": argb_to_color_code(light_theme.tertiaryContainer), - "success_fg_color": argb_to_color_code(light_theme.onTertiaryContainer), - "warning_color": argb_to_color_code(light_theme.secondary), - "warning_bg_color": argb_to_color_code(light_theme.secondaryContainer), - "warning_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), - "error_color": argb_to_color_code(light_theme.error), - "error_bg_color": argb_to_color_code(light_theme.errorContainer), - # Avoid using .onError as it causes contrast issues - "error_fg_color": argb_to_color_code(light_theme.onErrorContainer), - "window_bg_color": argb_to_color_code(light_theme.surface), - "window_fg_color": argb_to_color_code(light_theme.onSurface), - "view_bg_color": argb_to_color_code(light_theme.secondaryContainer), - "view_fg_color": argb_to_color_code(light_theme.onSurface), - "headerbar_bg_color": argb_to_color_code(light_theme.primary, "0.08"), - "headerbar_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), - "headerbar_border_color": argb_to_color_code(light_theme.onSurface, "0.8"), - "headerbar_backdrop_color": "@window_bg_color", - "headerbar_shade_color": argb_to_color_code(light_theme.onSurface, "0.07"), - "card_bg_color": argb_to_color_code(light_theme.primary, "0.05"), - "card_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), - "card_shade_color": argb_to_color_code(light_theme.shadow, "0.07"), - "dialog_bg_color": argb_to_color_code(light_theme.secondaryContainer), - "dialog_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), - "popover_bg_color": argb_to_color_code(light_theme.secondaryContainer), - "popover_fg_color": argb_to_color_code(light_theme.onSecondaryContainer), - "shade_color": argb_to_color_code(light_theme.shadow, "0.07"), - "scrollbar_outline_color": argb_to_color_code(light_theme.outline), - } - elif theme == "dark": - dark_theme = monet_palette["schemes"]["dark"] - variable = { - "accent_color": argb_to_color_code(dark_theme.primary), - "accent_bg_color": argb_to_color_code(dark_theme.primary), - "accent_fg_color": argb_to_color_code(dark_theme.onPrimary), - "destructive_color": argb_to_color_code(dark_theme.error), - "destructive_bg_color": argb_to_color_code(dark_theme.errorContainer), - # Avoid using .onError as it causes contrast issues - "destructive_fg_color": argb_to_color_code(dark_theme.onErrorContainer), - "success_color": argb_to_color_code(dark_theme.tertiary), - "success_bg_color": argb_to_color_code(dark_theme.tertiaryContainer), - "success_fg_color": argb_to_color_code(dark_theme.onTertiaryContainer), - "warning_color": argb_to_color_code(dark_theme.secondary), - "warning_bg_color": argb_to_color_code(dark_theme.secondaryContainer), - "warning_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), - "error_color": argb_to_color_code(dark_theme.error), - "error_bg_color": argb_to_color_code(dark_theme.errorContainer), - # Avoid using .onError as it causes contrast issues - "error_fg_color": argb_to_color_code(dark_theme.onErrorContainer), - "window_bg_color": argb_to_color_code(dark_theme.surface), - "window_fg_color": argb_to_color_code(dark_theme.onSurface), - "view_bg_color": argb_to_color_code(dark_theme.secondaryContainer), - "view_fg_color": argb_to_color_code(dark_theme.onSurface), - "headerbar_bg_color": argb_to_color_code(dark_theme.primary, "0.08"), - "headerbar_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), - "headerbar_border_color": argb_to_color_code(dark_theme.onSurface, "0.8"), - "headerbar_backdrop_color": "@window_bg_color", - "headerbar_shade_color": argb_to_color_code(dark_theme.onSurface, "0.07"), - "card_bg_color": argb_to_color_code(dark_theme.primary, "0.05"), - "card_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), - "card_shade_color": argb_to_color_code(dark_theme.shadow, "0.07"), - "dialog_bg_color": argb_to_color_code(dark_theme.secondaryContainer), - "dialog_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), - "popover_bg_color": argb_to_color_code(dark_theme.secondaryContainer), - "popover_fg_color": argb_to_color_code(dark_theme.onSecondaryContainer), - "shade_color": argb_to_color_code(dark_theme.shadow, "0.36"), - "scrollbar_outline_color": argb_to_color_code(dark_theme.outline, "0.5"), - } - - if obj_only == False and not name: - raise AttributeError("You either need to set 'obj_only' property to True, or add value to 'name' property") - - if obj_only: - if name: - logging.debug("with name, obj_only") - self.preset.new(variables=variable, display_name=name) - else: - logging.debug("no name, obj_only") - self.preset.new(variables=variable) - return self.preset - - if obj_only == False: - logging.debug("no obj_only, name") - self.preset.new(variables=variable, display_name=name) - - try: - self.preset.save_to_file() - except OSError: - raise - - def get_presets_list(self, repo=None, full_list=False) -> dict: - presets_list = {} - - def get_repo_presets(repo): - if repo.is_dir(): - for file_name in repo.iterdir(): - file_name = str(file_name) - if file_name.endswith(".json"): - try: - with open( - os.path.join(presets_dir, file_name), - "r", - encoding="utf-8", - ) as file: - preset_text = file.read() - file.close() - except (OSError, KeyError) as e: - logging.error("Failed to load preset information.", exc=e) - raise - else: - preset = json.loads(preset_text) - if preset.get("variables") is None: - raise KeyError("'variables' section missing in loaded preset file") - if preset.get("palette") is None: - raise KeyError("'palette' section missing in loaded preset file") - presets_list[file_name] = preset[ - "name" - ] - elif repo.is_file(): - # this exists to keep compatibility with old presets - if repo.name.endswith(".json"): - logging.warning("Legacy preset found. Moving to new structure.") - - try: - if not os.path.isdir(os.path.join(presets_dir, "user")): - os.mkdir(os.path.join(presets_dir, "user")) - - os.rename(repo, os.path.join( - presets_dir, "user", repo.name)) - - with open( - os.path.join(presets_dir, "user", repo), - "r", - encoding="utf-8", - ) as file: - preset_text = file.read() - file.close() - except (OSError, KeyError) as e: - logging.error("Failed to load preset information.", exc=e) - raise - else: - preset = json.loads(preset_text) - if preset.get("variables") is None: - raise KeyError("'variables' section missing in loaded preset file") - if preset.get("palette") is None: - raise KeyError("'palette' section missing in loaded preset file") - presets_list["user"][file_name] = preset[ - "name" - ] - - if full_list: - for repo in Path(presets_dir).iterdir(): - logging.debug(f"presets_dir.iterdir: {repo}") - get_repo_presets(repo) - return presets_list - elif repo: - get_repo_presets(repo) - return presets_list - else: - raise AttributeError("You either need to set 'repo' property, or change 'full_list' property to True") - - def apply_preset(self, app_type: str, preset: Preset) -> None: - if app_type == "gtk4": - theme_dir = get_gtk_theme_dir(app_type) - - if not os.path.exists(theme_dir): - os.makedirs(theme_dir) - - gtk4_css = self.generate_gtk_css("gtk4", preset) - contents = "" - - try: - with open( - os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8" - ) as file: - contents = file.read() - except FileNotFoundError: # first run - pass - else: - with open( - os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8" - ) as file: - file.write(contents) - finally: - with open( - os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8" - ) as file: - file.write(gtk4_css) - elif app_type == "gtk3": - theme_dir = get_gtk_theme_dir(app_type) - - if not os.path.exists(theme_dir): - os.makedirs(theme_dir) - - gtk3_css = self.generate_gtk_css("gtk3", preset) - contents = "" - - try: - with open( - os.path.join(theme_dir, "gtk.css"), "r", encoding="utf-8" - ) as file: - contents = file.read() - except FileNotFoundError: # first run - pass - else: - with open( - os.path.join(theme_dir, "gtk.css.bak"), "w", encoding="utf-8" - ) as file: - file.write(contents) - finally: - with open( - os.path.join(theme_dir, "gtk.css"), "w", encoding="utf-8" - ) as file: - file.write(gtk3_css) - - def restore_gtk4_preset(self) -> None: - try: - with open( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + - "/.config" - ), - "gtk-4.0/gtk.css.bak", - ), - "r", - encoding="utf-8", - ) as backup: - contents = backup.read() - backup.close() - - with open( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + - "/.config" - ), - "gtk-4.0/gtk.css", - ), - "w", - encoding="utf-8", - ) as gtk4css: - gtk4css.write(contents) - gtk4css.close() - except OSError as e: - logging.error("Unable to restore Gtk4 backup.", exc=e) - raise - - def reset_preset(self, app_type: str) -> None: - if app_type == "gtk4": - file = Gio.File.new_for_path( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" - ), - "gtk-4.0/gtk.css", - ) - ) - - try: - file.delete() - except GLib.GError as e: - logging.error("Unable to delete current preset.", exc=e) - raise - elif app_type == "gtk3": - file = Gio.File.new_for_path( - os.path.join( - os.environ.get( - "XDG_CONFIG_HOME", os.environ["HOME"] + "/.config" - ), - "gtk-3.0/gtk.css", - ) - ) - - try: - file.delete() - except GLib.GError as e: - logging.error("Unable to delete current preset.", exc=e) - raise diff --git a/gradience/backend/theming/shell.py b/gradience/backend/theming/shell.py new file mode 100644 index 00000000..3cd29a71 --- /dev/null +++ b/gradience/backend/theming/shell.py @@ -0,0 +1,347 @@ +# shell.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import re +import shutil +import os.path +import sass + +from gi.repository import GObject, Gio, GLib + +from gradience.backend.models.preset import Preset +from gradience.backend.utils.colors import color_vars_to_color_code +from gradience.backend.utils.gnome import get_shell_version, get_shell_colors +from gradience.backend.utils.gsettings import GSettingsSetting, FlatpakGSettings, GSettingsMissingError +from gradience.backend.constants import datadir + +from gradience.backend.logger import Logger +from gradience.backend.exceptions import UnsupportedShellVersion +from gradience.backend.globals import is_sandboxed + +logging = Logger(logger_name="ShellTheme") + + +class ShellTheme: + # Supported GNOME Shell versions: 42, 43, 44 + shell_versions = [42, 43, 44] + shell_versions_str = [str(version) for version in shell_versions] + version_target = None + + theme_variant = None + + shell_colors = {} + + preset_variables = {} + preset_palette = {} + + custom_css = None + + def __init__(self, shell_version=None): + self._cancellable = Gio.Cancellable() + + if not shell_version: + self._detect_shell_version() + elif shell_version and shell_version in self.shell_versions: + self.version_target = shell_version + else: + raise UnsupportedShellVersion( + f"GNOME Shell version {shell_version} is not supported. (Supported versions: {', '.join(self.shell_versions_str)})") + + self.THEME_GSETTINGS_SCHEMA_ID = "org.gnome.shell.extensions.user-theme" + self.THEME_GSETTINGS_SCHEMA_PATH = "/org/gnome/shell/extensions/user-theme/" + self.THEME_GSETTINGS_SCHEMA_KEY = "name" + + self.THEME_EXT_NAME = "user-theme@gnome-shell-extensions.gcampax.github.com" + self.THEME_GSETTINGS_DIR = os.path.join(GLib.get_home_dir(), ".local/share/", + "gnome-shell", "extensions", self.THEME_EXT_NAME, "schemas") + + try: + if os.path.exists(self.THEME_GSETTINGS_DIR): + if not is_sandboxed(): + self.settings = GSettingsSetting(self.THEME_GSETTINGS_SCHEMA_ID, + schema_dir=self.THEME_GSETTINGS_DIR) + else: + self.settings = FlatpakGSettings(self.THEME_GSETTINGS_SCHEMA_ID, + schema_dir=self.THEME_GSETTINGS_DIR) + else: + if not is_sandboxed(): + self.settings = GSettingsSetting(self.THEME_GSETTINGS_SCHEMA_ID) + else: + self.settings = FlatpakGSettings(self.THEME_GSETTINGS_SCHEMA_ID) + except (GSettingsMissingError, GLib.GError): + raise + + # Theme source/output paths + self.templates_dir = os.path.join(datadir, "gradience", "shell", "templates", str(self.version_target)) + self.source_dir = os.path.join(GLib.get_home_dir(), ".cache", "gradience", "gradience-shell", str(self.version_target)) + + if os.path.exists(self.source_dir): + shutil.rmtree(self.source_dir) + + # Copy shell theme source directories to ~/.cache/gradience/gradience-shell + shutil.copytree(os.path.join(datadir, "gradience", "shell", + str(self.version_target)), self.source_dir, dirs_exist_ok=True + ) + + # TODO: Allow user to use different name than "gradience-shell" (also, with default name, we should append "-light" suffix when generated from light preset) + self.output_dir = os.path.join(GLib.get_home_dir(), ".local/share/themes", "gradience-shell", "gnome-shell") + + self.main_template = os.path.join(self.templates_dir, "gnome-shell.template") + self.colors_template = os.path.join(self.templates_dir, "colors.template") + self.palette_template = os.path.join(self.templates_dir, "palette.template") + self.switches_template = os.path.join(self.templates_dir, "switches.template") + + self.main_source = os.path.join(self.source_dir, "gnome-shell.scss") + self.colors_source = os.path.join(self.source_dir, "gnome-shell-sass", "_colors.scss") + self.palette_source = os.path.join(self.source_dir, "gnome-shell-sass", "_palette.scss") + self.switches_source = os.path.join(self.source_dir, "gnome-shell-sass", "widgets", "_switches.scss") + + self.assets_output = os.path.join(self.output_dir, "assets") + + def get_cancellable(self) -> Gio.Cancellable: + return self._cancellable + + def apply_theme_async(self, caller:GObject.Object, callback:callable, + theme_variant:str, + preset: Preset): + task = Gio.Task.new(caller, None, callback, self._cancellable) + self.async_data = [theme_variant, preset] + + task.set_return_on_cancel(True) + task.run_in_thread(self._apply_theme_thread) + + def _apply_theme_thread(self, task:Gio.Task, source_object:GObject.Object, + task_data:object, + cancellable:Gio.Cancellable): + if task.return_error_if_cancelled(): + return + + theme_variant = self.async_data[0] + preset = self.async_data[1] + + output = self.apply_theme(source_object, theme_variant, preset) + task.return_value(output) + + # TODO: Make it accept either dict or callable in `parent` parameter + def apply_theme(self, parent: callable, theme_variant: str, preset: Preset): + if theme_variant in ("light", "dark"): + self.theme_variant = theme_variant + else: + raise ValueError( + f"Theme variant {theme_variant} not in list: [light, dark]") + + try: + self._create_theme(parent, preset) + except (OSError, GLib.GError) as e: + raise + + def _create_theme(self, parent: callable, preset: Preset): + # Convert GTK color variables to normal color values + self.preset_variables = color_vars_to_color_code(preset.variables, preset.palette) + self.preset_palette = preset.palette + self.custom_css = preset.custom_css + + # TODO: Move custom Shell colors list to Shell modules + self.shell_colors = parent.shell_colors if parent != None else None + + self._insert_variables() + self._recolor_assets() + + if not os.path.exists(self.output_dir): + try: + dirs = Gio.File.new_for_path(self.output_dir) + dirs.make_directory_with_parents(None) + except GLib.GError as e: + logging.error(f"Unable to create directories.", exc=e) + raise + + self._compile_sass(os.path.join(self.source_dir, "gnome-shell.scss"), + os.path.join(self.output_dir, "gnome-shell.css")) + + self._set_shell_theme() + + def _insert_variables(self): + # hexcode_regex = re.compile(r".*#[0-9a-f]{3,6}") + template_regex = re.compile(r"{{(.*?)}}") + + palette_content = "" + + with open(self.palette_template, "r", encoding="utf-8") as template: + for line in template: + template_match = re.search(template_regex, line) + if template_match != None: + _key = template_match.__getitem__(1) + prefix = _key.split("_")[0] + "_" + key = _key.split("_")[1] + inserted = line.replace("{{" + _key + "}}", self.preset_palette[prefix][key]) + palette_content += inserted + else: + palette_content += line + template.close() + + with open(self.palette_source, "w", encoding="utf-8") as sheet: + sheet.write(palette_content) + sheet.close() + + colors_content = "" + + with open(self.colors_template, "r", encoding="utf-8") as template: + for line in template: + template_match = re.search(template_regex, line) + if template_match != None: + key = template_match.__getitem__(1) + shell_colors = get_shell_colors(self.preset_variables) + try: + if self.shell_colors: + inserted = line.replace( + "{{" + key + "}}", self.shell_colors[key]) + else: + inserted = line.replace( + "{{" + key + "}}", shell_colors[key]) + except KeyError: + inserted = line.replace( + "{{" + key + "}}", self.preset_variables[key]) + colors_content += inserted + else: + colors_content += line + template.close() + + with open(self.colors_source, "w", encoding="utf-8") as sheet: + sheet.write(colors_content) + sheet.close() + + main_content = "" + + with open(self.main_template, "r", encoding="utf-8") as template: + key = "theme_variant" + + for line in template: + if key in line: + inserted = line.replace( + "{{" + key + "}}", f"'{self.theme_variant}'") + main_content += inserted + elif "custom_css" in line: + key = "custom_css" + try: + inserted = line.replace( + "{{" + key + "}}", self.custom_css['shell']) + except KeyError: # No custom CSS + inserted = line.replace("{{" + key + "}}", "") + main_content += inserted + else: + main_content += line + template.close() + + with open(self.main_source, "w", encoding="utf-8") as sheet: + sheet.write(main_content) + sheet.close() + + def _compile_sass(self, sass_path, output_path): + try: + compiled = sass.compile(filename=sass_path, output_style="nested") + except (GLib.GError, sass.CompileError) as e: + logging.error( + f"Failed to compile SCSS source files.", exc=e) + else: + with open(output_path, "w", encoding="utf-8") as css_file: + css_file.write(compiled) + css_file.close() + + # TODO: Add recoloring for other assets + def _recolor_assets(self): + accent_bg = self.preset_variables["accent_bg_color"] + + switch_on_source = os.path.join(self.source_dir, "toggle-on.svg") + + shutil.copy( + self.switches_template, + self.switches_source + ) + + with open(switch_on_source, "r", encoding="utf-8") as svg_data: + switch_on_svg = svg_data.read() + switch_on_svg = switch_on_svg.replace( + "fill:#3584e4", f"fill:{accent_bg}") + svg_data.close() + + with open(switch_on_source, "w", encoding="utf-8") as svg_data: + svg_data.write(switch_on_svg) + svg_data.close() + + if not os.path.exists(self.assets_output): + try: + dirs = Gio.File.new_for_path(self.assets_output) + dirs.make_directory_with_parents(None) + except GLib.GError as e: + logging.error(f"Unable to create directories.", exc=e) + raise + + shutil.copy( + switch_on_source, + os.path.join(self.assets_output, "toggle-on.svg") + ) + + def _set_shell_theme(self): + key = self.THEME_GSETTINGS_SCHEMA_KEY + + # Set default theme + self.settings.reset(key) + + if is_sandboxed(): + # Set theme generated by Gradience + self.settings.set(key, "gradience-shell") + else: + # Set theme generated by Gradience + self.settings.set_string(key, "gradience-shell") + + def _detect_shell_version(self): + shell_ver = get_shell_version() + + if shell_ver.startswith("3"): + raise UnsupportedShellVersion( + f"GNOME Shell version {shell_ver} is not supported. (Supported versions: {', '.join(self.shell_versions_str)})") + + if shell_ver.startswith("4"): + shell_ver = int(shell_ver[:2]) + + if shell_ver in self.shell_versions: + self.version_target = shell_ver + else: + raise UnsupportedShellVersion( + f"GNOME Shell version {shell_ver} is not supported. (Supported versions: {', '.join(self.shell_versions_str)})") + + def reset_theme_async(self, caller:GObject.Object, callback:callable): + task = Gio.Task.new(caller, None, callback, self._cancellable) + + task.set_return_on_cancel(True) + task.run_in_thread(self._reset_theme_thread) + + def reset_theme(self): + key = self.THEME_GSETTINGS_SCHEMA_KEY + + # Set default theme + self.settings.reset(key) + + def _reset_theme_thread(self, task:Gio.Task, source_object:GObject.Object, + task_data:object, cancellable:Gio.Cancellable): + if task.return_error_if_cancelled(): + return + + output = self.reset_theme() + task.return_value(output) diff --git a/gradience/backend/utils/colors.py b/gradience/backend/utils/colors.py index c0b2d828..bf2a764c 100644 --- a/gradience/backend/utils/colors.py +++ b/gradience/backend/utils/colors.py @@ -1,7 +1,7 @@ # colors.py # # Change the look of Adwaita, with ease -# Copyright (C) 2022 Gradience Team +# Copyright (C) 2022-2023, Gradience Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,17 +18,12 @@ import material_color_utilities_python as monet +from gradience.backend.globals import adw_variables_prefixes, adw_palette_prefixes -def rgba_from_argb(argb, alpha=None) -> str: - base = "rgba({}, {}, {}, {})" +from gradience.backend.logger import Logger - red = monet.redFromArgb(argb) - green = monet.greenFromArgb(argb) - blue = monet.blueFromArgb(argb) - if not alpha: - alpha = monet.alphaFromArgb(argb) +logging = Logger(logger_name="ColorUtils") - return base.format(red, green, blue, alpha) def rgb_to_hash(rgb) -> [str, float]: """ @@ -82,3 +77,47 @@ def argb_to_color_code(argb, alpha=None) -> str: return monet.hexFromArgb(argb) return rgba_base.format(red, green, blue, alpha) + +def color_vars_to_color_code(variables: dict, palette: dict) -> dict: + """ + This function converts GTK color variables to color code + (hexadecimal code if no transparency channel, RGBA format if otherwise). + + You can bypass passing a `palette` parameter if you put an None value to it. + This isn't recommended however, because in most cases you'll be unable to determine + if variables you pass don't contain any palette color variables. + """ + + output = variables + + if palette == None: + logging.warning("Palette parameter in `color_vars_to_color_code()` function not set. Incoming bugs ahead!") + + def __has_palette_prefix(color): + return any(prefix in color for prefix in adw_palette_prefixes) + + def __has_variable_prefix(color): + return any(prefix in color for prefix in adw_variables_prefixes) + + def __update_vars(var_type, variable, color_value): + if var_type == "palette": + output[variable] = palette[color_value[:-1]][color_value[-1:]] + elif var_type == "variable": + output[variable] = output[color_value] + + if __has_variable_prefix(output[variable]): + __update_variable_vars(variable, output[variable]) + + if __has_palette_prefix(output[variable]): + __update_palette_vars(variable, output[variable]) + + for variable, color in output.items(): + color_value = color[1:] # Remove '@' from the beginning of the color variable + + if __has_palette_prefix(color_value) and palette != None: + __update_vars("palette", variable, color_value) + + if __has_variable_prefix(color_value): + __update_vars("variable", variable, color_value) + + return output diff --git a/gradience/backend/utils/common.py b/gradience/backend/utils/common.py index 5e71efb2..9b32e8b8 100644 --- a/gradience/backend/utils/common.py +++ b/gradience/backend/utils/common.py @@ -1,7 +1,7 @@ # common.py # # Change the look of Adwaita, with ease -# Copyright (C) 2022 Gradience Team +# Copyright (C) 2022-2023, Gradience Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,18 +18,25 @@ import re import os -import subprocess from anyascii import anyascii +from gi.repository import Gio + def to_slug_case(non_slug) -> str: return re.sub(r"[^0-9a-z]+", "-", anyascii(non_slug).lower()).strip("-") -def run_command(command, *args, **kwargs): - if isinstance(command, str): # run on the host - command = [command] - if os.environ.get('FLATPAK_ID'): # run in flatpak - command = ['flatpak-spawn', '--host'] + command +def extract_version(text, prefix_text=None): + ''' + Extracts version number from a provided text. - return subprocess.run(command, *args, **kwargs, check=True) + You can also set the prefix_text parameter to reduce searching to + lines with only this text prefixed to the version number. + ''' + if not prefix_text: + version = re.search(r"\s*([0-9.]+)", text) + else: + version = re.search(prefix_text + r"\s*([0-9.]+)", text) + + return version.__getitem__(1) diff --git a/gradience/backend/utils/gnome.py b/gradience/backend/utils/gnome.py new file mode 100644 index 00000000..5b90e2d4 --- /dev/null +++ b/gradience/backend/utils/gnome.py @@ -0,0 +1,99 @@ +# shell.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os + +from gradience.backend.models.preset import Preset +from gradience.backend.utils.subprocess import GradienceSubprocess +from gradience.backend.utils.common import extract_version + +# TODO: Remove this import later (imports from gradience.frontend are not allowed in backend) +from gradience.frontend.schemas.shell_schema import shell_schema + + +# TODO: Return failure if command was not found +def get_shell_version() -> str: + cmd_list = ["gnome-shell", "--version"] + process = GradienceSubprocess() + + completed = process.run(cmd_list, allow_escaping=True) + stdout = process.get_stdout_data(completed, decode=True) + + shell_version = extract_version(stdout, "GNOME Shell") + + return shell_version + +def get_full_shell_version() -> str: + cmd_list = ["gnome-shell", "--version"] + process = GradienceSubprocess() + + completed = process.run(cmd_list, allow_escaping=True) + stdout = process.get_stdout_data(completed, decode=True) + + shell_version = stdout[12:] + + return shell_version + +def is_gnome_available() -> bool: + xdg_current_desktop = os.environ.get("XDG_CURRENT_DESKTOP").lower() + + if "gnome" in xdg_current_desktop: + return True + + return False + +def is_shell_ext_installed(uuid: str, check_enabled: bool = False) -> bool: + """ + Checks if Shell extension with provided UUID from `uuid` parameter + is installed in system. + + `check_enabled` parameter allows for checking if extension is enabled. + """ + + if check_enabled: + cmd_list = ["gnome-extensions", "list", "--enabled"] + else: + cmd_list = ["gnome-extensions", "list"] + + process = GradienceSubprocess() + + completed = process.run(cmd_list, allow_escaping=True) + stdout = process.get_stdout_data(completed, decode=True) + + ext_list = stdout.split("\n") + if ext_list[-1] == "": + ext_list.pop(-1) + + if uuid in ext_list: + return True + + return False + +def get_shell_colors(preset_variables: Preset.variables) -> dict: + shell_colors = {} + + for variable in shell_schema["variables"]: + shell_colors[variable["name"]] = variable["var_name"] + + for shell_key, var_name in shell_colors.items(): + if shell_key == "panel_bg_color": + shell_colors[shell_key] = shell_schema["variables"][5]["default_value"] + continue + shell_colors[shell_key] = preset_variables[var_name] + + return shell_colors diff --git a/gradience/backend/utils/gsettings.py b/gradience/backend/utils/gsettings.py new file mode 100644 index 00000000..744f5c4b --- /dev/null +++ b/gradience/backend/utils/gsettings.py @@ -0,0 +1,273 @@ +# gsettings.py +# +# Change the look of Adwaita, with ease +# Copyright (c) 2011, John Stowers +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# NOTICE: +# This code is from the GNOME Tweaks application, which is licensed under the GPL-3.0 license. +# https://gitlab.gnome.org/GNOME/gnome-tweaks/-/blob/master/gtweak/gsettings.py + +import os +import re +import shutil +import os.path +import xml.dom.minidom +import gettext + +from subprocess import SubprocessError, CompletedProcess + +from gi.repository import Gio, GLib + +from gradience.backend.utils.subprocess import GradienceSubprocess +from gradience.backend.constants import localedir, app_id + +from gradience.backend.logger import Logger + +logging = Logger(logger_name="GSettings") + + +_SCHEMA_CACHE = {} +_GSETTINGS_SCHEMAS = set(Gio.Settings.list_schemas()) +_GSETTINGS_RELOCATABLE_SCHEMAS = set(Gio.Settings.list_relocatable_schemas()) + + +class GSettingsMissingError(Exception): + pass + + +class _GSettingsSchema: + def __init__(self, schema_name, schema_dir=None, schema_filename=None, **options): + if not schema_filename: + schema_filename = schema_name + ".gschema.xml" + if not schema_dir: + schema_dir = app_id + for xdg_dir in GLib.get_system_data_dirs(): + dir = os.path.join(xdg_dir, "glib-2.0", "schemas") + if os.path.exists(os.path.join(dir, schema_filename)): + schema_dir = dir + break + + schema_path = os.path.join(schema_dir, schema_filename) + if not os.path.exists(schema_path): + logging.critical("Could not find schema %s" % schema_path) + assert (False) + + self._schema_name = schema_name + self._schema = {} + + try: + dom = xml.dom.minidom.parse(schema_path) + global_gettext_domain = dom.documentElement.getAttribute( + 'gettext-domain') + try: + if global_gettext_domain: + # We can't know where the schema owner was installed, let's assume it's + # the same prefix as ours + global_translation = gettext.translation( + global_gettext_domain, localedir) + else: + global_translation = gettext.NullTranslations() + except IOError: + global_translation = None + logging.debug("No translated schema for %s (domain: %s)" % ( + schema_name, global_gettext_domain)) + for schema in dom.getElementsByTagName("schema"): + gettext_domain = schema.getAttribute('gettext-domain') + try: + if gettext_domain: + translation = gettext.translation( + gettext_domain, localedir) + else: + translation = global_translation + except IOError: + translation = None + logging.debug("Schema not translated %s (domain: %s)" % ( + schema_name, gettext_domain)) + if schema_name == schema.getAttribute("id"): + for key in schema.getElementsByTagName("key"): + name = key.getAttribute("name") + # summary is 'compulsory', description is optional + # …in theory, but we should not barf on bad schemas ever + try: + summary = key.getElementsByTagName( + "summary")[0].childNodes[0].data + except: + summary = "" + logging.info("Schema missing summary %s (key %s)" % + (os.path.basename(schema_path), name)) + try: + description = key.getElementsByTagName( + "description")[0].childNodes[0].data + except: + description = "" + + # if missing translations, use the untranslated values + self._schema[name] = dict( + summary=translation.gettext( + summary) if translation else summary, + description=translation.gettext( + description) if translation else description + ) + + except: + logging.critical("Error parsing schema %s (%s)" % + (schema_name, schema_path), exc_info=True) + + def __repr__(self): + return "" % self._schema_name + + +class GSettingsSetting(Gio.Settings): + def __init__(self, schema_name, schema_dir=None, schema_path=None, **options): + + if schema_dir is None: + if schema_path is None and schema_name not in _GSETTINGS_SCHEMAS: + raise GSettingsMissingError(schema_name) + + if schema_path is not None and schema_name not in _GSETTINGS_RELOCATABLE_SCHEMAS: + raise GSettingsMissingError(schema_name) + + if schema_path is None: + Gio.Settings.__init__(self, schema=schema_name) + else: + Gio.Settings.__init__( + self, schema=schema_name, path=schema_path) + else: + GioSSS = Gio.SettingsSchemaSource + schema_source = GioSSS.new_from_directory(schema_dir, + GioSSS.get_default(), + False) + schema_obj = schema_source.lookup(schema_name, True) + if not schema_obj: + raise GSettingsMissingError(schema_name) + + Gio.Settings.__init__(self, None, settings_schema=schema_obj) + + if schema_name not in _SCHEMA_CACHE: + _SCHEMA_CACHE[schema_name] = _GSettingsSchema( + schema_name, schema_dir=schema_dir, **options) + logging.debug("Caching gsettings: %s" % _SCHEMA_CACHE[schema_name]) + + self._schema = _SCHEMA_CACHE[schema_name] + + def _on_changed(self, settings, key_name): + logging.debug("Change: %s %s -> %s" % + (self.props.schema, key_name, self[key_name])) + + def _setting_check_is_list(self, key): + variant = Gio.Settings.get_value(self, key) + return variant.get_type_string() == "as" + + def schema_get_summary(self, key): + return self._schema._schema[key]["summary"] + + def schema_get_description(self, key): + return self._schema._schema[key]["description"] + + def schema_get_all(self, key): + return self._schema._schema[key] + + def setting_add_to_list(self, key, value): + """ helper function, ensures value is present in the GSettingsList at key """ + assert self._setting_check_is_list(key) + + vals = self[key] + if value not in vals: + vals.append(value) + self[key] = vals + return True + + def setting_remove_from_list(self, key, value): + """ helper function, removes value in the GSettingsList at key (if present)""" + assert self._setting_check_is_list(key) + + vals = self[key] + try: + vals.remove(value) + self[key] = vals + return True + except ValueError: + # not present + pass + + def setting_is_in_list(self, key, value): + assert self._setting_check_is_list(key) + return value in self[key] + + +class FlatpakGSettings: + def __init__(self, schema_name, schema_dir=None, **options): + self.schema_name = schema_name + self.schema_dir = schema_dir + + def list_keys(self) -> str: + dconf_cmd = ["gsettings", "list-keys", self.schema_name] + process = GradienceSubprocess() + + if self.schema_dir: + self._insert_schemadir(dconf_cmd) + + try: + completed = process.run(dconf_cmd, allow_escaping=True) + stdout = process.get_stdout_data(completed, decode=True) + except SubprocessError: + raise + else: + return stdout + + def get(self, key:str) -> CompletedProcess: + dconf_cmd = ["gsettings", "get", self.schema_name, key] + process = GradienceSubprocess() + + if self.schema_dir: + self._insert_schemadir(dconf_cmd) + + try: + completed = process.run(dconf_cmd, allow_escaping=True) + stdout = process.get_stdout_data(completed, decode=True) + except SubprocessError: + raise + else: + return stdout + + def set(self, key:str, value:str) -> None: + dconf_cmd = ["gsettings", "set", self.schema_name, key, value] + process = GradienceSubprocess() + + if self.schema_dir: + self._insert_schemadir(dconf_cmd) + + try: + process.run(dconf_cmd, allow_escaping=True) + except SubprocessError: + raise + + def reset(self, key:str = None) -> None: + dconf_cmd = ["gsettings", "reset", self.schema_name, key] + process = GradienceSubprocess() + + if self.schema_dir: + self._insert_schemadir(dconf_cmd) + + try: + process.run(dconf_cmd, allow_escaping=True) + except SubprocessError: + raise + + def _insert_schemadir(self, dconf_cmd): + dconf_cmd.insert(1, "--schemadir") + dconf_cmd.insert(2, self.schema_dir) diff --git a/gradience/backend/utils/meson.build b/gradience/backend/utils/meson.build index 30855aa5..f30db5c3 100644 --- a/gradience/backend/utils/meson.build +++ b/gradience/backend/utils/meson.build @@ -3,6 +3,10 @@ utilsdir = 'gradience/backend/utils' gradience_sources = [ '__init__.py', 'colors.py', - 'common.py' + 'common.py', + 'gnome.py', + 'gsettings.py', + 'subprocess.py', + 'theming.py' ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: utilsdir) diff --git a/gradience/backend/utils/shell.py b/gradience/backend/utils/shell.py new file mode 100644 index 00000000..902a2b03 --- /dev/null +++ b/gradience/backend/utils/shell.py @@ -0,0 +1,58 @@ +# shell.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gi.repository import Gio + +from gradience.backend.models.preset import Preset +from gradience.backend.utils.common import extract_version, run_command + +# TODO: Remove this import later (imports from gradience.frontend are not allowed in backend) +from gradience.frontend.schemas.shell_schema import shell_schema + + +def get_shell_version(): + stdout = run_command(["gnome-shell", "--version"], + get_stdout_text=True, + allow_escaping=True).replace("\n", "") + + shell_version = extract_version(stdout, "GNOME Shell") + + return shell_version + +def get_full_shell_version(): + stdout = run_command(["gnome-shell", "--version"], + get_stdout_text=True, + allow_escaping=True).replace("\n", "") + + shell_version = stdout[12:] + + return shell_version + +def get_shell_colors(preset_variables: Preset.variables): + shell_colors = {} + + for variable in shell_schema["variables"]: + shell_colors[variable["name"]] = variable["var_name"] + + for shell_key, var_name in shell_colors.items(): + if shell_key == "panel_bg_color": + shell_colors[shell_key] = shell_schema["variables"][5]["default_value"] + continue + shell_colors[shell_key] = preset_variables[var_name] + + return shell_colors diff --git a/gradience/backend/utils/subprocess.py b/gradience/backend/utils/subprocess.py new file mode 100644 index 00000000..f94e4188 --- /dev/null +++ b/gradience/backend/utils/subprocess.py @@ -0,0 +1,94 @@ +# subprocess.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022-2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +#import signal + +from typing import Union + +import subprocess +from subprocess import SubprocessError, CompletedProcess + +from gradience.backend.logger import Logger + +logging = Logger(logger_name="GradienceSubprocess") + + +# TODO: Check how Dev Toolbox has its backend done fully in async using Gio.Task. +# Example: https://github.com/aleiepure/devtoolbox/blob/main/src/services/gzip_compressor.py +# TODO: Replace subprocess.run() with subprocess.Popen() for more control over subprocesses +class GradienceSubprocess: + """ + Wrapper for Python's `subprocess` module to provide an easy to use + synchronous process spawning and stdout data retrievement with support + for Flatpak sandbox escape. + + Documentation: https://docs.python.org/3/library/subprocess.html + """ + + def __init__(self): + pass + + def run(self, command: list, timeout: int = None, allow_escaping: bool = False) -> CompletedProcess: + """ + Spawns synchronously a new child process (subprocess) using Python's `subprocess` module. + + You can set the `timeout` parameter to kill the process after a + specified amount of seconds. + + You can enable executing commands outside Flatpak sandbox by + enabling `allow_escaping` parameter. + """ + + if allow_escaping and os.environ.get('FLATPAK_ID'): + command = ['flatpak-spawn', '--host'] + command + + logging.debug(f"Spawning: {command}") + + try: + process = subprocess.run(command, check=True, + capture_output=True, timeout=timeout) + except SubprocessError: + raise + except FileNotFoundError: + raise + + return process + + def get_stdout_data(self, process: CompletedProcess, decode: bool = False) -> Union[str, bytes]: + """ + Returns a data retrieved from stdout stream. + + Default behavior returns a full data collection in bytes array. + Setting ``decode`` parameter to True will automatically decode data to string object. + """ + + if decode: + stdout_string = process.stdout.decode() + return stdout_string + + return process.stdout + + '''def stop(self, process: CompletedProcess) -> None: + logging.debug(f"Terminating process, ID {process.get_identifier()}") + process.send_signal(signal.SIGTERM) + + def kill(self, process: CompletedProcess) -> None: + logging.debug(f"Killing process, ID {process.get_identifier()}") + self.cancel_read() + process.send_signal(signal.SIGKILL)''' diff --git a/gradience/backend/utils/theming.py b/gradience/backend/utils/theming.py new file mode 100644 index 00000000..0d29db5c --- /dev/null +++ b/gradience/backend/utils/theming.py @@ -0,0 +1,50 @@ +# theming.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gradience.backend.models.preset import Preset + + +def generate_gtk_css(app_type: str, preset: Preset) -> str: + variables = preset.variables + palette = preset.palette + custom_css = preset.custom_css + + theming_warning = """/* +Generated with Gradience + +Issues caused by theming should be reported to Gradience repository, and not upstream + +https://github.com/GradienceTeam/Gradience +*/ + +""" + + gtk_css = "" + + for key in variables.keys(): + gtk_css += f"@define-color {key} {variables[key]};\n" + + for prefix_key in palette.keys(): + for key in palette[prefix_key].keys(): + gtk_css += f"@define-color {prefix_key + key} {palette[prefix_key][key]};\n" + + gtk_css += custom_css.get(app_type, "") + + final_css = theming_warning + gtk_css + + return final_css diff --git a/gradience/frontend/cli/cli.in b/gradience/frontend/cli/cli.in index d0639e6c..75087291 100755 --- a/gradience/frontend/cli/cli.in +++ b/gradience/frontend/cli/cli.in @@ -25,9 +25,12 @@ import shutil import signal import argparse import warnings +import locale +import gettext version = "@VERSION@" is_local = @local_build@ +localedir = '@LOCALE_DIR@' if is_local: # In the local use case, use gradience module from the sourcetree @@ -37,19 +40,26 @@ if is_local: os.environ["XDG_DATA_DIRS"] = '@SCHEMAS_DIR@:' + os.environ.get("XDG_DATA_DIRS", "") signal.signal(signal.SIGINT, signal.SIG_DFL) +gettext.install('gradience', localedir) + +locale.bindtextdomain('gradience', localedir) +locale.textdomain('gradience') warnings.filterwarnings("ignore") # suppress GTK warnings from gi.repository import GLib, Gio +from gradience.backend.utils.gnome import is_gnome_available, is_shell_ext_installed from gradience.backend.utils.common import to_slug_case from gradience.backend.globals import preset_repos, presets_dir from gradience.backend.theming.monet import Monet from gradience.backend.models.preset import Preset -from gradience.backend.theming.preset_utils import PresetUtils +from gradience.backend.theming.shell import ShellTheme +from gradience.backend.theming.preset import PresetUtils from gradience.backend.preset_downloader import PresetDownloader -from gradience.backend.flatpak_overrides import list_file_access, allow_file_access, disallow_file_access, create_gtk_user_override, remove_gtk_user_override +from gradience.backend.flatpak_overrides import (list_file_access, allow_file_access, + disallow_file_access, create_gtk_user_override, remove_gtk_user_override) from gradience.backend.logger import Logger @@ -82,13 +92,13 @@ class CLI: apply_parser = subparsers.add_parser("apply", help="apply a preset") apply_group = apply_parser.add_mutually_exclusive_group(required=True) - apply_group.add_argument("-n", "--preset-name", help="display name for a preset") - apply_group.add_argument("-p", "--preset-path", help="absolute path to a preset file") + apply_group.add_argument("-n", "--preset-name", help="display name of the preset") + apply_group.add_argument("-p", "--preset-path", help="absolute path to the preset file") apply_parser.add_argument("--gtk", choices=["gtk4", "gtk3", "both"], default="gtk4", help="types of applications you want to theme (default: gtk4)") #new_parser = subparsers.add_parser("new", help="create a new preset") #new_parser.add_argument("-i", "--interactive", action="store_true", help="") - #new_parser.add_argument("-n", "--name", help="display name for a preset", required=True) + #new_parser.add_argument("-n", "--name", help="display name of the preset", required=True) #new_parser.add_argument("--colors", help="", required=True) #new_parser.add_argument("--palette", help="") #new_parser.add_argument("--custom-css", help="") @@ -99,6 +109,12 @@ class CLI: download_parser.add_argument("-n", "--preset-name", help="name of a preset you want to get", required=True) #download_parser.add_argument("--custom-url", help="use custom repository's presets.json to download other presets") + shell_parser = subparsers.add_parser("gnome-shell", help="generate a GNOME Shell theme from any preset") + choose_preset_group = shell_parser.add_mutually_exclusive_group(required=True) + choose_preset_group.add_argument("-n", "--preset-name", help="display name of the preset") + choose_preset_group.add_argument("-p", "--preset-path", help="absolute path to the preset file") + shell_parser.add_argument("-v", "--preset-variant", choices=["light", "dark"], help="select which preset variant you use to generate a theme") + monet_parser = subparsers.add_parser("monet", help="generate Material You preset from an image") #monet_parser.add_argument("-a", "--apply", help="apply Monet's generated preset after it has been created", action='store_true') monet_parser.add_argument("-n", "--preset-name", help="name for a generated preset", required=True) @@ -150,6 +166,9 @@ class CLI: elif args.command == "download": self.download_preset(args) + elif args.command == "gnome-shell": + self.gnome_shell(args) + elif args.command == "monet": self.generate_monet(args) @@ -339,6 +358,85 @@ class CLI: continue repo_no += 1 + # TODO: Add support for custom colors + def gnome_shell(self, args): + _preset_name = args.preset_name + _preset_path = args.preset_path + _preset_variant = args.preset_variant + + try: + presets_list = PresetUtils().get_presets_list(full_list=True) + except (OSError, KeyError, AttributeError) as e: + logging.error("Failed to retrieve a list of presets.", exc=e) + exit(1) + + presets_name = list(presets_list.values()) + + def __get_preset_from_name(): + for path, name in presets_list.items(): + if name == _preset_name: + preset = Preset().new_from_path(path) + return preset + + if _preset_name: + if _preset_name in presets_name: + preset = __get_preset_from_name() + else: + logging.error(f"Failed to find preset named {_preset_name}. Verify if you wrote the name right with `presets` command.") + exit(1) + elif _preset_path: + try: + preset = Preset().new_from_path(_preset_path) + except OSError as e: + exit(1) + + if not is_gnome_available(): + logging.warning("Shell Engine is designed to work only on systems running GNOME. You can still generate themes on other desktop environments, but it won't have any affect on them.") + prompt = input("Do you want to continue? [N/y] ") + + if prompt.lower() == "y": + pass + elif prompt.lower() == "n" or prompt == "": + logging.info("Aborting all operations...") + exit(0) + else: + logging.info("Aborting all operations...") + exit(0) + + shell_engine = ShellTheme() + + is_user_themes_available = is_shell_ext_installed(shell_engine.THEME_EXT_NAME) + is_user_themes_enabled = is_shell_ext_installed(shell_engine.THEME_EXT_NAME, check_enabled=True) + + if not is_user_themes_available: + logging.warning("Gradience requires User Themes extension installed in order to apply Shell theme. You can still generate a theme, but you won't be able to apply it without this extension.") + prompt = input("Do you want to continue? [N/y] ") + + if prompt.lower() == "y": + pass + elif prompt.lower() == "n" or prompt == "": + logging.info("Aborting all operations...") + exit(0) + else: + logging.info("Aborting all operations...") + exit(0) + elif not is_user_themes_enabled: + logging.warning("User Themes extension is currently disabled on your system. Please enable it in order to apply theme.") + prompt = input("Do you want to continue? [N/y] ") + + if prompt.lower() == "y": + pass + elif prompt.lower() == "n" or prompt == "": + logging.info("Aborting all operations...") + exit(0) + else: + logging.info("Aborting all operations...") + exit(0) + + shell_engine.apply_theme(None, _preset_variant, preset) + logging.info("GNOME Shell theme generated successfully.") + exit(0) + # NOTE: Possible useful portals to use in future: org.freedesktop.portal.Documents \ # (support missing in libportal, only D-Bus calls), org.freedesktop.portal.FileChooser def generate_monet(self, args): @@ -350,7 +448,7 @@ class CLI: _json = args.json try: - palette = Monet().generate_from_image(_image_path) + palette = Monet().generate_palette_from_image(_image_path) except (OSError, ValueError) as e: logging.info("If you are getting an `no such file or directory` error on Gradience installed as Flatpak, " "try adding the file to the access list by using `gradience-cli access-file --allow 'path/to/file'` command.") @@ -360,8 +458,8 @@ class CLI: if _json: try: - preset = PresetUtils().new_preset_from_monet(_preset_name, - palette, props, True) + preset = Monet().new_preset_from_monet(_preset_name, + palette, props, True) except (OSError, AttributeError) as e: logging.error("Unexpected error while generating preset from Monet palette.", exc=e) exit(1) @@ -371,8 +469,7 @@ class CLI: exit(0) try: - PresetUtils().new_preset_from_monet(_preset_name, palette, props) - #raise OSError() + Monet().new_preset_from_monet(_preset_name, palette, props) except (OSError, AttributeError) as e: logging.error("Unexpected error while generating preset from Monet palette.", exc=e) exit(1) diff --git a/gradience/frontend/dialogs/app_type_dialog.py b/gradience/frontend/dialogs/app_type_dialog.py index bb2cb3cf..788d5258 100644 --- a/gradience/frontend/dialogs/app_type_dialog.py +++ b/gradience/frontend/dialogs/app_type_dialog.py @@ -28,22 +28,18 @@ class GradienceAppTypeDialog(Adw.MessageDialog): gtk4_app_type = Gtk.Template.Child("gtk4-app-type") gtk3_app_type = Gtk.Template.Child("gtk3-app-type") - def __init__( - self, - parent, - heading, - body, - ok_res_name, - ok_res_label, - ok_res_appearance, - **kwargs - ): + def __init__(self, parent, heading, body, ok_res_name, ok_res_label, ok_res_appearance, **kwargs): super().__init__(**kwargs) self.parent = parent self.app = self.parent.get_application() - self.set_transient_for(self.app.get_active_window()) + if isinstance(self.parent, Gtk.Window): + self.win = self.parent + else: + self.win = self.app.get_active_window() + + self.set_transient_for(self.win) self.set_heading(heading) self.set_body(body) diff --git a/gradience/frontend/dialogs/log_out_dialog.py b/gradience/frontend/dialogs/log_out_dialog.py index 95634919..944521e7 100644 --- a/gradience/frontend/dialogs/log_out_dialog.py +++ b/gradience/frontend/dialogs/log_out_dialog.py @@ -31,7 +31,12 @@ class GradienceLogOutDialog(Adw.MessageDialog): self.parent = parent self.app = self.parent.get_application() - self.set_transient_for(self.app.get_active_window()) + if isinstance(self.parent, Gtk.Window): + self.win = self.parent + else: + self.win = self.app.get_active_window() + + self.set_transient_for(self.win) self.add_response("ok", _("OK")) self.set_default_response("ok") diff --git a/gradience/frontend/dialogs/meson.build b/gradience/frontend/dialogs/meson.build index 46984ba2..22b6a422 100644 --- a/gradience/frontend/dialogs/meson.build +++ b/gradience/frontend/dialogs/meson.build @@ -4,6 +4,7 @@ gradience_sources = [ '__init__.py', 'app_type_dialog.py', 'log_out_dialog.py', - 'save_dialog.py' + 'save_dialog.py', + 'unsupported_shell_dialog.py' ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: dialogsdir) diff --git a/gradience/frontend/dialogs/save_dialog.py b/gradience/frontend/dialogs/save_dialog.py index 21052d16..1fd3e143 100644 --- a/gradience/frontend/dialogs/save_dialog.py +++ b/gradience/frontend/dialogs/save_dialog.py @@ -37,10 +37,15 @@ class GradienceSaveDialog(Adw.MessageDialog): self.body = _( "Saving preset to {0}. If that preset already " - "exists, it will be overwritten!" + "exists, it will be overwritten." ) - self.set_transient_for(self.app.get_active_window()) + if isinstance(self.parent, Gtk.Window): + self.win = self.parent + else: + self.win = self.app.get_active_window() + + self.set_transient_for(self.win) if heading: self.heading = heading diff --git a/gradience/frontend/dialogs/unsupported_shell_dialog.py b/gradience/frontend/dialogs/unsupported_shell_dialog.py new file mode 100644 index 00000000..46fbd758 --- /dev/null +++ b/gradience/frontend/dialogs/unsupported_shell_dialog.py @@ -0,0 +1,45 @@ +# unsupported_shell_version.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2022-2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gi.repository import Gtk, Adw + +from gradience.backend.constants import rootdir +from gradience.backend.utils.gnome import get_full_shell_version + +class GradienceUnsupportedShellDialog(Adw.MessageDialog): + __gtype_name__ = "GradienceUnsupportedShellDialog" + + def __init__(self, parent, **kwargs): + super().__init__(**kwargs) + + self.parent = parent + self.app = self.parent.get_application() + + if isinstance(self.parent, Gtk.Window): + self.win = self.parent + else: + self.win = self.app.get_active_window() + + self.set_transient_for(self.win) + + self.set_heading(_(f"Unsupported Shell Version ({get_full_shell_version()})")) + self.set_body(_("The Shell version you are using is not supported by Gradience. Please upgrade to a newer version of GNOME.")) + + self.add_response("ok", _("OK")) + self.set_default_response("ok") + self.set_close_response("ok") diff --git a/gradience/frontend/main.py b/gradience/frontend/main.py index 9822b1c2..dc9dba64 100644 --- a/gradience/frontend/main.py +++ b/gradience/frontend/main.py @@ -22,13 +22,15 @@ import threading from pathlib import Path from material_color_utilities_python import hexFromArgb -from gi.repository import Gtk, Gdk, Gio, Adw, GLib +from gi.repository import GObject, Gtk, Gdk, Gio, Adw, GLib from gradience.backend.globals import presets_dir, get_gtk_theme_dir from gradience.backend.css_parser import parse_css from gradience.backend.models.preset import Preset -from gradience.backend.theming.preset_utils import PresetUtils +from gradience.backend.theming.preset import PresetUtils +from gradience.backend.theming.monet import Monet from gradience.backend.utils.common import to_slug_case +from gradience.backend.utils.theming import generate_gtk_css from gradience.backend.constants import rootdir, app_id, rel_ver from gradience.frontend.views.main_window import GradienceMainWindow @@ -99,6 +101,8 @@ class GradienceApplication(Adw.Application): self.win = self.props.active_window + self.setup_signals() + if not self.win: self.win = GradienceMainWindow( application=self, @@ -126,15 +130,9 @@ class GradienceApplication(Adw.Application): self.actions.create_action("apply_color_scheme", self.show_apply_color_scheme_dialog) - self.actions.create_action("restore_color_scheme", - self.show_restore_color_scheme_dialog) - self.actions.create_action("manage_presets", self.show_presets_manager) - self.actions.create_action("reset_color_scheme", - self.show_reset_color_scheme_dialog) - self.actions.create_action("preferences", self.show_preferences) @@ -145,7 +143,6 @@ class GradienceApplication(Adw.Application): self.show_about_window) self.load_preset_from_css() - self.reload_user_defined_presets() if self.first_run: @@ -159,6 +156,16 @@ class GradienceApplication(Adw.Application): logging.debug("normal run") self.win.present() + def setup_signals(self): + # Custom signals + GObject.signal_new( + "preset-reload", + self, + GObject.SignalFlags.RUN_LAST, + bool, + (object,) + ) + def save_favourite(self): self.settings.set_value( "favourite", GLib.Variant("as", self.favourite)) @@ -368,7 +375,7 @@ class GradienceApplication(Adw.Application): preset_variant = "light" try: - preset_object = PresetUtils().new_preset_from_monet(monet_palette=monet, + preset_object = Monet().new_preset_from_monet(monet_palette=monet, props=[tone, preset_variant], obj_only=True) except (OSError, AttributeError) as e: logging.error("An error occurred while generating preset from Monet palette.", exc=e) @@ -410,7 +417,7 @@ class GradienceApplication(Adw.Application): def reload_variables(self): parsing_errors = [] - gtk_css = PresetUtils().generate_gtk_css("gtk4", self.preset) + gtk_css = generate_gtk_css("gtk4", self.preset) css_provider = Gtk.CssProvider() def on_error(_, section, error): @@ -455,6 +462,7 @@ class GradienceApplication(Adw.Application): ) self.current_css_provider = css_provider + self.emit("preset-reload", object()) self.is_ready = True def load_preset_action(self, _unused, *args): @@ -507,33 +515,6 @@ class GradienceApplication(Adw.Application): dialog.connect("response", self.apply_color_scheme) dialog.present() - def show_restore_color_scheme_dialog(self, *_args): - dialog = GradienceAppTypeDialog( - self.win, - _("Restore applied color scheme?"), - _("Make sure you have the current settings saved as a preset."), - "restore", - _("_Restore"), - Adw.ResponseAppearance.DESTRUCTIVE - ) - - dialog.gtk3_app_type.set_sensitive(False) - dialog.connect("response", self.restore_color_scheme) - dialog.present() - - def show_reset_color_scheme_dialog(self, *_args): - dialog = GradienceAppTypeDialog( - self.win, - _("Reset applied color scheme?"), - _("Make sure you have the current settings saved as a preset."), - "reset", - _("_Reset"), - Adw.ResponseAppearance.DESTRUCTIVE - ) - - dialog.connect("response", self.reset_color_scheme) - dialog.present() - def show_save_preset_dialog(self, *_args): dialog = GradienceSaveDialog(self.win, path=os.path.join( presets_dir, @@ -627,40 +608,6 @@ class GradienceApplication(Adw.Application): dialog = GradienceLogOutDialog(self.win) dialog.present() - def restore_color_scheme(self, widget, response): - if response == "restore": - if widget.get_app_types()["gtk4"]: - try: - PresetUtils().restore_gtk4_preset() - except OSError: - self.win.toast_overlay.add_toast( - Adw.Toast(title=_("Unable to restore GTK 4 backup")) - ) - - dialog = GradienceLogOutDialog(self.win) - dialog.present() - - def reset_color_scheme(self, widget, response): - if response == "reset": - if widget.get_app_types()["gtk4"]: - try: - PresetUtils().reset_preset("gtk4") - except GLib.GError: - self.win.toast_overlay.add_toast( - Adw.Toast(title=_("Unable to delete current preset")) - ) - - if widget.get_app_types()["gtk3"]: - try: - PresetUtils().reset_preset("gtk3") - except GLib.GError: - self.win.toast_overlay.add_toast( - Adw.Toast(title=_("Unable to delete current preset")) - ) - - dialog = GradienceLogOutDialog(self.win) - dialog.present() - def show_preferences(self, *_args): prefs = GradiencePreferencesWindow(self.win) prefs.present() diff --git a/gradience/frontend/schemas/meson.build b/gradience/frontend/schemas/meson.build index c36e6c01..9cf4e5b5 100644 --- a/gradience/frontend/schemas/meson.build +++ b/gradience/frontend/schemas/meson.build @@ -2,6 +2,7 @@ schemasdir = 'gradience/frontend/schemas' gradience_sources = [ '__init__.py', - 'preset_schema.py' + 'preset_schema.py', + 'shell_schema.py' ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: schemasdir) diff --git a/gradience/frontend/schemas/preset_schema.py b/gradience/frontend/schemas/preset_schema.py index ab41fb98..cc56002d 100644 --- a/gradience/frontend/schemas/preset_schema.py +++ b/gradience/frontend/schemas/preset_schema.py @@ -299,6 +299,23 @@ preset_schema = { }, ], }, + { + "name": "thumbnail_colors", + "title": _("Thumbnail Colors"), + "description": _("These colors are used for Tab Overview thumbnails."), + "variables": [ + { + "name": "thumbnail_bg_color", + "title": _("Background Color"), + "adw_gtk3_support": "yes", + }, + { + "name": "thumbnail_fg_color", + "title": _("Foreground Color"), + "adw_gtk3_support": "yes", + }, + ], + }, { "name": "dialog_colors", "title": _("Dialog Colors"), @@ -372,5 +389,5 @@ preset_schema = { {"prefix": "light_", "title": _("Light"), "n_shades": 5}, {"prefix": "dark_", "title": _("Dark"), "n_shades": 5}, ], - "custom_css_app_types": ["gtk4", "gtk3"], + "custom_css_app_types": ["gtk4", "gtk3"] } diff --git a/gradience/frontend/schemas/shell_schema.py b/gradience/frontend/schemas/shell_schema.py new file mode 100644 index 00000000..cdbf89f1 --- /dev/null +++ b/gradience/frontend/schemas/shell_schema.py @@ -0,0 +1,64 @@ +# shell_schema.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +shell_schema = { + "variables": [ + { + "name": "bg_color", + "var_name": "window_bg_color", + "title": _("Base Background Color") + }, + { + "name": "fg_color", + "var_name": "window_fg_color", + "title": _("Base Foreground Color") + }, + { + "name": "system_bg_color", + "var_name": "window_bg_color", + "title": _("Overview Background Color") + }, + { + "name": "selected_bg_color", + "var_name": "accent_bg_color", + "title": _("Accent Background Color") + }, + { + "name": "selected_fg_color", + "var_name": "window_fg_color", + "title": _("Accent Foreground Color") + }, + # TODO: Fix panel background color injection code + #{ + # "name": "panel_bg_color", + # "var_name": "panel_bg_color", + # "title": _("Panel Background Color"), + # "default_value": "#000" + #}, + #{ + # "name": "osd_bg_color", + # "var_name": "window_bg_color", + # "title": _("OSD Background Color") + #}, + { + "name": "osd_fg_color", + "var_name": "window_fg_color", + "title": _("OSD Foreground Color") + } + ] +} diff --git a/gradience/frontend/views/main_window.py b/gradience/frontend/views/main_window.py index d9d4f016..340f97ab 100644 --- a/gradience/frontend/views/main_window.py +++ b/gradience/frontend/views/main_window.py @@ -1,7 +1,7 @@ # main_window.py # # Change the look of Adwaita, with ease -# Copyright (C) 2022 Gradience Team +# Copyright (C) 2022-2023, Gradience Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -20,12 +20,14 @@ from enum import Enum from gi.repository import Gtk, Adw, Gio -from gradience.backend.theming.monet import Monet from gradience.backend.constants import rootdir, app_id, build_type -from gradience.frontend.widgets.error_list_row import GradienceErrorListRow +from gradience.frontend.widgets.shell_theming_group import GradienceShellThemingGroup +from gradience.frontend.widgets.monet_theming_group import GradienceMonetThemingGroup from gradience.frontend.widgets.palette_shades import GradiencePaletteShades +from gradience.frontend.widgets.error_list_row import GradienceErrorListRow from gradience.frontend.widgets.option_row import GradienceOptionRow +from gradience.frontend.widgets.theming_empty_group import GradienceEmptyThemingGroup from gradience.frontend.schemas.preset_schema import preset_schema from gradience.backend.logger import Logger @@ -37,17 +39,18 @@ logging = Logger() class GradienceMainWindow(Adw.ApplicationWindow): __gtype_name__ = "GradienceMainWindow" - content = Gtk.Template.Child() + content_colors = Gtk.Template.Child("content-colors") + content_theming = Gtk.Template.Child("content-theming") + content_plugins = Gtk.Template.Child("content-plugins") + toast_overlay = Gtk.Template.Child() - content_monet = Gtk.Template.Child("content_monet") - content_plugins = Gtk.Template.Child("content_plugins") + save_preset_button = Gtk.Template.Child("save-preset-button") - main_menu = Gtk.Template.Child("main-menu") errors_button = Gtk.Template.Child("errors-button") + errors_list = Gtk.Template.Child("errors-list") presets_dropdown = Gtk.Template.Child("presets-dropdown") presets_menu = Gtk.Template.Child("presets-menu") - monet_image_file = None def __init__(self, **kwargs): super().__init__(**kwargs) @@ -55,21 +58,34 @@ class GradienceMainWindow(Adw.ApplicationWindow): self.app = Gtk.Application.get_default() self.settings = Gio.Settings(app_id) - self.presets_dropdown.get_popover().connect( - "show", self.on_presets_dropdown_activate + self.style_manager = self.app.style_manager + + self.monet_image_file = None + + self.enabled_theme_engines = set( + self.settings.get_value("enabled-theme-engines").unpack() ) + self.setup_signals() + self.setup() + + def setup_signals(self): + self.presets_dropdown.get_popover().connect("show", + self.on_presets_dropdown_activate) + + self.connect("close-request", + self.on_close_request) + + self.connect("unrealize", + self.save_window_props) + + def setup(self): # Set devel style if build_type == "debug": self.get_style_context().add_class("devel") - self.setup_monet_page() - self.setup_colors_page() - - self.connect("close-request", self.on_close_request) - self.connect("unrealize", self.save_window_props) - - self.style_manager = self.app.style_manager + self.setup_theming_page() + self.setup_colors_group() # TODO: Check if org.freedesktop.portal.Settings portal will allow us to \ # read org.gnome.desktop.background DConf key @@ -88,15 +104,12 @@ class GradienceMainWindow(Adw.ApplicationWindow): image_basename = self.monet_image_file.get_basename() logging.debug(image_basename) self.monet_image_file = self.monet_image_file.get_path() - self.monet_file_chooser_button.set_label(image_basename) - self.monet_file_chooser_button.set_tooltip_text(self.monet_image_file) + #self.monet_file_chooser_button.set_label(image_basename) + #self.monet_file_chooser_button.set_tooltip_text(self.monet_image_file) logging.debug(self.monet_image_file) - # self.on_apply_button() # Comment out for now, because it always shows + # self.on_apply_button_clicked() # Comment out for now, because it always shows # that annoying toast on startup''' - def on_file_picker_button_clicked(self, *args): - self.monet_file_chooser_dialog.show() - def on_close_request(self, *args): if self.app.is_dirty: logging.debug("Window close request") @@ -113,143 +126,47 @@ class GradienceMainWindow(Adw.ApplicationWindow): self.settings.set_boolean("window-maximized", self.is_maximized()) self.settings.set_boolean("window-fullscreen", self.is_fullscreen()) - def on_monet_file_chooser_response(self, widget, response): - if response == Gtk.ResponseType.ACCEPT: - self.monet_image_file = self.monet_file_chooser_dialog.get_file() - image_basename = self.monet_image_file.get_basename() - self.monet_file_chooser_button.set_label(image_basename) - self.monet_file_chooser_button.set_tooltip_text(image_basename) + def setup_theming_page(self): + # TODO: Show fallback page if no theme engines are enabled + no_engines_label = Gtk.Label.new("No Theme Engines enabled") - self.monet_file_chooser_dialog.hide() + self.setup_empty_page() + self.setup_shell_group() + self.setup_monet_group() - if response == Gtk.ResponseType.ACCEPT: - self.monet_image_file = self.monet_image_file.get_path() - self.on_apply_button() + def setup_empty_page(self): + self.empty_page = GradienceEmptyThemingGroup(self) - def setup_monet_page(self): - self.monet_pref_group = Adw.PreferencesGroup() - self.monet_pref_group.set_name("monet") - self.monet_pref_group.set_title(_("Monet Engine")) - self.monet_pref_group.set_description( - _( - "Monet is an engine that generates a Material Design 3 " - "palette from an image's color." - ) - ) + if not self.enabled_theme_engines: + self.content_theming.add(self.empty_page) - self.apply_button = Gtk.Button() - self.apply_button.set_label(_("Apply")) - self.apply_button.set_valign(Gtk.Align.CENTER) - self.apply_button.connect("clicked", self.on_apply_button) - self.apply_button.set_css_classes("suggested-action") + def setup_shell_group(self): + self.shell_group = GradienceShellThemingGroup(self) - self.monet_pref_group.set_header_suffix(self.apply_button) + if "shell" in self.enabled_theme_engines: + self.content_theming.add(self.shell_group) - self.monet_file_chooser_row = Adw.ActionRow() - self.monet_file_chooser_row.set_title(_("Select an Image")) + def setup_monet_group(self): + self.monet_group = GradienceMonetThemingGroup(self) - self.monet_file_chooser_dialog = Gtk.FileChooserNative() - self.monet_file_chooser_dialog.set_title(_("Choose a Image File")) - self.monet_file_chooser_dialog.set_transient_for(self) - self.monet_file_chooser_dialog.set_modal(True) + if "monet" in self.enabled_theme_engines: + self.content_theming.add(self.monet_group) - self.monet_file_chooser_button = Gtk.Button() - self.monet_file_chooser_button.set_valign(Gtk.Align.CENTER) + def reload_theming_page(self): + if self.shell_group.is_ancestor(self.content_theming): + self.content_theming.remove(self.shell_group) - child_button = Gtk.Box() - label = Gtk.Label() - label.set_label(_("Choose a File")) - child_button.append(label) + if self.monet_group.is_ancestor(self.content_theming): + self.content_theming.remove(self.monet_group) - icon = Gtk.Image() - icon.set_from_icon_name("folder-pictures-symbolic") - child_button.append(icon) - child_button.set_spacing(10) + if self.empty_page.is_ancestor(self.content_theming): + self.content_theming.remove(self.empty_page) - self.monet_file_chooser_button.set_child(child_button) + self.setup_shell_group() + self.setup_monet_group() + self.setup_empty_page() - self.monet_file_chooser_button.connect( - "clicked", self.on_file_picker_button_clicked - ) - - self.monet_file_chooser_dialog.connect( - "response", self.on_monet_file_chooser_response - ) - - self.monet_file_chooser_row.add_suffix(self.monet_file_chooser_button) - self.monet_pref_group.add(self.monet_file_chooser_row) - - self.monet_palette_shades = GradiencePaletteShades( - "monet", _("Monet Palette"), 6 - ) - self.app.pref_palette_shades["monet"] = self.monet_palette_shades - self.monet_pref_group.add(self.monet_palette_shades) - - # FIXME: Comment out for now - '''self.tone_row = Adw.ComboRow() - self.tone_row.set_title(_("Tone")) - - store = Gtk.StringList() - store_values = [] - for i in range(20, 80, 5): - store_values.append(str(i)) - for v in store_values: - store.append(v) - self.tone_row.set_model(store) - self.monet_pref_group.add(self.tone_row)''' - - self.monet_theme_row = Adw.ComboRow() - self.monet_theme_row.set_title(_("Theme")) - - store = Gtk.StringList() - store.append(_("Auto")) - store.append(_("Light")) - store.append(_("Dark")) - self.monet_theme_row.set_model(store) - self.monet_pref_group.add(self.monet_theme_row) - - self.content_monet.add(self.monet_pref_group) - - def on_apply_button(self, *_args): - if self.monet_image_file: - try: - self.theme = Monet().generate_from_image(self.monet_image_file) - #self.tone = self.tone_row.get_selected_item() # TODO: Remove tone requirement from Monet Engine - variant_pos = self.monet_theme_row.props.selected - - class variantEnum(Enum): - AUTO = 0 - LIGHT = 1 - DARK = 2 - - def __get_variant_string(): - if variant_pos == variantEnum.AUTO.value: - return "auto" - elif variant_pos == variantEnum.DARK.value: - return "dark" - elif variant_pos == variantEnum.LIGHT.value: - return "light" - - variant_str = __get_variant_string() - - self.app.custom_css_group.reset_buffer() - - self.app.update_theme_from_monet(self.theme, variant_str) - except (OSError, AttributeError, ValueError) as e: - logging.error("Failed to generate Monet palette.", exc=e) - self.toast_overlay.add_toast( - Adw.Toast(title=_("Failed to generate Monet palette")) - ) - else: - self.toast_overlay.add_toast( - Adw.Toast(title=_("Palette generated")) - ) - else: - self.toast_overlay.add_toast( - Adw.Toast(title=_("Select a background first")) - ) - - def setup_colors_page(self): + def setup_colors_group(self): for group in preset_schema["groups"]: pref_group = Adw.PreferencesGroup() pref_group.set_name(group["name"]) @@ -264,9 +181,11 @@ class GradienceMainWindow(Adw.ApplicationWindow): variable["adw_gtk3_support"], ) pref_group.add(pref_variable) + + pref_variable.connect_signals(update_vars=True) self.app.pref_variables[variable["name"]] = pref_variable - self.content.add(pref_group) + self.content_colors.add(pref_group) palette_pref_group = Adw.PreferencesGroup() palette_pref_group.set_name("palette_colors") @@ -285,7 +204,7 @@ class GradienceMainWindow(Adw.ApplicationWindow): ) palette_pref_group.add(palette_shades) self.app.pref_palette_shades[color["prefix"]] = palette_shades - self.content.add(palette_pref_group) + self.content_colors.add(palette_pref_group) def update_errors(self, errors): child = self.errors_list.get_row_at_index(0) diff --git a/gradience/frontend/views/meson.build b/gradience/frontend/views/meson.build index 37899bcb..6987ba05 100644 --- a/gradience/frontend/views/meson.build +++ b/gradience/frontend/views/meson.build @@ -9,6 +9,7 @@ gradience_sources = [ 'preferences_window.py', 'presets_manager_window.py', 'share_window.py', + 'shell_prefs_window.py', 'welcome_window.py' ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: viewsdir) diff --git a/gradience/frontend/views/preferences_window.py b/gradience/frontend/views/preferences_window.py index 732415e3..81a8d496 100644 --- a/gradience/frontend/views/preferences_window.py +++ b/gradience/frontend/views/preferences_window.py @@ -16,11 +16,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from gi.repository import Gtk, Adw +from gi.repository import GLib, Gtk, Adw from gradience.backend.flatpak_overrides import create_gtk_user_override, remove_gtk_user_override from gradience.backend.flatpak_overrides import create_gtk_global_override, remove_gtk_global_override +from gradience.frontend.widgets.reset_preset_group import GradienceResetPresetGroup +from gradience.frontend.views.main_window import GradienceMainWindow + from gradience.backend.constants import rootdir from gradience.backend.logger import Logger @@ -32,12 +35,18 @@ logging = Logger() class GradiencePreferencesWindow(Adw.PreferencesWindow): __gtype_name__ = "GradiencePreferencesWindow" + general_page = Gtk.Template.Child() + theming_page = Gtk.Template.Child() + gtk4_user_theming_switch = Gtk.Template.Child() gtk4_global_theming_switch = Gtk.Template.Child() gtk3_user_theming_switch = Gtk.Template.Child() gtk3_global_theming_switch = Gtk.Template.Child() + monet_engine_switch = Gtk.Template.Child() + gnome_shell_engine_switch = Gtk.Template.Child() + def __init__(self, parent, **kwargs): super().__init__(**kwargs) @@ -53,6 +62,32 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow): def setup(self): self.setup_flatpak_group() + self.setup_theme_engines_group() + self.setup_reset_preset_group() + + def setup_reset_preset_group(self): + self.reset_preset_group = GradienceResetPresetGroup(self) + + self.theming_page.add(self.reset_preset_group) + + def setup_theme_engines_group(self): + if "shell" in self.win.enabled_theme_engines: + self.gnome_shell_engine_switch.set_state(True) + else: + self.gnome_shell_engine_switch.set_state(False) + + if "monet" in self.win.enabled_theme_engines: + self.monet_engine_switch.set_state(True) + else: + self.monet_engine_switch.set_state(False) + + self.gnome_shell_engine_switch.connect( + "state-set", self.on_gnome_shell_engine_switch_toggled + ) + + self.monet_engine_switch.connect( + "state-set", self.on_monet_engine_switch_toggled + ) def setup_flatpak_group(self): user_flatpak_theming_gtk4 = self.settings.get_boolean( @@ -72,7 +107,7 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow): self.gtk3_user_theming_switch.set_state( user_flatpak_theming_gtk3 ) - + # self.gtk3_global_theming_switch.set_state(global_flatpak_theming_gtk3) self.gtk4_user_theming_switch.connect( @@ -130,3 +165,37 @@ class GradiencePreferencesWindow(Adw.PreferencesWindow): logging.debug( f"global-flatpak-theming-gtk3: {self.settings.get_boolean('global-flatpak-theming-gtk3')}" ) + + def on_gnome_shell_engine_switch_toggled(self, *args): + state = self.gnome_shell_engine_switch.props.state + + if not state: + self.win.enabled_theme_engines.add("shell") + else: + self.win.enabled_theme_engines.remove("shell") + + enabled_engines = GLib.Variant.new_strv(list(self.win.enabled_theme_engines)) + self.settings.set_value("enabled-theme-engines", enabled_engines) + + self.win.reload_theming_page() + + logging.debug( + f"enabled-theme-engines: {self.settings.get_value('enabled-theme-engines')}" + ) + + def on_monet_engine_switch_toggled(self, *args): + state = self.monet_engine_switch.props.state + + if not state: + self.win.enabled_theme_engines.add("monet") + else: + self.win.enabled_theme_engines.remove("monet") + + enabled_engines = GLib.Variant.new_strv(list(self.win.enabled_theme_engines)) + self.settings.set_value("enabled-theme-engines", enabled_engines) + + self.win.reload_theming_page() + + logging.debug( + f"enabled-theme-engines: {self.settings.get_value('enabled-theme-engines')}" + ) diff --git a/gradience/frontend/views/presets_manager_window.py b/gradience/frontend/views/presets_manager_window.py index 2b5bf204..7a0a846f 100644 --- a/gradience/frontend/views/presets_manager_window.py +++ b/gradience/frontend/views/presets_manager_window.py @@ -24,7 +24,7 @@ from pathlib import Path from gi.repository import Gtk, Adw, GLib from gradience.backend.preset_downloader import PresetDownloader -from gradience.backend.theming.preset_utils import PresetUtils +from gradience.backend.theming.preset import PresetUtils from gradience.backend.globals import presets_dir, preset_repos from gradience.backend.constants import rootdir @@ -73,8 +73,6 @@ class GradiencePresetWindow(Adw.Window): def __init__(self, parent, **kwargs): super().__init__(**kwargs) - self.app = Gtk.Application.get_default() - self.parent = parent self.settings = parent.settings self.app = self.parent.get_application() diff --git a/gradience/frontend/views/shell_prefs_window.py b/gradience/frontend/views/shell_prefs_window.py new file mode 100644 index 00000000..7dcdadff --- /dev/null +++ b/gradience/frontend/views/shell_prefs_window.py @@ -0,0 +1,94 @@ +# shell_prefs_window.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gi.repository import Gtk, Adw + +from gradience.backend.utils.colors import rgb_to_hash +from gradience.backend.constants import rootdir + +from gradience.frontend.widgets.option_row import GradienceOptionRow +from gradience.frontend.schemas.shell_schema import shell_schema + +from gradience.backend.logger import Logger + +logging = Logger() + + +@Gtk.Template(resource_path=f"{rootdir}/ui/shell_prefs_window.ui") +class GradienceShellPrefsWindow(Adw.PreferencesWindow): + __gtype_name__ = "GradienceShellPrefsWindow" + + custom_colors_group = Gtk.Template.Child("custom-colors-group") + + def __init__(self, parent, shell_colors: dict, **kwargs): + super().__init__(**kwargs) + + self.shell_colors = shell_colors + + self.parent = parent + self.settings = parent.settings + self.app = self.parent.get_application() + + self.set_transient_for(self.app.get_active_window()) + + self.setup() + + def setup(self): + for variable in shell_schema["variables"]: + pref_variable = GradienceOptionRow( + variable["name"], + variable["title"] + #variable.get("explanation") + ) + self.custom_colors_group.add(pref_variable) + + pref_variable.color_value.connect("color-set", self.on_color_value_changed, pref_variable) + pref_variable.text_value.connect("changed", self.on_text_value_changed, pref_variable) + + self.set_colors(pref_variable, variable) + + def set_colors(self, widget, variable): + if len(self.shell_colors) != len(shell_schema["variables"]): + try: + self.shell_colors[variable["name"]] = variable["default_value"] + except KeyError: + try: + self.shell_colors[variable["name"]] = self.app.variables[variable["var_name"]] + except KeyError: + raise + finally: + widget.update_value(self.shell_colors[variable["name"]]) + else: + widget.update_value(self.shell_colors[variable["name"]]) + + def on_color_value_changed(self, widget, parent, *_args): + color_name = parent.props.name + color_value = widget.get_rgba().to_string() + + if color_value.startswith("rgb") or color_value.startswith("rgba"): + color_hex, alpha = rgb_to_hash(color_value) + if not alpha: + color_value = color_hex + + self.shell_colors[color_name] = color_value + + def on_text_value_changed(self, widget, parent, *_args): + color_name = parent.props.name + color_value = widget.get_text() + + self.shell_colors[color_name] = color_value diff --git a/gradience/frontend/widgets/custom_css_group.py b/gradience/frontend/widgets/custom_css_group.py index 77ea6395..07bf65e7 100644 --- a/gradience/frontend/widgets/custom_css_group.py +++ b/gradience/frontend/widgets/custom_css_group.py @@ -42,6 +42,7 @@ class GradienceCustomCSSGroup(Adw.PreferencesGroup): def load_custom_css(self, custom_css): self.custom_css = custom_css + self.custom_css_text_view.get_buffer().set_text( list(self.custom_css.values())[ self.app_type_dropdown.get_selected()] diff --git a/gradience/frontend/widgets/meson.build b/gradience/frontend/widgets/meson.build index e9604d40..21d2e0aa 100644 --- a/gradience/frontend/widgets/meson.build +++ b/gradience/frontend/widgets/meson.build @@ -6,10 +6,14 @@ gradience_sources = [ 'custom_css_group.py', 'error_list_row.py', 'explore_preset_row.py', + 'monet_theming_group.py', 'option_row.py', 'palette_shades.py', 'plugin_row.py', 'preset_row.py', - 'repo_row.py' + 'repo_row.py', + 'reset_preset_group.py', + 'shell_theming_group.py', + 'theming_empty_group.py', ] PY_INSTALLDIR.install_sources(gradience_sources, subdir: widgetsdir) diff --git a/gradience/frontend/widgets/monet_theming_group.py b/gradience/frontend/widgets/monet_theming_group.py new file mode 100644 index 00000000..e419f838 --- /dev/null +++ b/gradience/frontend/widgets/monet_theming_group.py @@ -0,0 +1,159 @@ +# monet_theming_group.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from enum import Enum + +from gi.repository import Gtk, Adw + +from gradience.backend.theming.monet import Monet +from gradience.backend.constants import rootdir + +from gradience.frontend.widgets.palette_shades import GradiencePaletteShades + +from gradience.backend.logger import Logger + +logging = Logger() + + +@Gtk.Template(resource_path=f"{rootdir}/ui/monet_theming_group.ui") +class GradienceMonetThemingGroup(Adw.PreferencesGroup): + __gtype_name__ = "GradienceMonetThemingGroup" + + monet_theming_expander = Gtk.Template.Child("monet-theming-expander") + monet_file_chooser = Gtk.Template.Child("monet-file-chooser") + monet_file_chooser_button = Gtk.Template.Child("file-chooser-button") + + def __init__(self, parent, **kwargs): + super().__init__(**kwargs) + + self.parent = parent + self.app = self.parent.get_application() + + self.monet_image_file = None + + self.setup_signals() + self.setup() + + def setup_signals(self): + self.monet_file_chooser.connect( + "response", self.on_monet_file_chooser_response) + + def setup(self): + self.monet_file_chooser.set_transient_for(self.parent) + + self.setup_palette_shades() + #self.setup_tone_row() + self.setup_theme_row() + + def setup_palette_shades(self): + self.monet_palette_shades = GradiencePaletteShades( + "monet", _("Monet Palette"), 6 + ) + self.app.pref_palette_shades["monet"] = self.monet_palette_shades + + self.monet_theming_expander.add_row(self.monet_palette_shades) + + # TODO: Rethink how it should be implemented + '''def setup_tone_row(self): + self.tone_row = Adw.ComboRow() + self.tone_row.set_title(_("Tone")) + + tone_store = Gtk.StringList() + tone_store_values = [] + + for i in range(20, 80, 5): + tone_store_values.append(str(i)) + + for v in tone_store_values: + tone_store.append(v) + + self.tone_row.set_model(tone_store) + + self.monet_theming_expander.add_row(self.tone_row)''' + + def setup_theme_row(self): + self.theme_row = Adw.ComboRow() + self.theme_row.set_title(_("Theme")) + + theme_store = Gtk.StringList() + theme_store.append(_("Auto")) + theme_store.append(_("Light")) + theme_store.append(_("Dark")) + + self.theme_row.set_model(theme_store) + + self.monet_theming_expander.add_row(self.theme_row) + + @Gtk.Template.Callback() + def on_apply_button_clicked(self, *_args): + if self.monet_image_file: + try: + monet_theme = Monet().generate_palette_from_image(self.monet_image_file) + #tone = self.tone_row.get_selected_item().get_string() # TODO: Remove tone requirement from Monet Engine + variant_pos = self.theme_row.props.selected + + class variantEnum(Enum): + AUTO = 0 + LIGHT = 1 + DARK = 2 + + def __get_variant_string(): + if variant_pos == variantEnum.AUTO.value: + return "auto" + elif variant_pos == variantEnum.DARK.value: + return "dark" + elif variant_pos == variantEnum.LIGHT.value: + return "light" + + variant_str = __get_variant_string() + + self.app.custom_css_group.reset_buffer() + + self.app.update_theme_from_monet(monet_theme, variant_str) + except (OSError, AttributeError, ValueError) as e: + logging.error("Failed to generate Monet palette", exc=e) + self.parent.toast_overlay.add_toast( + Adw.Toast(title=_("Failed to generate Monet palette")) + ) + else: + logging.info("Monet palette generated successfully") + self.parent.toast_overlay.add_toast( + Adw.Toast(title=_("Palette generated")) + ) + else: + logging.error("Input image for Monet generation not selected") + self.parent.toast_overlay.add_toast( + Adw.Toast(title=_("Select an image first")) + ) + + @Gtk.Template.Callback() + def on_file_chooser_button_clicked(self, *_args): + self.monet_file_chooser.show() + + def on_monet_file_chooser_response(self, widget, response): + if response == Gtk.ResponseType.ACCEPT: + self.monet_image_file = self.monet_file_chooser.get_file() + image_basename = self.monet_image_file.get_basename() + self.monet_file_chooser_button.set_label(image_basename) + self.monet_file_chooser_button.set_tooltip_text(image_basename) + + self.monet_file_chooser.hide() + + if response == Gtk.ResponseType.ACCEPT: + self.monet_image_file = self.monet_image_file.get_path() + self.on_apply_button_clicked() diff --git a/gradience/frontend/widgets/option_row.py b/gradience/frontend/widgets/option_row.py index 2666b01e..01057024 100644 --- a/gradience/frontend/widgets/option_row.py +++ b/gradience/frontend/widgets/option_row.py @@ -1,7 +1,7 @@ # option_row.py # # Change the look of Adwaita, with ease -# Copyright (C) 2022 Gradience Team +# Copyright (C) 2022-2023, Gradience Team # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -35,7 +35,7 @@ class GradienceOptionRow(Adw.ActionRow): explanation_button = Gtk.Template.Child("explanation-button") explanation_label = Gtk.Template.Child("explanation-label") - def __init__(self, name, title, explanation, adw_gtk3_support="yes", **kwargs): + def __init__(self, name, title, explanation=None, adw_gtk3_support=None, update_var=None, **kwargs): super().__init__(**kwargs) self.app = Gtk.Application.get_default() @@ -44,7 +44,7 @@ class GradienceOptionRow(Adw.ActionRow): self.set_title(title) self.set_subtitle("@" + name) - if adw_gtk3_support == "yes": + if adw_gtk3_support == "yes" or not adw_gtk3_support: self.warning_button.set_visible(False) elif adw_gtk3_support == "partial": self.warning_button.add_css_class("warning") @@ -58,11 +58,17 @@ class GradienceOptionRow(Adw.ActionRow): ) self.explanation_label.set_label(explanation or "") - if explanation is None: + + if not explanation: self.explanation_button.set_visible(False) - @Gtk.Template.Callback() - def on_color_value_changed(self, *_args): + self.update_var = update_var + + def connect_signals(self, update_vars): + self.color_value.connect("color-set", self.on_color_value_changed, update_vars) + self.text_value.connect("changed", self.on_text_value_changed, update_vars) + + def on_color_value_changed(self, _unused, update_vars, *_args): color_value = self.color_value.get_rgba().to_string() if color_value.startswith("rgb") or color_value.startswith("rgba"): @@ -71,14 +77,14 @@ class GradienceOptionRow(Adw.ActionRow): color_value = color_hex self.update_value( - color_value, update_from="color_value" + color_value, update_from="color_value", update_vars=update_vars ) - @Gtk.Template.Callback() - def on_text_value_changed(self, *_args): + def on_text_value_changed(self, _unused, update_vars, *_args): color_value = self.text_value.get_text() + self.update_value( - color_value, update_from="text_value" + color_value, update_from="text_value", update_vars=update_vars ) @Gtk.Template.Callback() @@ -88,7 +94,7 @@ class GradienceOptionRow(Adw.ActionRow): else: self.value_stack.set_visible_child(self.color_value) - def update_value(self, new_value, **kwargs): + def update_value(self, new_value, update_vars=False, **kwargs): rgba = Gdk.RGBA() is_app_ready = self.app.is_ready @@ -109,7 +115,11 @@ class GradienceOptionRow(Adw.ActionRow): self.color_value.set_rgba(rgba) self.color_value.set_tooltip_text(_("Not a color, see text value")) - if is_app_ready and kwargs.get("update_from") == "text_value" and new_value != "": - self.app.variables[self.get_name()] = new_value - self.app.mark_as_dirty() - self.app.reload_variables() + if update_vars == True: + if is_app_ready and kwargs.get("update_from") == "text_value" and new_value != "": + if self.update_var: + self.update_var[self.get_name()] = new_value + else: + self.app.variables[self.get_name()] = new_value + self.app.mark_as_dirty() + self.app.reload_variables() diff --git a/gradience/frontend/widgets/reset_preset_group.py b/gradience/frontend/widgets/reset_preset_group.py new file mode 100644 index 00000000..aa400887 --- /dev/null +++ b/gradience/frontend/widgets/reset_preset_group.py @@ -0,0 +1,98 @@ +# reset_preset_group.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gi.repository import GLib, Gtk, Adw + +from gradience.backend.constants import rootdir +from gradience.backend.logger import Logger +from gradience.backend.theming.preset import PresetUtils + +from gradience.frontend.dialogs.log_out_dialog import GradienceLogOutDialog + +logging = Logger() + + +@Gtk.Template(resource_path=f"{rootdir}/ui/reset_preset_group.ui") +class GradienceResetPresetGroup(Adw.PreferencesGroup): + __gtype_name__ = "GradienceResetPresetGroup" + + def __init__(self, parent, **kwargs): + super().__init__(**kwargs) + + self.parent = parent + + self.app = self.parent.get_application() + self.win = self.parent + + self.setup_signals() + self.setup() + + def setup_signals(self): + pass + + def setup(self): + pass + + @Gtk.Template.Callback() + def on_libadw_restore_button_clicked(self, *_args): + try: + PresetUtils().restore_preset("gtk4") + except GLib.GError: + self.parent.add_toast( + Adw.Toast(title=_("Unable to restore GTK 4 backup")) + ) + else: + dialog = GradienceLogOutDialog(self.win) + dialog.present() + + @Gtk.Template.Callback() + def on_libadw_reset_button_clicked(self, *_args): + try: + PresetUtils().reset_preset("gtk4") + except GLib.GError: + self.parent.add_toast( + Adw.Toast(title=_("Unable to delete current preset")) + ) + else: + dialog = GradienceLogOutDialog(self.win) + dialog.present() + + + @Gtk.Template.Callback() + def on_gtk3_restore_button_clicked(self, *_args): + try: + PresetUtils().restore_preset("gtk3") + except GLib.GError: + self.parent.add_toast( + Adw.Toast(title=_("Unable to restore GTK 3 backup")) + ) + else: + dialog = GradienceLogOutDialog(self.win) + dialog.present() + + @Gtk.Template.Callback() + def on_gtk3_reset_button_clicked(self, *_args): + try: + PresetUtils().reset_preset("gtk3") + except GLib.GError: + self.parent.add_toast( + Adw.Toast(title=_("Unable to delete current preset")) + ) + else: + dialog = GradienceLogOutDialog(self.win) + dialog.present() diff --git a/gradience/frontend/widgets/shell_theming_group.py b/gradience/frontend/widgets/shell_theming_group.py new file mode 100644 index 00000000..65a2c2c2 --- /dev/null +++ b/gradience/frontend/widgets/shell_theming_group.py @@ -0,0 +1,246 @@ +# shell_theming_group.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from enum import Enum +from subprocess import SubprocessError + +from gi.repository import GObject, GLib, Gio, Gtk, Adw + +from gradience.backend.utils.gnome import is_gnome_available, is_shell_ext_installed +from gradience.backend.utils.subprocess import GradienceSubprocess +from gradience.backend.constants import rootdir +from gradience.backend.exceptions import UnsupportedShellVersion +from gradience.backend.logger import Logger + +from gradience.backend.theming.shell import ShellTheme + +from gradience.frontend.schemas.shell_schema import shell_schema +from gradience.frontend.dialogs.unsupported_shell_dialog import GradienceUnsupportedShellDialog +from gradience.frontend.views.shell_prefs_window import GradienceShellPrefsWindow + +logging = Logger() + + +@Gtk.Template(resource_path=f"{rootdir}/ui/shell_theming_group.ui") +class GradienceShellThemingGroup(Adw.PreferencesGroup): + __gtype_name__ = "GradienceShellThemingGroup" + + variant_row = Gtk.Template.Child("variant-row") + shell_theming_expander = Gtk.Template.Child("shell-theming-expander") + other_options_row = Gtk.Template.Child("other-options-row") + + def __init__(self, parent, **kwargs): + super().__init__(**kwargs) + + self.shell_colors = {} + + self.parent = parent + self.settings = parent.settings + + self.app = parent.get_application() + self.win = self.app.get_active_window() + self.toast_overlay = parent.toast_overlay + + self.setup_signals() + self.setup() + + def setup_signals(self): + self.app.connect("preset-reload", self.reload_colors) + + def setup(self): + self.setup_variant_row() + + self.shell_theming_expander.add_row(self.other_options_row) + + def setup_variant_row(self): + variant_store = Gtk.StringList() + variant_store.append(_("Dark")) + variant_store.append(_("Light")) + + self.variant_row.set_model(variant_store) + + # TODO: Maybe allow it when using export option? + '''def setup_version_row(self): + version_store = Gtk.StringList() + version_store.append(_("Auto")) + version_store.append(_("43")) + version_store.append(_("42")) + + self.shell_version_row.set_model(version_store)''' + + def reload_colors(self, *args): + try: + for variable in shell_schema["variables"]: + self.set_colors(variable) + except Exception as e: + logging.error("An unexpected error occurred while loading variable colors.", exc=e) + self.toast_overlay.add_toast( + Adw.Toast( + title=_("An unexpected error occurred while loading variable colors.")) + ) + + def set_colors(self, variable): + try: + self.shell_colors[variable["name"]] = variable["default_value"] + except KeyError: + try: + self.shell_colors[variable["name"]] = self.app.variables[variable["var_name"]] + except KeyError: + raise + + @Gtk.Template.Callback() + def on_custom_colors_button_clicked(self, *_args): + self.shell_pref_window = GradienceShellPrefsWindow(self.parent, self.shell_colors) + self.shell_pref_window.present() + + @Gtk.Template.Callback() + def on_apply_button_clicked(self, *_args): + user_themes_available = is_shell_ext_installed(ShellTheme().THEME_EXT_NAME) + user_themes_enabled = is_shell_ext_installed( + ShellTheme().THEME_EXT_NAME, check_enabled=True) + + if not is_gnome_available(): + dialog = Adw.MessageDialog(transient_for=self.win, heading=_("GNOME Shell Missing"), + body=_("Shell Engine is designed to work only on systems running GNOME. You can still generate themes on other desktop environments, but it won't have any affect on them.")) + + dialog.add_response("disable-engine", _("Disable Engine")) + dialog.add_response("continue-anyway", _("Continue Anyway")) + dialog.set_response_appearance("disable-engine", Adw.ResponseAppearance.DESTRUCTIVE) + dialog.set_default_response("continue-anyway") + + dialog.connect("response", self.on_shell_missing_response) + dialog.present() + elif is_gnome_available() and not user_themes_available: + dialog = Adw.MessageDialog(transient_for=self.win, heading=_("User Themes Extension Missing"), + body=_("Gradience requires User Themes extension installed in order to apply Shell theme. You can still generate a theme, but you won't be able to apply it without this extension.")) + + dialog.add_response("install-extension", _("Install Extension")) + dialog.add_response("continue-anyway", _("Continue Anyway")) + dialog.set_response_appearance("install-extension", Adw.ResponseAppearance.SUGGESTED) + dialog.set_default_response("continue-anyway") + + dialog.connect("response", self.on_user_themes_missing_response) + dialog.present() + elif is_gnome_available() and user_themes_available and not user_themes_enabled: + dialog = Adw.MessageDialog(transient_for=self.win, heading=_("User Themes Extension Disabled"), + body=_("User Themes extension is currently disabled on your system. Please enable it in order to apply theme.")) + + dialog.add_response("cancel", _("Cancel")) + #dialog.add_response("enable-extension", _("Enable Extension")) + dialog.add_response("continue-anyway", _("Continue Anyway")) + dialog.set_response_appearance("cancel", Adw.ResponseAppearance.SUGGESTED) + #dialog.set_response_appearance("enable-extension", Adw.ResponseAppearance.SUGGESTED) + dialog.set_default_response("continue-anyway") + + dialog.connect("response", self.on_user_themes_disabled_response) + dialog.present() + else: + self.apply_shell_theme() + + def apply_shell_theme(self): + variant_pos = self.variant_row.props.selected + + class variantEnum(Enum): + DARK = 0 + LIGHT = 1 + + def __get_variant_string(): + if variant_pos == variantEnum.DARK.value: + return "dark" + elif variant_pos == variantEnum.LIGHT.value: + return "light" + + variant_str = __get_variant_string() + + try: + ShellTheme().apply_theme_async(self, self._on_shell_theme_done, + variant_str, self.app.preset) + except UnsupportedShellVersion as exception_message: + logging.error(exception_message) + GradienceUnsupportedShellDialog(self.parent).present() + except (ValueError, OSError, GLib.GError) as e: + logging.error( + "An error occurred while generating a Shell theme.", exc=e) + self.toast_overlay.add_toast( + Adw.Toast( + title=_("An error occurred while generating a Shell theme.")) + ) + + def _on_shell_theme_done(self, source_widget:GObject.Object, + result:Gio.AsyncResult, user_data:GObject.GPointer): + logging.debug("It works! \o/") + self.toast_overlay.add_toast( + Adw.Toast(title=_("Shell theme applied successfully.")) + ) + + def on_shell_missing_response(self, widget, response, *args): + if response == "disable-engine": + self.win.enabled_theme_engines.remove("shell") + + enabled_engines = GLib.Variant.new_strv(list(self.win.enabled_theme_engines)) + self.settings.set_value("enabled-theme-engines", enabled_engines) + + self.win.reload_theming_page() + elif response == "continue-anyway": + self.apply_shell_theme() + + # FIXME: Hangs until Extension Manager is closed. We might something like `run_app` function \ + # with subrocess.Popen instead of subrocess.run to make it not hang Gradience + def on_user_themes_missing_response(self, widget, response, *args): + if response == "install-extension": + try: + cmd_list = ["xdg-open", "gnome-extensions://user-theme%40gnome-shell-extensions.gcampax.github.com?action=install"] + GradienceSubprocess().run(cmd_list, allow_escaping=True) + except SubprocessError: + logging.warning("Can't open 'gnome-extensions://' URI scheme, trying to open EGO webpage") + try: + cmd_list = ["xdg-open", "https://extensions.gnome.org/extension/19/user-themes/"] + GradienceSubprocess().run(cmd_list, allow_escaping=True) + except SubprocessError as e: + logging.error("Failed to load extension's website", exc=e) + self.toast_overlay.add_toast( + Adw.Toast(title=_("Failed to load extension's install link.")) + ) + except FileNotFoundError: + logging.error("xdg-open command missing, are you even on GNOME? \nOpen this link: https://extensions.gnome.org/extension/19/user-themes/") + self.toast_overlay.add_toast( + Adw.Toast(title=_("Failed to load extension's install link.")) + ) + elif response == "continue-anyway": + self.apply_shell_theme() + + def on_user_themes_disabled_response(self, widget, response, *args): + '''if response == "enable-extension": + pass''' + if response == "continue-anyway": + self.apply_shell_theme() + + @Gtk.Template.Callback() + def on_reset_theme_clicked(self, *_args): + # TODO: Make this function actually remove Shell theme + ShellTheme().reset_theme_async(self, self._on_reset_theme_done) + + def _on_reset_theme_done(self, source_widget:GObject.Object, + result:Gio.AsyncResult, user_data:GObject.GPointer): + self.toast_overlay.add_toast( + Adw.Toast(title=_("Shell theme successfully reset.")) + ) + + @Gtk.Template.Callback() + def on_restore_button_clicked(self, *_args): + logging.debug("Nothing here yet /o\ ") diff --git a/gradience/frontend/widgets/theming_empty_group.py b/gradience/frontend/widgets/theming_empty_group.py new file mode 100644 index 00000000..b5a180be --- /dev/null +++ b/gradience/frontend/widgets/theming_empty_group.py @@ -0,0 +1,51 @@ +# theming_empty_group.py +# +# Change the look of Adwaita, with ease +# Copyright (C) 2023, Gradience Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from enum import Enum + +from gi.repository import Adw, Gio, GLib, Gtk + +from gradience.backend.constants import rootdir +from gradience.backend.exceptions import UnsupportedShellVersion +from gradience.backend.logger import Logger +from gradience.backend.theming.shell import ShellTheme + +from gradience.frontend.views.shell_prefs_window import GradienceShellPrefsWindow + +logging = Logger() + + +@Gtk.Template(resource_path=f"{rootdir}/ui/theming_empty_group.ui") +class GradienceEmptyThemingGroup(Adw.PreferencesGroup): + __gtype_name__ = "GradienceEmptyThemingGroup" + + def __init__(self, parent, **kwargs): + super().__init__(**kwargs) + + self.parent = parent + self.settings = parent.settings + self.app = self.parent.get_application() + + self.setup_signals() + self.setup() + + def setup_signals(self): + pass + + def setup(self): + pass diff --git a/po/POTFILES b/po/POTFILES index be92a3af..c7ef1f38 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -5,6 +5,7 @@ data/ui/app_type_dialog.blp data/ui/builtin_preset_row.blp data/ui/custom_css_group.blp data/ui/explore_preset_row.blp +data/ui/monet_theming_group.blp data/ui/log_out_dialog.blp data/ui/no_plugin_window.blp data/ui/option_row.blp @@ -12,28 +13,38 @@ data/ui/plugin_row.blp data/ui/preferences_window.blp data/ui/preset_row.blp data/ui/presets_manager_window.blp +data/ui/reset_preset_group.blp data/ui/save_dialog.blp data/ui/share_window.blp +data/ui/shell_prefs_window.blp +data/ui/shell_theming_group.blp +data/ui/theming_empty_group.blp data/ui/welcome_window.blp data/ui/window.blp +gradience/frontend/schemas/preset_schema.py +gradience/frontend/schemas/shell_schema.py gradience/frontend/dialogs/app_type_dialog.py gradience/frontend/dialogs/log_out_dialog.py gradience/frontend/dialogs/save_dialog.py -gradience/frontend/main.py -gradience/frontend/schemas/preset_schema.py +gradience/frontend/dialogs/unsupported_shell_dialog.py +gradience/frontend/widgets/builtin_preset_row.py +gradience/frontend/widgets/custom_css_group.py +gradience/frontend/widgets/error_list_row.py +gradience/frontend/widgets/explore_preset_row.py +gradience/frontend/widgets/monet_theming_group.py +gradience/frontend/widgets/option_row.py +gradience/frontend/widgets/palette_shades.py +gradience/frontend/widgets/plugin_row.py +gradience/frontend/widgets/preset_row.py +gradience/frontend/widgets/repo_row.py +gradience/frontend/widgets/reset_preset_group.py +gradience/frontend/widgets/shell_theming_group.py gradience/frontend/views/about_window.py gradience/frontend/views/main_window.py gradience/frontend/views/no_plugin_window.py gradience/frontend/views/plugins_list.py gradience/frontend/views/preferences_window.py gradience/frontend/views/presets_manager_window.py +gradience/frontend/views/shell_prefs_window.py gradience/frontend/views/welcome_window.py -gradience/frontend/widgets/builtin_preset_row.py -gradience/frontend/widgets/custom_css_group.py -gradience/frontend/widgets/error_list_row.py -gradience/frontend/widgets/explore_preset_row.py -gradience/frontend/widgets/option_row.py -gradience/frontend/widgets/palette_shades.py -gradience/frontend/widgets/plugin_row.py -gradience/frontend/widgets/preset_row.py -gradience/frontend/widgets/repo_row.py +gradience/frontend/main.py diff --git a/requirements.txt b/requirements.txt index 41fad0a9..3ec0bf4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,3 +31,4 @@ material-color-utilities-python svglib yapsy Jinja2 +libsass \ No newline at end of file