GNOME Shell theming support (#679)

Signed-off-by: tfuxu <73042332+tfuxu@users.noreply.github.com>
Co-authored-by: 0xMRTT <0xMRTT@evta.fr>
Co-authored-by: 0xMRTT <0xMRTT@proton.me>
Co-authored-by: daudix-UFO <ddaudix@gmail.com>
This commit is contained in:
tfuxu 2023-05-04 13:24:18 +00:00 committed by GitHub
parent 905b314b6a
commit e412a2c537
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 3995 additions and 777 deletions

View file

@ -38,6 +38,9 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
with:
submodules: recursive
- name: Install dependencies
run: |
dnf -y install docker

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "data/submodules"]
path = data/submodules
url = https://github.com/GradienceTeam/Submodules

View file

@ -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

View file

@ -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)
<details>
<summary>🪛️ Manual revert</summary>

View file

@ -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",

View file

@ -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",

View file

@ -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"
}
]
}

View file

@ -22,6 +22,9 @@
<key name="enabled-plugins" type="as">
<default>[]</default>
</key>
<key name="enabled-theme-engines" type="as">
<default>['shell', 'monet']</default>
</key>
<key name="user-flatpak-theming-gtk4" type="b">
<default>false</default>
</key>

View file

@ -10,6 +10,7 @@
<file preprocess="xml-stripblanks">ui/error_list_row.ui</file>
<file preprocess="xml-stripblanks">ui/explore_preset_row.ui</file>
<file preprocess="xml-stripblanks">ui/log_out_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/monet_theming_group.ui</file>
<file preprocess="xml-stripblanks">ui/no_plugin_window.ui</file>
<file preprocess="xml-stripblanks">ui/option_row.ui</file>
<file preprocess="xml-stripblanks">ui/palette_shades.ui</file>
@ -18,8 +19,12 @@
<file preprocess="xml-stripblanks">ui/preset_row.ui</file>
<file preprocess="xml-stripblanks">ui/presets_manager_window.ui</file>
<file preprocess="xml-stripblanks">ui/repo_row.ui</file>
<file preprocess="xml-stripblanks">ui/reset_preset_group.ui</file>
<file preprocess="xml-stripblanks">ui/save_dialog.ui</file>
<file preprocess="xml-stripblanks">ui/share_window.ui</file>
<file preprocess="xml-stripblanks">ui/shell_prefs_window.ui</file>
<file preprocess="xml-stripblanks">ui/shell_theming_group.ui</file>
<file preprocess="xml-stripblanks">ui/theming_empty_group.ui</file>
<file preprocess="xml-stripblanks">ui/welcome_window.ui</file>
<file preprocess="xml-stripblanks">ui/window.ui</file>
<file>images/welcome-dark.svg</file>

View file

@ -73,3 +73,6 @@ endif
subdir('icons')
subdir('plugins')
subdir('shell')
subdir('submodules/gnome-shell')

View file

@ -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"
}
}
}
}

View file

@ -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"
}
}
}
}

View file

@ -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"
}
}
}
}

5
data/shell/meson.build Normal file
View file

@ -0,0 +1,5 @@
install_subdir('templates',
install_dir: join_paths(get_option('datadir'), 'gradience', 'shell'),
exclude_files: 'meson.build',
strip_directory : false
)

View file

@ -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"); }
}

View file

@ -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%));

View file

@ -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}}

View file

@ -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}};

View file

@ -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;
}
}

View file

@ -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"));
}
}

View file

@ -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"); }
}

View file

@ -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%));

View file

@ -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}}

View file

@ -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}};

View file

@ -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;
}
}

View file

@ -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"));
}
}

View file

@ -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"); }
}

View file

@ -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%));

View file

@ -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}}

View file

@ -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}};

View file

@ -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;
}
}

View file

@ -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"));
}
}

1
data/submodules Submodule

@ -0,0 +1 @@
Subproject commit 49a44283b22d831abc7898c2dd52313e3b951be3

View file

@ -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@']

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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;
}
}
}
}
}

View file

@ -0,0 +1,47 @@
using Gtk 4.0;
using Adw 1;
template GradienceResetPresetGroup : Adw.PreferencesGroup {
title: _("Reset &amp; 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"]
}
}
}

View file

@ -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.");
}
}
}

View file

@ -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"]
}
}

View file

@ -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";
}
}
}

View file

@ -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 {

View file

@ -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");

View file

@ -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]

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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 <https://www.gnu.org/licenses/>.
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)

View file

@ -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

View file

@ -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)

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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 <https://www.gnu.org/licenses/>.
#
# 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 "<gradience._GSettingsSchema: %s>" % 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)

View file

@ -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)

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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 <https://www.gnu.org/licenses/>.
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)'''

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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)

View file

@ -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)

View file

@ -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")

View file

@ -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)

View file

@ -37,10 +37,15 @@ class GradienceSaveDialog(Adw.MessageDialog):
self.body = _(
"Saving preset to <tt>{0}</tt>. 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

View file

@ -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 <https://www.gnu.org/licenses/>.
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")

View file

@ -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()

View file

@ -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)

View file

@ -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"]
}

View file

@ -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 <https://www.gnu.org/licenses/>.
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")
}
]
}

View file

@ -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)

View file

@ -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)

View file

@ -16,11 +16,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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')}"
)

View file

@ -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()

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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()]

View file

@ -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)

View file

@ -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 <https://www.gnu.org/licenses/>.
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()

View file

@ -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()

View file

@ -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 <https://www.gnu.org/licenses/>.
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()

View file

@ -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 <https://www.gnu.org/licenses/>.
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\ ")

View file

@ -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 <https://www.gnu.org/licenses/>.
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

View file

@ -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

View file

@ -31,3 +31,4 @@ material-color-utilities-python
svglib
yapsy
Jinja2
libsass