From ca13ba8caca1a3f0bdee06799847edd95efe70f1 Mon Sep 17 00:00:00 2001 From: Popa Ioan Alexandru Date: Tue, 16 Jul 2024 09:40:49 +0300 Subject: [PATCH] implement gtk4 theming over libadwaita apps by overwriting the GTK4 CSS --- src/applythemes.py | 5 ++++- src/getavailablethemes.py | 41 +++++++++++++++++++------------------- src/searchablethemelist.py | 16 ++++++++------- src/window.py | 19 +++++++++++++++++- src/window.ui | 2 +- 5 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/applythemes.py b/src/applythemes.py index 4904896..c3c4a1c 100644 --- a/src/applythemes.py +++ b/src/applythemes.py @@ -23,7 +23,7 @@ class BaseApplyThemes: """ The base theme applying mechanism: write themes and settings to config files """ - def applyThemes(self, props, gtk2Theme, gtk4Theme, kvantumTheme, kvantumThemeFilePath, cssText): + def applyThemes(self, props, gtk2Theme, gtk4Theme, kvantumTheme, kvantumThemeFilePath, cssText, gtk4ThemePath): gtkKeyFile = GLib.KeyFile() gtkKeyFile.set_string("Settings", "gtk-theme-name", gtk4Theme) @@ -99,6 +99,9 @@ def applyThemes(self, props, gtk2Theme, gtk4Theme, kvantumTheme, kvantumThemeFil with open(os.path.join(GLib.get_user_config_dir(), "gtk-3.0", "gtk.css"), "w") as cssFile: cssFile.write(cssText) with open(os.path.join(GLib.get_user_config_dir(), "gtk-4.0", "gtk.css"), "w") as cssFile: + if gtk4ThemePath is not None: + cssFile.write("* {all: unset;}\n") + cssFile.write(f'@import url("{gtk4ThemePath}")\n') cssFile.write(cssText) class GSettingsApplyThemes(BaseApplyThemes): diff --git a/src/getavailablethemes.py b/src/getavailablethemes.py index 5834de0..9fb0861 100644 --- a/src/getavailablethemes.py +++ b/src/getavailablethemes.py @@ -1,15 +1,15 @@ # Copyright (C) 2021 Popa Ioan Alexandru -# +# # 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 2 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. @@ -31,17 +31,16 @@ def uniquifySortedListStore(listStore): """ The functions below create Gtk.ListStore's that store themes for use in SearchableThemeList's and have the following columns: -1. A str column which holds the display name of the theme -2. A str column which holds the underlying theme name -3. (Optional, depends on which kind of themes are used) - a) A GdkPixbuf.Pixbuf column which holds the theme's preview image - b) A str column which holds the theme file path (for Kvantum) +0. A str column which holds the display name of the theme +1. A str column which holds the underlying theme name +2. A str column which holds the theme file/folder path (those starting in "//" are fake ones that show a lack of a theme file) +3. (Optional) A GdkPixbuf.Pixbuf column which holds the theme's preview image """ def getAvailableGtk3Themes(): - availableThemes = Gtk.ListStore(str, str, GdkPixbuf.Pixbuf) + availableThemes = Gtk.ListStore(str, str, str, GdkPixbuf.Pixbuf) availableThemes.set_sort_column_id(0, Gtk.SortType.ASCENDING) - availableThemes.append([" Adwaita", "Adwaita", + availableThemes.append([" Adwaita", "Adwaita", "//none", GdkPixbuf.Pixbuf.new_from_resource( "/com/github/alex11br/themechanger/adwaita.png" ) @@ -58,7 +57,7 @@ def getAvailableGtk3Themes(): availableThemes.append([ # we add a space to the beginning of the display name # to create spacing between the theme preview image and the theme name text - " "+f, f, + " "+f, f, os.path.join(lookupPath, f), GdkPixbuf.Pixbuf.new_from_file_at_size( thumbnailFile, 120, 35 ) if os.path.isfile(thumbnailFile) else GdkPixbuf.Pixbuf.new_from_resource( @@ -70,7 +69,7 @@ def getAvailableGtk3Themes(): return uniquifySortedListStore(availableThemes) def getAvailableIconThemes(): - availableThemes = Gtk.ListStore(str, str, GdkPixbuf.Pixbuf) + availableThemes = Gtk.ListStore(str, str, str, GdkPixbuf.Pixbuf) availableThemes.set_sort_column_id(0, Gtk.SortType.ASCENDING) lookupPaths = [ os.path.join(GLib.get_user_data_dir(), "icons"), @@ -89,7 +88,7 @@ def getAvailableIconThemes(): ) if (themeFileParser.get_string("Icon Theme", "Directories")): availableThemes.append([ - themeFileParser.get_locale_string("Icon Theme", "Name"), f, + themeFileParser.get_locale_string("Icon Theme", "Name"), f, os.path.join(lookupPath, f), iconTheme.load_icon( iconTheme.get_example_icon_name(), 32, Gtk.IconLookupFlags.FORCE_SIZE @@ -102,10 +101,10 @@ def getAvailableIconThemes(): return uniquifySortedListStore(availableThemes) def getAvailableCursorThemes(): - availableThemes = Gtk.ListStore(str, str, GdkPixbuf.Pixbuf) + availableThemes = Gtk.ListStore(str, str, str, GdkPixbuf.Pixbuf) availableThemes.set_sort_column_id(0, Gtk.SortType.ASCENDING) availableThemes.append([ - "Default cursor", "default", + "Default cursor", "default", "//none", GdkPixbuf.Pixbuf.new_from_resource( "/com/github/alex11br/themechanger/defaultcursor.png" ) @@ -125,7 +124,7 @@ def getAvailableCursorThemes(): os.path.join(lookupPath, f, "index.theme"), GLib.KeyFileFlags.NONE ) availableThemes.append([ - themeFileParser.get_locale_string("Icon Theme", "Name"), f, + themeFileParser.get_locale_string("Icon Theme", "Name"), f, os.path.join(lookupPath, f), pixbufFromXCursor( cursorPath ) if os.path.isfile(cursorPath) else GdkPixbuf.Pixbuf.new_from_resource( @@ -139,9 +138,9 @@ def getAvailableCursorThemes(): return uniquifySortedListStore(availableThemes) def getAvailableGtk4Themes(): - availableThemes = Gtk.ListStore(str, str) + availableThemes = Gtk.ListStore(str, str, str) availableThemes.set_sort_column_id(0, Gtk.SortType.ASCENDING) - availableThemes.append(["Adwaita", "Adwaita"]) + availableThemes.append(["Adwaita", "Adwaita", "//none"]) lookupPaths = [ os.path.join(GLib.get_user_data_dir(), "themes"), os.path.join(GLib.get_home_dir(), ".themes"), @@ -150,13 +149,13 @@ def getAvailableGtk4Themes(): try: for f in os.listdir(lookupPath): if os.path.isfile(os.path.join(lookupPath, f, "gtk-4.0", "gtk.css")): - availableThemes.append([f, f]) + availableThemes.append([f, f, os.path.join(lookupPath, f)]) except: pass return uniquifySortedListStore(availableThemes) def getAvailableGtk2Themes(): - availableThemes = Gtk.ListStore(str, str) + availableThemes = Gtk.ListStore(str, str, str) availableThemes.set_sort_column_id(0, Gtk.SortType.ASCENDING) lookupPaths = [ os.path.join(GLib.get_user_data_dir(), "themes"), @@ -166,7 +165,7 @@ def getAvailableGtk2Themes(): try: for f in os.listdir(lookupPath): if os.path.isfile(os.path.join(lookupPath, f, "gtk-2.0", "gtkrc")): - availableThemes.append([f, f]) + availableThemes.append([f, f, os.path.join(lookupPath, f)]) except: pass return uniquifySortedListStore(availableThemes) diff --git a/src/searchablethemelist.py b/src/searchablethemelist.py index af5114b..6f49a90 100644 --- a/src/searchablethemelist.py +++ b/src/searchablethemelist.py @@ -1,15 +1,15 @@ # Copyright (C) 2021 Popa Ioan Alexandru -# +# # 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 2 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, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. @@ -38,8 +38,8 @@ def __init__(self, themesTreeViewModel, selectedTheme, onThemeSelectedCallback, if hasPixbuf: pixbufCell = Gtk.CellRendererPixbuf() themeColumn.pack_start(pixbufCell, False) - themeColumn.add_attribute(pixbufCell, "pixbuf", 2) - + themeColumn.add_attribute(pixbufCell, "pixbuf", 3) + nameCell = Gtk.CellRendererText() themeColumn.pack_start(nameCell, True) themeColumn.add_attribute(nameCell, "text", 0) @@ -54,10 +54,12 @@ def selectTheme(self): self.themesTreeViewModelFiltered.get_path(row.iter), None, False ) + self.selectedThemeRow = row return # Maybe the selectedTheme isn't in the model; we'll properly handle this case here firstRow = self.themesTreeViewModelFiltered[0] self.selectedTheme = firstRow[1] + self.selectedThemeRow = firstRow self.themesTreeViewSelection.select_iter(firstRow.iter) self.themesTreeView.scroll_to_point(0, 0) @@ -70,7 +72,7 @@ def setThemesTreeViewModel(self, themesTreeViewModel): def setScrolledWindowOverlayScrolling(self, state): self.themesScrolledWindow.set_overlay_scrolling(state) - + def filterFunc(self, model, treeiter, data): return self.themesSearchEntry.get_text().lower() in model[treeiter][0].lower() @@ -83,6 +85,7 @@ def onThemeSelected(self, selection): if not treeiter: return self.selectedTheme = model[treeiter][1] + self.selectedThemeRow = model[treeiter] if self.hasThemeFilePath: self.onThemeSelectedCallback(self.selectedTheme, model[treeiter][2]) else: @@ -93,4 +96,3 @@ def onChangeFilter(self, searchentry): self.themesTreeViewModelFiltered.refilter() # This handles the case in which the newly filtered model doen't have the selectedTheme anymore self.selectTheme() - \ No newline at end of file diff --git a/src/window.py b/src/window.py index d135777..b4b48bb 100644 --- a/src/window.py +++ b/src/window.py @@ -246,6 +246,22 @@ def setDefaultCursor(self): defaultCursor = Gdk.Cursor.new_for_display(self.defaultDisplay, Gdk.CursorType.LEFT_PTR) self.defaultScreen.get_root_window().set_cursor(defaultCursor) + def getGtk4ThemeCssPath(self): + if self.anotherGtk4ThemeSwitch.get_active(): + gtk4ThemeRoot = self.gtk4SearchableThemeList.selectedThemeRow[2] + else: + gtk4ThemeRoot = self.gtkSearchableThemeList.selectedThemeRow[2] + + if gtk4ThemeRoot[:2] == "//": + return None + + gtk4ThemeCssPath = os.path.join(gtk4ThemeRoot, "gtk-4.0", "gtk-dark.css" if self.gtkProps.gtk_application_prefer_dark_theme else "gtk.css") + if not os.path.isfile(gtk4ThemeCssPath): + gtk4ThemeCssPath = os.path.join(gtk4ThemeRoot, "gtk-4.0", "gtk.css") + if not os.path.isfile(gtk4ThemeCssPath): + return None + return gtk4ThemeCssPath + def onGtkThemeChanged(self, themename): self.onSettingChanged() @@ -380,7 +396,8 @@ def applySettings(self, *args): gtk4Theme=self.gtk4ThemeName, kvantumTheme=self.kvantumThemeName, kvantumThemeFilePath=self.kvantumThemeFilePath, - cssText=self.cssTextBuffer.props.text + cssText=self.cssTextBuffer.props.text, + gtk4ThemePath=self.getGtk4ThemeCssPath() ) except Exception as err: showErrorDialog("Error: Could not apply settings", str(err)) diff --git a/src/window.ui b/src/window.ui index 8c1f486..81ed7e0 100644 --- a/src/window.ui +++ b/src/window.ui @@ -248,7 +248,7 @@ True False - Instantaneous theme changing is supported. This means that setting another theme for GTK2 or GTK4 won't have any effect. + Instantaneous theme changing is supported. This means that setting another theme for GTK2 won't have any effect. True