Skip to content

Commit 1c21371

Browse files
authored
feat(themes): Implement categorized theme menus and debug tools (#12905)
* feat(themes): Implement categorized theme menus and debug tools - Organizes themes into distinct categories (Mint-X, Mint-Y, etc.) with labeled separators - Groups theme variants (Light/Dark/Darker) within each category - Improves menu readability by fixing gray separator background issues - Enhances theme selection UX with logical grouping and clear visual hierarchy * feat(themes): Enhance theme display with categorized and uncategorized grouping - Implemented grouping of single-item categories under 'Other Themes' for improved UI clarity - Added translation support for 'Other Themes' label - Ensured themes are sorted by variant order: Light, Darker, Dark - Removed unnecessary separators between variants of the same category
1 parent 69f154b commit 1c21371

File tree

2 files changed

+244
-16
lines changed

2 files changed

+244
-16
lines changed

files/usr/share/cinnamon/cinnamon-settings/bin/ChooserButtonWidgets.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,30 @@ def add_picture(self, path, callback, title=None, id=None):
197197
if self.col == 0:
198198
self.row = self.row + 1
199199

200-
def add_separator(self):
200+
def add_separator(self, label_text=None):
201201
self.row = self.row + 1
202-
self.menu.attach(Gtk.SeparatorMenuItem(), 0, self.num_cols, self.row, self.row+1)
202+
203+
# Always use a MenuItem with label (or blank) instead of SeparatorMenuItem
204+
# to avoid gray background issues
205+
item = Gtk.MenuItem()
206+
item.set_sensitive(False)
207+
208+
if label_text is not None:
209+
# Add centered, bold label
210+
label = Gtk.Label()
211+
label.set_markup(f"<b>{label_text}</b>")
212+
label.set_halign(Gtk.Align.CENTER)
213+
item.add(label)
214+
else:
215+
# Add an empty label to create space without gray background
216+
label = Gtk.Label()
217+
label.set_text(" ")
218+
item.add(label)
219+
220+
self.menu.attach(item, 0, self.num_cols, self.row, self.row+1)
221+
222+
# Reset column position after separator
223+
self.col = 0
203224

204225
def add_menuitem(self, menuitem):
205226
self.row = self.row + 1

files/usr/share/cinnamon/cinnamon-settings/modules/cs_themes.py

Lines changed: 221 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ def reset_look_ui(self):
525525
self.style_combo.set_active(len(self.styles.keys()))
526526
self.ui_ready = True
527527

528-
def on_customize_button_clicked(self, button):
528+
def on_customize_button_clicked(self, widget):
529529
self.set_button_chooser(self.icon_chooser, self.settings.get_string("icon-theme"), 'icons', 'icons', ICON_SIZE)
530530
self.set_button_chooser(self.cursor_chooser, self.settings.get_string("cursor-theme"), 'icons', 'cursors', 32)
531531
self.set_button_chooser(self.theme_chooser, self.settings.get_string("gtk-theme"), 'themes', 'gtk-3.0', 35)
@@ -640,11 +640,60 @@ def refresh_choosers(self):
640640
self.refresh_chooser(chooser, path_suffix, themes, callback)
641641
self.refreshing = False
642642

643+
def get_theme_category(self, theme_name, theme_type):
644+
parts = theme_name.split('-')
645+
646+
if theme_type == 'cursor':
647+
# For cursors - first part of the name
648+
return (parts[0], "Light")
649+
650+
elif theme_type in ['gtk', 'icon']:
651+
# Basic category is always the first part of the name
652+
base_category = parts[0]
653+
654+
# Exception: if the second part is a single letter, it's part of the category (e.g., Mint-X, Mint-Y)
655+
if len(parts) >= 2 and len(parts[1]) == 1:
656+
base_category = f"{parts[0]}-{parts[1]}"
657+
658+
# Determine variant (light/dark/darker)
659+
theme_lower = theme_name.lower()
660+
if 'darker' in theme_lower:
661+
variant = "Darker"
662+
elif 'dark' in theme_lower:
663+
variant = "Dark"
664+
else:
665+
variant = "Light"
666+
667+
return (base_category, variant)
668+
669+
elif theme_type == 'cinnamon':
670+
# For desktop - basic category is the first part of the name
671+
base_category = parts[0]
672+
673+
# Exception: if the second part is a single letter, it's part of the category
674+
if len(parts) >= 2 and len(parts[1]) == 1:
675+
base_category = f"{parts[0]}-{parts[1]}"
676+
677+
# Determine variant
678+
theme_lower = theme_name.lower()
679+
if 'darker' in theme_lower:
680+
variant = "Darker"
681+
elif 'dark' in theme_lower:
682+
variant = "Dark"
683+
else:
684+
variant = "Light"
685+
686+
return (base_category, variant)
687+
688+
return ("Other", "Light")
689+
643690
def refresh_chooser(self, chooser, path_suffix, themes, callback):
644691
inc = 1.0
645692
if len(themes) > 0:
646693
inc = 1.0 / len(themes)
647694

695+
variant_sort_order = {"Light": 0, "Darker": 1, "Dark": 2}
696+
648697
if path_suffix == 'icons':
649698
cache_folder = GLib.get_user_cache_dir() + '/cs_themes/'
650699
icon_cache_path = os.path.join(cache_folder, 'icons')
@@ -662,9 +711,95 @@ def refresh_chooser(self, chooser, path_suffix, themes, callback):
662711
icon_paths[theme_name] = icon_path
663712

664713
dump = False
714+
715+
# Collect all themes with their categories and variants
716+
categorized_themes = []
665717
for theme in themes:
666-
theme_path = None
718+
category, variant = self.get_theme_category(theme, 'icon')
719+
categorized_themes.append({'name': theme, 'category': category, 'variant': variant})
720+
721+
# Count themes per category
722+
category_counts = {}
723+
for theme_info in categorized_themes:
724+
category_counts[theme_info['category']] = category_counts.get(theme_info['category'], 0) + 1
725+
726+
# Separate single-item categories
727+
single_item_category_themes = []
728+
multi_item_category_themes = []
729+
730+
for theme_info in categorized_themes:
731+
if category_counts[theme_info['category']] == 1:
732+
single_item_category_themes.append(theme_info)
733+
else:
734+
multi_item_category_themes.append(theme_info)
735+
736+
# Sort both lists
737+
single_item_category_themes.sort(key=lambda x: (variant_sort_order.get(x['variant'], 3), x['name']))
738+
multi_item_category_themes.sort(key=lambda x: (x['category'], variant_sort_order.get(x['variant'], 3), x['name']))
739+
740+
# Reset column position at the start
741+
chooser.col = 0
742+
chooser.row = 0
743+
744+
# Display single-item category themes first, under an "Other Themes" label
745+
if single_item_category_themes:
746+
chooser.add_separator(_("Other Themes"))
747+
# Add an additional blank separator after the category label
748+
chooser.add_separator()
749+
for theme_info in single_item_category_themes:
750+
theme = theme_info['name']
751+
theme_path = None
752+
if theme in icon_paths:
753+
for theme_folder in ICON_FOLDERS:
754+
possible_path = os.path.join(theme_folder, icon_paths[theme])
755+
if os.path.exists(possible_path):
756+
theme_path = possible_path
757+
break
758+
759+
if theme_path is None:
760+
icon_theme = Gtk.IconTheme()
761+
icon_theme.set_custom_theme(theme)
762+
folder = icon_theme.lookup_icon('folder', ICON_SIZE, Gtk.IconLookupFlags.FORCE_SVG)
763+
if folder:
764+
theme_path = folder.get_filename()
765+
for theme_folder in ICON_FOLDERS:
766+
if os.path.commonpath([theme_folder, theme_path]) == theme_folder:
767+
icon_paths[theme] = os.path.relpath(theme_path, start=theme_folder)
768+
break
769+
dump = True
770+
771+
if theme_path is None:
772+
continue
667773

774+
if os.path.exists(theme_path):
775+
chooser.add_picture(theme_path, callback, title=theme, id=theme)
776+
GLib.timeout_add(5, self.increment_progress, (chooser, inc))
777+
778+
# Add a blank separator if both single and multi-item categories exist
779+
if single_item_category_themes and multi_item_category_themes:
780+
chooser.add_separator()
781+
782+
current_category = None
783+
current_variant = None
784+
# Display multi-item category themes
785+
for theme_info in multi_item_category_themes:
786+
category = theme_info['category']
787+
variant = theme_info['variant']
788+
theme = theme_info['name']
789+
790+
if current_category != category:
791+
if current_category is not None:
792+
chooser.add_separator() # Blank separator between categories
793+
current_category = category
794+
current_variant = None # Reset variant when category changes
795+
chooser.add_separator(category) # Category name separator
796+
# Add an additional blank separator after the category label
797+
chooser.add_separator()
798+
799+
# Update current_variant, no separator here if it's just a variant change
800+
current_variant = variant
801+
802+
theme_path = None
668803
if theme in icon_paths:
669804
# loop through all possible locations until we find a match
670805
# (user folders should override system ones)
@@ -680,14 +815,12 @@ def refresh_chooser(self, chooser, path_suffix, themes, callback):
680815
folder = icon_theme.lookup_icon('folder', ICON_SIZE, Gtk.IconLookupFlags.FORCE_SVG)
681816
if folder:
682817
theme_path = folder.get_filename()
683-
684818
# we need to get the relative path for storage
685819
for theme_folder in ICON_FOLDERS:
686820
if os.path.commonpath([theme_folder, theme_path]) == theme_folder:
687821
icon_paths[theme] = os.path.relpath(theme_path, start=theme_folder)
688822
break
689-
690-
dump = True
823+
dump = True
691824

692825
if theme_path is None:
693826
continue
@@ -701,24 +834,98 @@ def refresh_chooser(self, chooser, path_suffix, themes, callback):
701834
os.mkdir(cache_folder)
702835

703836
with open(icon_cache_path, 'w') as cache_file:
704-
for theme_name, icon_path in icon_paths.items():
705-
cache_file.write('%s:%s\n' % (theme_name, icon_path))
837+
for theme_name, icon_path_val in icon_paths.items(): # Renamed icon_path to avoid conflict
838+
cache_file.write('%s:%s\\n' % (theme_name, icon_path_val))
706839

707840
else:
708841
if path_suffix == "cinnamon":
709842
chooser.add_picture("/usr/share/cinnamon/theme/thumbnail.png", callback, title="cinnamon", id="cinnamon")
710843
if path_suffix in ["gtk-3.0", "cinnamon"]:
711-
themes = sorted(themes, key=lambda t: (not t[1].startswith(GLib.get_home_dir())))
844+
# Sort themes by user-installed first, then alphabetically
845+
themes = sorted(themes, key=lambda t: (not t[1].startswith(GLib.get_home_dir()), t[0].lower()))
846+
847+
848+
# Collect all themes with their categories and variants
849+
categorized_themes = []
850+
for theme_data in themes: # theme_data is a tuple (name, path)
851+
name = theme_data[0]
852+
path = theme_data[1]
853+
theme_type = 'gtk' if path_suffix == 'gtk-3.0' else 'cinnamon'
854+
category, variant = self.get_theme_category(name, theme_type)
855+
categorized_themes.append({'name': name, 'path': path, 'category': category, 'variant': variant, 'original_tuple': theme_data})
856+
857+
# Count themes per category
858+
category_counts = {}
859+
for theme_info in categorized_themes:
860+
category_counts[theme_info['category']] = category_counts.get(theme_info['category'], 0) + 1
861+
862+
single_item_category_themes = []
863+
multi_item_category_themes = []
864+
865+
for theme_info in categorized_themes:
866+
if category_counts[theme_info['category']] == 1:
867+
single_item_category_themes.append(theme_info)
868+
else:
869+
multi_item_category_themes.append(theme_info)
870+
871+
# Sort both lists
872+
single_item_category_themes.sort(key=lambda x: (variant_sort_order.get(x['variant'], 3), x['name'].lower()))
873+
multi_item_category_themes.sort(key=lambda x: (x['category'].lower(), variant_sort_order.get(x['variant'], 3), x['name'].lower()))
874+
875+
# Reset column position at the start
876+
chooser.col = 0
877+
chooser.row = 0
878+
879+
# Display single-item category themes first, under an "Other Themes" label
880+
if single_item_category_themes:
881+
chooser.add_separator(_("Other Themes"))
882+
# Add an additional blank separator after the category label
883+
chooser.add_separator()
884+
for theme_info in single_item_category_themes:
885+
theme_name = theme_info['name']
886+
theme_path_val = theme_info['path'] # Renamed theme_path to avoid conflict
887+
try:
888+
for path_option in ["%s/%s/%s/thumbnail.png" % (theme_path_val, theme_name, path_suffix),
889+
"/usr/share/cinnamon/thumbnails/%s/%s.png" % (path_suffix, theme_name),
890+
"/usr/share/cinnamon/thumbnails/%s/unknown.png" % path_suffix]:
891+
if os.path.exists(path_option):
892+
chooser.add_picture(path_option, callback, title=theme_name, id=theme_name)
893+
break
894+
except:
895+
chooser.add_picture("/usr/share/cinnamon/thumbnails/%s/unknown.png" % path_suffix, callback, title=theme_name, id=theme_name)
896+
GLib.timeout_add(5, self.increment_progress, (chooser, inc))
897+
898+
# Add a blank separator if both single and multi-item categories exist
899+
if single_item_category_themes and multi_item_category_themes:
900+
chooser.add_separator()
901+
902+
current_category = None
903+
current_variant = None
904+
# Display multi-item category themes
905+
for theme_info in multi_item_category_themes:
906+
category = theme_info['category']
907+
variant = theme_info['variant']
908+
theme_name = theme_info['name']
909+
theme_path_val = theme_info['path'] # Renamed theme_path to avoid conflict
910+
911+
if current_category != category:
912+
if current_category is not None:
913+
chooser.add_separator() # Blank separator between categories
914+
current_category = category
915+
current_variant = None # Reset variant when category changes
916+
chooser.add_separator(category) # Category name separator
917+
# Add an additional blank separator after the category label
918+
chooser.add_separator()
919+
920+
# Update current_variant, no separator here if it's just a variant change
921+
current_variant = variant
712922

713-
for theme in themes:
714-
theme_name = theme[0]
715-
theme_path = theme[1]
716923
try:
717-
for path in ["%s/%s/%s/thumbnail.png" % (theme_path, theme_name, path_suffix),
924+
for path_option in ["%s/%s/%s/thumbnail.png" % (theme_path_val, theme_name, path_suffix),
718925
"/usr/share/cinnamon/thumbnails/%s/%s.png" % (path_suffix, theme_name),
719926
"/usr/share/cinnamon/thumbnails/%s/unknown.png" % path_suffix]:
720-
if os.path.exists(path):
721-
chooser.add_picture(path, callback, title=theme_name, id=theme_name)
927+
if os.path.exists(path_option):
928+
chooser.add_picture(path_option, callback, title=theme_name, id=theme_name)
722929
break
723930
except:
724931
chooser.add_picture("/usr/share/cinnamon/thumbnails/%s/unknown.png" % path_suffix, callback, title=theme_name, id=theme_name)

0 commit comments

Comments
 (0)