cinnamon: Add patches to accelerate settings
Signed-off-by: Andreas Müller <schnitzeltony@gmail.com>
This commit is contained in:
@@ -29,6 +29,12 @@ inherit meson pkgconfig gobject-introspection gtk-icon-cache gsettings gtk-doc g
|
|||||||
SRC_URI = " \
|
SRC_URI = " \
|
||||||
git://github.com/linuxmint/cinnamon.git;branch=master;protocol=https \
|
git://github.com/linuxmint/cinnamon.git;branch=master;protocol=https \
|
||||||
file://0001-Do-not-crash-on-systemd-reporting-Univeral-timezone.patch \
|
file://0001-Do-not-crash-on-systemd-reporting-Univeral-timezone.patch \
|
||||||
|
file://okaestne-settings-performance/0001-ExtensionCore-defer-loading-of-cinnamon-version-fix-.patch \
|
||||||
|
file://okaestne-settings-performance/0002-cs_privacy-defer-init-of-NM.Client.patch \
|
||||||
|
file://okaestne-settings-performance/0003-cs_backgrounds-defer-import-of-imtools-module.patch \
|
||||||
|
file://okaestne-settings-performance/0004-Spices-defer-import-of-requests-module.patch \
|
||||||
|
file://okaestne-settings-performance/0005-cs-fix-print_timing-remove-stale-touch-function.patch \
|
||||||
|
file://okaestne-settings-performance/0006-cs-lazy-load-python-modules-when-passed-as-arg.patch \
|
||||||
"
|
"
|
||||||
SRCREV = "037b17248b176c7f3dd5c9848f8c6738105c4cc2"
|
SRCREV = "037b17248b176c7f3dd5c9848f8c6738105c4cc2"
|
||||||
PV = "5.2.7+git${SRCPV}"
|
PV = "5.2.7+git${SRCPV}"
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
From 03599325d88f453b464f8c500fea0e758dd6a386 Mon Sep 17 00:00:00 2001
|
||||||
|
From: okaestne <git@oliver-kaestner.de>
|
||||||
|
Date: Sun, 3 Apr 2022 15:39:36 +0200
|
||||||
|
Subject: [PATCH 1/6] ExtensionCore: defer loading of cinnamon version & fix
|
||||||
|
comparison
|
||||||
|
|
||||||
|
* saves ~15ms at import time
|
||||||
|
* fix: comparsion was able to fail because of string comparision
|
||||||
|
e.g `['5', '8'] > ['5', '10']` is `True`
|
||||||
|
---
|
||||||
|
.../cinnamon-settings/bin/ExtensionCore.py | 34 ++++++++++---------
|
||||||
|
1 file changed, 18 insertions(+), 16 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py b/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py
|
||||||
|
index 72d124c28..e616b08e1 100644
|
||||||
|
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py
|
||||||
|
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/ExtensionCore.py
|
||||||
|
@@ -1,5 +1,6 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
+from functools import lru_cache
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import html
|
||||||
|
@@ -30,20 +31,22 @@ ROW_SIZE = 32
|
||||||
|
|
||||||
|
UNSAFE_ITEMS = ['spawn_sync', 'spawn_command_line_sync', 'GTop', 'get_file_contents_utf8_sync']
|
||||||
|
|
||||||
|
-curr_ver = subprocess.check_output(['cinnamon', '--version']).decode("utf-8").splitlines()[0].split(' ')[1]
|
||||||
|
-curr_ver_elements = curr_ver.split(".")
|
||||||
|
-curr_ver_major = int(curr_ver_elements[0])
|
||||||
|
-curr_ver_minor = int(curr_ver_elements[1])
|
||||||
|
-
|
||||||
|
LANGUAGE_CODE = "C"
|
||||||
|
try:
|
||||||
|
LANGUAGE_CODE = locale.getlocale()[0].split("_")[0]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
+
|
||||||
|
+@lru_cache(maxsize=None) # fetch only once
|
||||||
|
+def get_cinnamon_version():
|
||||||
|
+ version_str = subprocess.check_output(['cinnamon', '--version'], encoding="utf-8").split()[1]
|
||||||
|
+ return [int(part) for part in version_str.split(".")]
|
||||||
|
+
|
||||||
|
+
|
||||||
|
def find_extension_subdir(directory):
|
||||||
|
- largest = ['0']
|
||||||
|
- curr_a = curr_ver.split('.')
|
||||||
|
+ largest = [0]
|
||||||
|
+ curr_a = get_cinnamon_version()
|
||||||
|
|
||||||
|
for subdir in os.listdir(directory):
|
||||||
|
if not os.path.isdir(os.path.join(directory, subdir)):
|
||||||
|
@@ -52,15 +55,15 @@ def find_extension_subdir(directory):
|
||||||
|
if not re.match(r'^[1-9][0-9]*\.[0-9]+(\.[0-9]+)?$', subdir):
|
||||||
|
continue
|
||||||
|
|
||||||
|
- subdir_a = subdir.split(".")
|
||||||
|
+ subdir_a = [int(part) for part in subdir.split(".")]
|
||||||
|
|
||||||
|
- if subdir_a < curr_a and largest < subdir_a:
|
||||||
|
+ if subdir_a <= curr_a and largest < subdir_a:
|
||||||
|
largest = subdir_a
|
||||||
|
|
||||||
|
- if len(largest) == 1:
|
||||||
|
+ if largest == [0]:
|
||||||
|
return directory
|
||||||
|
else:
|
||||||
|
- return os.path.join(directory, ".".join(largest))
|
||||||
|
+ return os.path.join(directory, ".".join(map(str, largest)))
|
||||||
|
|
||||||
|
translations = {}
|
||||||
|
|
||||||
|
@@ -315,11 +318,11 @@ class ManageSpicesRow(Gtk.ListBoxRow):
|
||||||
|
try:
|
||||||
|
# Treat "cinnamon-version" as a list of minimum required versions
|
||||||
|
# if any version in there is lower than our Cinnamon version, then the spice is compatible.
|
||||||
|
+ curr_ver = get_cinnamon_version()
|
||||||
|
+
|
||||||
|
for version in self.metadata['cinnamon-version']:
|
||||||
|
- elements = version.split(".")
|
||||||
|
- major = int(elements[0])
|
||||||
|
- minor = int(elements[1])
|
||||||
|
- if curr_ver_major > major or (curr_ver_major == major and curr_ver_minor >= minor):
|
||||||
|
+ spice_ver = [int(part) for part in version.split(".")]
|
||||||
|
+ if spice_ver[:2] <= curr_ver:
|
||||||
|
# The version is OK, check that we can find the right .js file in the appropriate subdir
|
||||||
|
path = os.path.join(self.metadata['path'], self.extension_type + ".js")
|
||||||
|
if os.path.exists(path):
|
||||||
|
@@ -327,7 +330,6 @@ class ManageSpicesRow(Gtk.ListBoxRow):
|
||||||
|
else:
|
||||||
|
print ("The %s %s is not properly structured. Path not found: '%s'" % (self.uuid, self.extension_type, path))
|
||||||
|
return False
|
||||||
|
- return True
|
||||||
|
print ("The %s %s is not compatible with this version of Cinnamon." % (self.uuid, self.extension_type))
|
||||||
|
return False
|
||||||
|
except:
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
From b20036b87aa78242e11b4e5f023b4f8586cfe1d9 Mon Sep 17 00:00:00 2001
|
||||||
|
From: okaestne <git@oliver-kaestner.de>
|
||||||
|
Date: Sun, 3 Apr 2022 17:49:43 +0200
|
||||||
|
Subject: [PATCH 2/6] cs_privacy: defer init of NM.Client
|
||||||
|
|
||||||
|
saves ~10ms import time
|
||||||
|
|
||||||
|
also fix some imports
|
||||||
|
---
|
||||||
|
.../cinnamon-settings/modules/cs_privacy.py | 43 +++++++++++--------
|
||||||
|
1 file changed, 24 insertions(+), 19 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py
|
||||||
|
index a726b0776..26693b230 100755
|
||||||
|
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py
|
||||||
|
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_privacy.py
|
||||||
|
@@ -1,26 +1,16 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
-
|
||||||
|
-nm_client = None
|
||||||
|
-try:
|
||||||
|
- import gi
|
||||||
|
- gi.require_version('NM', '1.0')
|
||||||
|
- from gi.repository import NM
|
||||||
|
- nm_client = NM.Client.new(None)
|
||||||
|
- # call connectivity_check_get_available to make
|
||||||
|
- # sure it's available (it's new in libnm 1.10)
|
||||||
|
- # if it's not, we catch the exception and set
|
||||||
|
- # the client to None
|
||||||
|
- nm_client.connectivity_check_get_available()
|
||||||
|
-except:
|
||||||
|
- nm_client = None
|
||||||
|
+import gi
|
||||||
|
+gi.require_version('Gtk', '3.0')
|
||||||
|
+from gi.repository import Gio, Gtk
|
||||||
|
|
||||||
|
from SettingsWidgets import SidePage
|
||||||
|
-from xapp.GSettingsWidgets import *
|
||||||
|
+from xapp.GSettingsWidgets import GSettingsSwitch, SettingsLabel, SettingsPage, SettingsRevealer, SettingsWidget, Switch
|
||||||
|
|
||||||
|
PRIVACY_SCHEMA = "org.cinnamon.desktop.privacy"
|
||||||
|
GTK_RECENT_ENABLE_KEY = "remember-recent-files"
|
||||||
|
GTK_RECENT_MAX_AGE = "recent-files-max-age"
|
||||||
|
|
||||||
|
+
|
||||||
|
class Module:
|
||||||
|
name = "privacy"
|
||||||
|
comment = _("Cinnamon privacy settings")
|
||||||
|
@@ -31,6 +21,19 @@ class Module:
|
||||||
|
sidePage = SidePage(_("Privacy"), "cs-privacy", keywords, content_box, module=self)
|
||||||
|
self.sidePage = sidePage
|
||||||
|
self.settings = Gio.Settings(schema=PRIVACY_SCHEMA)
|
||||||
|
+ self.nm_client = None
|
||||||
|
+
|
||||||
|
+ def _init_nm_client(self):
|
||||||
|
+ try:
|
||||||
|
+ gi.require_version('NM', '1.0')
|
||||||
|
+ from gi.repository import NM
|
||||||
|
+ nm_client = NM.Client.new()
|
||||||
|
+
|
||||||
|
+ # we need libnm >=1.10
|
||||||
|
+ if hasattr(nm_client, 'connectivity_check_get_available'):
|
||||||
|
+ self.nm_client = nm_client
|
||||||
|
+ except ValueError:
|
||||||
|
+ pass
|
||||||
|
|
||||||
|
def on_module_selected(self):
|
||||||
|
if not self.loaded:
|
||||||
|
@@ -77,14 +80,16 @@ class Module:
|
||||||
|
else:
|
||||||
|
self.indefinite_switch.content_widget.set_active(False)
|
||||||
|
self.revealer.set_reveal_child(True)
|
||||||
|
- if start_age == 0: # Shouldn't happen, unless someone manually sets the value
|
||||||
|
+ if start_age == 0: # Shouldn't happen, unless someone manually sets the value
|
||||||
|
self.settings.set_int(GTK_RECENT_MAX_AGE, 30)
|
||||||
|
self.bind_spinner()
|
||||||
|
|
||||||
|
- if nm_client is not None and nm_client.connectivity_check_get_available():
|
||||||
|
+ self._init_nm_client()
|
||||||
|
+
|
||||||
|
+ if self.nm_client is not None and self.nm_client.connectivity_check_get_available():
|
||||||
|
section = page.add_section(_("Internet connectivity"))
|
||||||
|
connectivity_switch = Switch(_("Check connectivity"))
|
||||||
|
- connectivity_switch.content_widget.set_active(nm_client.connectivity_check_get_enabled())
|
||||||
|
+ connectivity_switch.content_widget.set_active(self.nm_client.connectivity_check_get_enabled())
|
||||||
|
connectivity_switch.content_widget.connect("notify::active", self.on_connectivity_toggled)
|
||||||
|
section.add_row(connectivity_switch)
|
||||||
|
section.add_note(_("Check that network connections can reach the Internet. This makes it possible to detect captive portals, but also generates periodic network traffic."))
|
||||||
|
@@ -117,4 +122,4 @@ class Module:
|
||||||
|
|
||||||
|
def on_connectivity_toggled(self, widget, gparam):
|
||||||
|
active = widget.get_active()
|
||||||
|
- nm_client.connectivity_check_set_enabled(active)
|
||||||
|
+ self.nm_client.connectivity_check_set_enabled(active)
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
From b7f582e2500959e89c56837675cfe454892846ce Mon Sep 17 00:00:00 2001
|
||||||
|
From: okaestne <git@oliver-kaestner.de>
|
||||||
|
Date: Sun, 3 Apr 2022 23:59:22 +0200
|
||||||
|
Subject: [PATCH 3/6] cs_backgrounds: defer import of imtools module
|
||||||
|
|
||||||
|
saves ~55ms import time
|
||||||
|
---
|
||||||
|
.../share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py | 3 ++-
|
||||||
|
1 file changed, 2 insertions(+), 1 deletion(-)
|
||||||
|
|
||||||
|
diff --git a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py
|
||||||
|
index c462177b7..5d772191b 100755
|
||||||
|
--- a/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py
|
||||||
|
+++ b/files/usr/share/cinnamon/cinnamon-settings/modules/cs_backgrounds.py
|
||||||
|
@@ -1,7 +1,6 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
-import imtools
|
||||||
|
import gettext
|
||||||
|
import _thread as thread
|
||||||
|
import subprocess
|
||||||
|
@@ -623,6 +622,8 @@ class PixCache(object):
|
||||||
|
img = img.convert("RGB")
|
||||||
|
if size:
|
||||||
|
img.thumbnail((size, size), Image.ANTIALIAS)
|
||||||
|
+
|
||||||
|
+ import imtools
|
||||||
|
img = imtools.round_image(img, {}, False, None, 3, 255)
|
||||||
|
img = imtools.drop_shadow(img, 4, 4, background_color=(255, 255, 255, 0),
|
||||||
|
shadow_color=0x444444, border=8, shadow_blur=3,
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
From 1358b9df2b18fb7ecb5077cdf54b427cf47e26ac Mon Sep 17 00:00:00 2001
|
||||||
|
From: okaestne <git@oliver-kaestner.de>
|
||||||
|
Date: Mon, 4 Apr 2022 00:55:26 +0200
|
||||||
|
Subject: [PATCH 4/6] Spices: defer import of requests module
|
||||||
|
|
||||||
|
saves ~25ms import time
|
||||||
|
---
|
||||||
|
files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py | 5 +++--
|
||||||
|
1 file changed, 3 insertions(+), 2 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py
|
||||||
|
index f345243f3..348d9488d 100644
|
||||||
|
--- a/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py
|
||||||
|
+++ b/files/usr/share/cinnamon/cinnamon-settings/bin/Spices.py
|
||||||
|
@@ -12,9 +12,7 @@ try:
|
||||||
|
import threading
|
||||||
|
from PIL import Image
|
||||||
|
import datetime
|
||||||
|
- import proxygsettings
|
||||||
|
import time
|
||||||
|
- import requests
|
||||||
|
except Exception as detail:
|
||||||
|
print(detail)
|
||||||
|
sys.exit(1)
|
||||||
|
@@ -382,6 +380,9 @@ class Spice_Harvester(GObject.Object):
|
||||||
|
#Like the one in urllib. Unlike urllib.retrieve url_retrieve
|
||||||
|
#can be interrupted. KeyboardInterrupt exception is raised when
|
||||||
|
#interrupted.
|
||||||
|
+ import proxygsettings
|
||||||
|
+ import requests
|
||||||
|
+
|
||||||
|
count = 0
|
||||||
|
blockSize = 1024 * 8
|
||||||
|
proxy_info = proxygsettings.get_proxy_settings()
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
From 0aa0e62dd49e74253d132c7bd9a900af03bbc421 Mon Sep 17 00:00:00 2001
|
||||||
|
From: okaestne <git@oliver-kaestner.de>
|
||||||
|
Date: Sat, 9 Apr 2022 18:50:32 +0200
|
||||||
|
Subject: [PATCH 5/6] cs: fix print_timing; remove stale touch function
|
||||||
|
|
||||||
|
---
|
||||||
|
.../cinnamon/cinnamon-settings/cinnamon-settings.py | 11 +++--------
|
||||||
|
1 file changed, 3 insertions(+), 8 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py b/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
|
||||||
|
index 65d4a3028..7dee496e0 100755
|
||||||
|
--- a/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
|
||||||
|
+++ b/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
|
||||||
|
@@ -152,20 +152,15 @@ ARG_REWRITE = {
|
||||||
|
|
||||||
|
def print_timing(func):
|
||||||
|
# decorate functions with @print_timing to output how long they take to run.
|
||||||
|
- def wrapper(*arg):
|
||||||
|
+ def wrapper(*args, **kwargs):
|
||||||
|
t1 = time.time()
|
||||||
|
- res = func(*arg)
|
||||||
|
+ res = func(*args, **kwargs)
|
||||||
|
t2 = time.time()
|
||||||
|
- print('%s took %0.3f ms' % (func.func_name, (t2-t1)*1000.0))
|
||||||
|
+ print('%s took %0.3f ms' % (func.__name__, (t2-t1)*1000.0))
|
||||||
|
return res
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
-def touch(fname, times=None):
|
||||||
|
- with open(fname, 'a'):
|
||||||
|
- os.utime(fname, times)
|
||||||
|
-
|
||||||
|
-
|
||||||
|
class MainWindow(Gio.Application):
|
||||||
|
# Change pages
|
||||||
|
def side_view_nav(self, side_view, path, cat):
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,489 @@
|
|||||||
|
From 7215d33cfb9a4fecc4dcf94ce4fd3d757d1bcf30 Mon Sep 17 00:00:00 2001
|
||||||
|
From: okaestne <git@oliver-kaestner.de>
|
||||||
|
Date: Sat, 9 Apr 2022 22:52:05 +0200
|
||||||
|
Subject: [PATCH 6/6] cs: lazy load python modules, when passed as arg
|
||||||
|
|
||||||
|
---
|
||||||
|
.../cinnamon-settings/cinnamon-settings.py | 358 ++++++++++--------
|
||||||
|
1 file changed, 204 insertions(+), 154 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py b/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
|
||||||
|
index 7dee496e0..41746c16f 100755
|
||||||
|
--- a/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
|
||||||
|
+++ b/files/usr/share/cinnamon/cinnamon-settings/cinnamon-settings.py
|
||||||
|
@@ -1,56 +1,47 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
-
|
||||||
|
-import getopt
|
||||||
|
-import sys
|
||||||
|
-
|
||||||
|
from bin import util
|
||||||
|
util.strip_syspath_locals()
|
||||||
|
|
||||||
|
-import os
|
||||||
|
-import glob
|
||||||
|
+from functools import cmp_to_key
|
||||||
|
+import getopt
|
||||||
|
import gettext
|
||||||
|
+import glob
|
||||||
|
+import locale
|
||||||
|
+import os
|
||||||
|
+from setproctitle import setproctitle
|
||||||
|
+import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
-import locale
|
||||||
|
-import urllib.request as urllib
|
||||||
|
-from functools import cmp_to_key
|
||||||
|
+import typing
|
||||||
|
import unicodedata
|
||||||
|
-import config
|
||||||
|
-from setproctitle import setproctitle
|
||||||
|
+import urllib.request as urllib
|
||||||
|
|
||||||
|
import gi
|
||||||
|
gi.require_version('Gtk', '3.0')
|
||||||
|
gi.require_version('XApp', '1.0')
|
||||||
|
from gi.repository import Gio, Gtk, Pango, Gdk, XApp
|
||||||
|
|
||||||
|
-sys.path.append(config.currentPath + "/modules")
|
||||||
|
-sys.path.append(config.currentPath + "/bin")
|
||||||
|
-import capi
|
||||||
|
-import proxygsettings
|
||||||
|
-import SettingsWidgets
|
||||||
|
+import config
|
||||||
|
+sys.path.append(os.path.join(config.currentPath, "bin"))
|
||||||
|
+sys.path.append(os.path.join(config.currentPath, "modules"))
|
||||||
|
+from bin import capi
|
||||||
|
+from bin import proxygsettings
|
||||||
|
+from bin import SettingsWidgets
|
||||||
|
|
||||||
|
# i18n
|
||||||
|
gettext.install("cinnamon", "/usr/share/locale", names=["ngettext"])
|
||||||
|
|
||||||
|
-# Standard setting pages... this can be expanded to include applet dirs maybe?
|
||||||
|
-mod_files = glob.glob(config.currentPath + "/modules/*.py")
|
||||||
|
-mod_files.sort()
|
||||||
|
-if len(mod_files) == 0:
|
||||||
|
- print("No settings modules found!!")
|
||||||
|
- sys.exit(1)
|
||||||
|
-
|
||||||
|
-mod_files = [x.split('/')[-1].split('.')[0] for x in mod_files]
|
||||||
|
-
|
||||||
|
-for mod_file in mod_files:
|
||||||
|
- if mod_file[0:3] != "cs_":
|
||||||
|
- raise Exception("Settings modules must have a prefix of 'cs_' !!")
|
||||||
|
-
|
||||||
|
-modules = map(__import__, mod_files)
|
||||||
|
-
|
||||||
|
# i18n for menu item
|
||||||
|
menuName = _("System Settings")
|
||||||
|
menuComment = _("Control Center")
|
||||||
|
|
||||||
|
+
|
||||||
|
+class SidePageData(typing.NamedTuple):
|
||||||
|
+ sp: SettingsWidgets.SidePage
|
||||||
|
+ name: str
|
||||||
|
+ cat: str
|
||||||
|
+
|
||||||
|
+
|
||||||
|
WIN_WIDTH = 800
|
||||||
|
WIN_HEIGHT = 600
|
||||||
|
WIN_H_PADDING = 20
|
||||||
|
@@ -169,7 +160,9 @@ class MainWindow(Gio.Application):
|
||||||
|
self.deselect(cat)
|
||||||
|
filtered_path = side_view.get_model().convert_path_to_child_path(selected_items[0])
|
||||||
|
if filtered_path is not None:
|
||||||
|
- self.go_to_sidepage(cat, filtered_path, user_action=True)
|
||||||
|
+ iterator = self.store_by_cat[cat].get_iter(filtered_path)
|
||||||
|
+ sidePage = self.store_by_cat[cat].get_value(iterator, 2)
|
||||||
|
+ self.go_to_sidepage(sidePage, user_action=True)
|
||||||
|
|
||||||
|
def _on_sidepage_hide_stack(self):
|
||||||
|
self.stack_switcher.set_opacity(0)
|
||||||
|
@@ -177,57 +170,56 @@ class MainWindow(Gio.Application):
|
||||||
|
def _on_sidepage_show_stack(self):
|
||||||
|
self.stack_switcher.set_opacity(1)
|
||||||
|
|
||||||
|
- def go_to_sidepage(self, cat, path, user_action=True):
|
||||||
|
- iterator = self.store[cat].get_iter(path)
|
||||||
|
- sidePage = self.store[cat].get_value(iterator, 2)
|
||||||
|
- if not sidePage.is_standalone:
|
||||||
|
- if not user_action:
|
||||||
|
- self.window.set_title(sidePage.name)
|
||||||
|
- self.window.set_icon_name(sidePage.icon)
|
||||||
|
- sidePage.build()
|
||||||
|
- if sidePage.stack:
|
||||||
|
- self.stack_switcher.set_stack(sidePage.stack)
|
||||||
|
- l = sidePage.stack.get_children()
|
||||||
|
- if len(l) > 0:
|
||||||
|
- if self.tab in range(len(l)):
|
||||||
|
- sidePage.stack.set_visible_child(l[self.tab])
|
||||||
|
- visible_child = sidePage.stack.get_visible_child()
|
||||||
|
- if self.tab == 1 \
|
||||||
|
- and hasattr(visible_child, 'sort_combo') \
|
||||||
|
- and self.sort in range(5):
|
||||||
|
- visible_child.sort_combo.set_active(self.sort)
|
||||||
|
- visible_child.sort_changed()
|
||||||
|
- else:
|
||||||
|
- sidePage.stack.set_visible_child(l[0])
|
||||||
|
- if sidePage.stack.get_visible():
|
||||||
|
- self.stack_switcher.set_opacity(1)
|
||||||
|
- else:
|
||||||
|
- self.stack_switcher.set_opacity(0)
|
||||||
|
- if hasattr(sidePage, "connect_proxy"):
|
||||||
|
- sidePage.connect_proxy("hide_stack", self._on_sidepage_hide_stack)
|
||||||
|
- sidePage.connect_proxy("show_stack", self._on_sidepage_show_stack)
|
||||||
|
+ def go_to_sidepage(self, sidePage: SettingsWidgets.SidePage, user_action=True):
|
||||||
|
+ sidePage.build()
|
||||||
|
+
|
||||||
|
+ if sidePage.is_standalone:
|
||||||
|
+ return # we're done
|
||||||
|
+
|
||||||
|
+ if not user_action:
|
||||||
|
+ self.window.set_title(sidePage.name)
|
||||||
|
+ self.window.set_icon_name(sidePage.icon)
|
||||||
|
+
|
||||||
|
+ if sidePage.stack:
|
||||||
|
+ self.stack_switcher.set_stack(sidePage.stack)
|
||||||
|
+ l = sidePage.stack.get_children()
|
||||||
|
+ if len(l) > 0:
|
||||||
|
+ if self.tab in range(len(l)):
|
||||||
|
+ sidePage.stack.set_visible_child(l[self.tab])
|
||||||
|
+ visible_child = sidePage.stack.get_visible_child()
|
||||||
|
+ if self.tab == 1 \
|
||||||
|
+ and hasattr(visible_child, 'sort_combo') \
|
||||||
|
+ and self.sort in range(5):
|
||||||
|
+ visible_child.sort_combo.set_active(self.sort)
|
||||||
|
+ visible_child.sort_changed()
|
||||||
|
+ else:
|
||||||
|
+ sidePage.stack.set_visible_child(l[0])
|
||||||
|
+ if sidePage.stack.get_visible():
|
||||||
|
+ self.stack_switcher.set_opacity(1)
|
||||||
|
else:
|
||||||
|
self.stack_switcher.set_opacity(0)
|
||||||
|
+ if hasattr(sidePage, "connect_proxy"):
|
||||||
|
+ sidePage.connect_proxy("hide_stack", self._on_sidepage_hide_stack)
|
||||||
|
+ sidePage.connect_proxy("show_stack", self._on_sidepage_show_stack)
|
||||||
|
else:
|
||||||
|
self.stack_switcher.set_opacity(0)
|
||||||
|
+ else:
|
||||||
|
+ self.stack_switcher.set_opacity(0)
|
||||||
|
|
||||||
|
- if user_action:
|
||||||
|
- self.main_stack.set_visible_child_name("content_box_page")
|
||||||
|
- self.header_stack.set_visible_child_name("content_box")
|
||||||
|
-
|
||||||
|
- else:
|
||||||
|
- self.main_stack.set_visible_child_full("content_box_page", Gtk.StackTransitionType.NONE)
|
||||||
|
- self.header_stack.set_visible_child_full("content_box", Gtk.StackTransitionType.NONE)
|
||||||
|
-
|
||||||
|
- self.current_sidepage = sidePage
|
||||||
|
- width = 0
|
||||||
|
- for widget in self.top_bar:
|
||||||
|
- m, n = widget.get_preferred_width()
|
||||||
|
- width += n
|
||||||
|
- self.top_bar.set_size_request(width + 20, -1)
|
||||||
|
- self.maybe_resize(sidePage)
|
||||||
|
+ if user_action:
|
||||||
|
+ self.main_stack.set_visible_child_name("content_box_page")
|
||||||
|
+ self.header_stack.set_visible_child_name("content_box")
|
||||||
|
else:
|
||||||
|
- sidePage.build()
|
||||||
|
+ self.main_stack.set_visible_child_full("content_box_page", Gtk.StackTransitionType.NONE)
|
||||||
|
+ self.header_stack.set_visible_child_full("content_box", Gtk.StackTransitionType.NONE)
|
||||||
|
+
|
||||||
|
+ self.current_sidepage = sidePage
|
||||||
|
+ width = 0
|
||||||
|
+ for widget in self.top_bar:
|
||||||
|
+ m, n = widget.get_preferred_width()
|
||||||
|
+ width += n
|
||||||
|
+ self.top_bar.set_size_request(width + 20, -1)
|
||||||
|
+ self.maybe_resize(sidePage)
|
||||||
|
|
||||||
|
def maybe_resize(self, sidePage):
|
||||||
|
m, n = self.content_box.get_preferred_size()
|
||||||
|
@@ -260,7 +252,7 @@ class MainWindow(Gio.Application):
|
||||||
|
flags=Gio.ApplicationFlags.NON_UNIQUE | Gio.ApplicationFlags.HANDLES_OPEN)
|
||||||
|
self.builder = Gtk.Builder()
|
||||||
|
self.builder.set_translation_domain('cinnamon') # let it translate!
|
||||||
|
- self.builder.add_from_file(config.currentPath + "/cinnamon-settings.ui")
|
||||||
|
+ self.builder.add_from_file(os.path.join(config.currentPath, "cinnamon-settings.ui"))
|
||||||
|
self.window = XApp.GtkWindow(window_position=Gtk.WindowPosition.CENTER,
|
||||||
|
default_width=800, default_height=600)
|
||||||
|
|
||||||
|
@@ -298,8 +290,7 @@ class MainWindow(Gio.Application):
|
||||||
|
self.window.connect("destroy", self._quit)
|
||||||
|
|
||||||
|
self.builder.connect_signals(self)
|
||||||
|
- self.unsortedSidePages = []
|
||||||
|
- self.sidePages = []
|
||||||
|
+ self.sidePages: typing.List[SidePageData] = []
|
||||||
|
self.settings = Gio.Settings.new("org.cinnamon")
|
||||||
|
self.current_cat_widget = None
|
||||||
|
|
||||||
|
@@ -308,58 +299,61 @@ class MainWindow(Gio.Application):
|
||||||
|
self.content_box.c_manager = self.c_manager
|
||||||
|
self.bar_heights = 0
|
||||||
|
|
||||||
|
- for module in modules:
|
||||||
|
- try:
|
||||||
|
- mod = module.Module(self.content_box)
|
||||||
|
- if self.loadCheck(mod) and self.setParentRefs(mod):
|
||||||
|
- self.unsortedSidePages.append((mod.sidePage, mod.name, mod.category))
|
||||||
|
- except:
|
||||||
|
- print("Failed to load module %s" % module)
|
||||||
|
- traceback.print_exc()
|
||||||
|
+ self.tab = 0 # open 'manage' tab by default
|
||||||
|
+ self.sort = 1 # sorted by 'score' by default
|
||||||
|
|
||||||
|
- for item in CONTROL_CENTER_MODULES:
|
||||||
|
- ccmodule = SettingsWidgets.CCModule(item[0], item[1], item[2], item[3], item[4], self.content_box)
|
||||||
|
- if ccmodule.process(self.c_manager):
|
||||||
|
- self.unsortedSidePages.append((ccmodule.sidePage, ccmodule.name, ccmodule.category))
|
||||||
|
+ self.store_by_cat: typing.Dict[str, Gtk.ListStore] = {}
|
||||||
|
+ self.storeFilter = {}
|
||||||
|
|
||||||
|
- for item in STANDALONE_MODULES:
|
||||||
|
- samodule = SettingsWidgets.SAModule(item[0], item[1], item[2], item[3], item[4], self.content_box)
|
||||||
|
- if samodule.process():
|
||||||
|
- self.unsortedSidePages.append((samodule.sidePage, samodule.name, samodule.category))
|
||||||
|
+ # load CCC and standalone modules, but not python modules yet
|
||||||
|
+ self.load_ccc_modules()
|
||||||
|
+ self.load_standalone_modules()
|
||||||
|
|
||||||
|
- # sort the modules alphabetically according to the current locale
|
||||||
|
+ # if a certain sidepage is given via arguments, try to load only it
|
||||||
|
+ if len(sys.argv) > 1:
|
||||||
|
+ if self.load_sidepage_as_standalone():
|
||||||
|
+ return
|
||||||
|
+
|
||||||
|
+ self.init_settings_overview()
|
||||||
|
+
|
||||||
|
+ def init_settings_overview(self):
|
||||||
|
+ """Load the system settings overview (default)
|
||||||
|
+
|
||||||
|
+ This requires to initialize all settings modules.
|
||||||
|
+ """
|
||||||
|
+ # 1. load all python modules
|
||||||
|
+ self.load_python_modules()
|
||||||
|
+
|
||||||
|
+ # 2. sort the modules alphabetically according to the current locale
|
||||||
|
localeStrKey = cmp_to_key(locale.strcoll)
|
||||||
|
# Apply locale key to the field name of each side page.
|
||||||
|
sidePagesKey = lambda m: localeStrKey(m[0].name)
|
||||||
|
- self.sidePages = sorted(self.unsortedSidePages, key=sidePagesKey)
|
||||||
|
+ self.sidePages = sorted(self.sidePages, key=sidePagesKey)
|
||||||
|
|
||||||
|
- # create the backing stores for the side nav-view.
|
||||||
|
- sidePagesIters = {}
|
||||||
|
- self.store = {}
|
||||||
|
- self.storeFilter = {}
|
||||||
|
+ # 3. create the backing stores for the side nav-view.
|
||||||
|
for sidepage in self.sidePages:
|
||||||
|
sp, sp_id, sp_cat = sidepage
|
||||||
|
- if sp_cat not in self.store: # Label Icon sidePage Category
|
||||||
|
- self.store[sidepage[2]] = Gtk.ListStore(str, str, object, str)
|
||||||
|
+ if sidepage.cat not in self.store_by_cat:
|
||||||
|
+ self.store_by_cat[sidepage.cat] = Gtk.ListStore(str, str, object, str) # Label, Icon, sidePage, Category
|
||||||
|
for category in CATEGORIES:
|
||||||
|
- if category["id"] == sp_cat:
|
||||||
|
+ if category["id"] == sidepage.cat:
|
||||||
|
category["show"] = True
|
||||||
|
|
||||||
|
# Don't allow item names (and their translations) to be more than 30 chars long. It looks ugly and it creates huge gaps in the icon views
|
||||||
|
name = sp.name
|
||||||
|
if len(name) > 30:
|
||||||
|
name = "%s..." % name[:30]
|
||||||
|
- sidePagesIters[sp_id] = (self.store[sp_cat].append([name, sp.icon, sp, sp_cat]), sp_cat)
|
||||||
|
+ self.store_by_cat[sp_cat].append([name, sp.icon, sp, sp_cat])
|
||||||
|
|
||||||
|
self.min_label_length = 0
|
||||||
|
self.min_pix_length = 0
|
||||||
|
|
||||||
|
- for key in self.store:
|
||||||
|
- char, pix = self.get_label_min_width(self.store[key])
|
||||||
|
+ for cat in self.store_by_cat:
|
||||||
|
+ char, pix = self.get_label_min_width(self.store_by_cat[cat])
|
||||||
|
self.min_label_length = max(char, self.min_label_length)
|
||||||
|
self.min_pix_length = max(pix, self.min_pix_length)
|
||||||
|
- self.storeFilter[key] = self.store[key].filter_new()
|
||||||
|
- self.storeFilter[key].set_visible_func(self.filter_visible_function)
|
||||||
|
+ self.storeFilter[cat] = self.store_by_cat[cat].filter_new()
|
||||||
|
+ self.storeFilter[cat].set_visible_func(self.filter_visible_function)
|
||||||
|
|
||||||
|
self.min_label_length += 2
|
||||||
|
self.min_pix_length += 4
|
||||||
|
@@ -378,38 +372,55 @@ class MainWindow(Gio.Application):
|
||||||
|
|
||||||
|
self.calculate_bar_heights()
|
||||||
|
|
||||||
|
- self.tab = 0 # open 'manage' tab by default
|
||||||
|
- self.sort = 1 # sorted by 'score' by default
|
||||||
|
+ self.search_entry.grab_focus()
|
||||||
|
+ self.window.connect("key-press-event", self.on_keypress)
|
||||||
|
+ self.window.connect("button-press-event", self.on_buttonpress)
|
||||||
|
+
|
||||||
|
+ self.window.show()
|
||||||
|
+
|
||||||
|
+ def load_sidepage_as_standalone(self) -> bool:
|
||||||
|
+ """
|
||||||
|
+ When an explicit sidepage is given as an argument,
|
||||||
|
+ try load only this module to save much startup time.
|
||||||
|
+
|
||||||
|
+ Analyses arguments to know the tab to open
|
||||||
|
+ and the sort to apply if the tab is the 'more' one.
|
||||||
|
+
|
||||||
|
+ Examples:
|
||||||
|
+ ```
|
||||||
|
+ cinnamon-settings.py applets --tab=more --sort=date
|
||||||
|
+ cinnamon-settings.py applets --tab=1 --sort=2
|
||||||
|
+ cinnamon-settings.py applets --tab=more --sort=date
|
||||||
|
+ cinnamon-settings.py applets --tab=1 -s 2
|
||||||
|
+ cinnamon-settings.py applets -t 1 -s installed
|
||||||
|
+ cinnamon-settings.py desklets -t 2
|
||||||
|
+ ```
|
||||||
|
+ Please note that useless or wrong arguments are ignored.
|
||||||
|
+
|
||||||
|
+ :return: True if sidepage was loaded successfully, False otherwise
|
||||||
|
+ """
|
||||||
|
+ if sys.argv == 1:
|
||||||
|
+ return False
|
||||||
|
|
||||||
|
- # Select the first sidePage
|
||||||
|
+ # (1) get the settings sidepage name and rewrite it if necessary
|
||||||
|
+ sidepage_name = ARG_REWRITE.get(sys.argv[1], sys.argv[1])
|
||||||
|
+ # pop the arg once we consume it so we don't pass it go Gio.application.run
|
||||||
|
+ sys.argv.pop(1)
|
||||||
|
+
|
||||||
|
+ # (2) Try to load a matching python module.
|
||||||
|
+ # Note: the requested module could also be a CCC or SA module (which are always loaded by __init__())
|
||||||
|
+ self.load_python_modules(only_module=sidepage_name)
|
||||||
|
+
|
||||||
|
+ # (3) set tab to show and/or spices sorting if specified via args
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
- arg1 = sys.argv[1]
|
||||||
|
- if arg1 in ARG_REWRITE.keys():
|
||||||
|
- arg1 = ARG_REWRITE[arg1]
|
||||||
|
- if len(sys.argv) > 1 and arg1 in sidePagesIters:
|
||||||
|
- # Analyses arguments to know the tab to open
|
||||||
|
- # and the sort to apply if the tab is the 'more' one.
|
||||||
|
- # Examples:
|
||||||
|
- # cinnamon-settings.py applets --tab=more --sort=date
|
||||||
|
- # cinnamon-settings.py applets --tab=1 --sort=2
|
||||||
|
- # cinnamon-settings.py applets --tab=more --sort=date
|
||||||
|
- # cinnamon-settings.py applets --tab=1 -s 2
|
||||||
|
- # cinnamon-settings.py applets -t 1 -s installed
|
||||||
|
- # cinnamon-settings.py desklets -t 2
|
||||||
|
- # Please note that useless or wrong arguments are ignored.
|
||||||
|
opts = []
|
||||||
|
sorts_literal = {"name":0, "score":1, "date":2, "installed":3, "update":4}
|
||||||
|
- tabs_literal = {"default":0}
|
||||||
|
- if arg1 in TABS.keys():
|
||||||
|
- tabs_literal = TABS[arg1]
|
||||||
|
+ tabs_literal = TABS.get(sidepage_name, {"default": 0})
|
||||||
|
|
||||||
|
try:
|
||||||
|
- if len(sys.argv) > 2:
|
||||||
|
- opts = getopt.getopt(sys.argv[2:], "t:s:", ["tab=", "sort="])[0]
|
||||||
|
+ opts = getopt.getopt(sys.argv[1:], "t:s:", ["tab=", "sort="])[0]
|
||||||
|
except getopt.GetoptError:
|
||||||
|
- pass
|
||||||
|
- # pop the arg once we consume it so we don't pass it go Gio.application.run
|
||||||
|
- sys.argv.pop(1)
|
||||||
|
+ pass # ignore unknown args
|
||||||
|
|
||||||
|
for opt, arg in opts:
|
||||||
|
if opt in ("-t", "--tab"):
|
||||||
|
@@ -426,30 +437,69 @@ class MainWindow(Gio.Application):
|
||||||
|
sys.argv.remove(opt)
|
||||||
|
sys.argv.remove(arg)
|
||||||
|
|
||||||
|
- # If we're launching a module directly, set the WM class so GWL
|
||||||
|
- # can consider it as a standalone app and give it its own
|
||||||
|
- # group.
|
||||||
|
- wm_class = "cinnamon-settings %s" % arg1
|
||||||
|
- self.window.set_wmclass(wm_class, wm_class)
|
||||||
|
- self.button_back.hide()
|
||||||
|
- (iter, cat) = sidePagesIters[arg1]
|
||||||
|
- path = self.store[cat].get_path(iter)
|
||||||
|
- if path:
|
||||||
|
- self.go_to_sidepage(cat, path, user_action=False)
|
||||||
|
- self.window.show()
|
||||||
|
- if arg1 in ("mintlocale", "blueberry", "system-config-printer", "mintlocale-im", "nvidia-settings"):
|
||||||
|
+ # (4) set the WM class so GWL can consider it as a standalone app and give it its own group.
|
||||||
|
+ wm_class = f"cinnamon-settings {sidepage_name}"
|
||||||
|
+ self.window.set_wmclass(wm_class, wm_class)
|
||||||
|
+ self.button_back.hide()
|
||||||
|
+
|
||||||
|
+ # (5) find and show it
|
||||||
|
+ for sp_data in self.sidePages:
|
||||||
|
+ if sp_data.name == sidepage_name:
|
||||||
|
+ self.go_to_sidepage(sp_data.sp, user_action=False)
|
||||||
|
+ if sp_data.sp.is_standalone:
|
||||||
|
# These modules do not need to leave the System Settings window open,
|
||||||
|
# when selected by command line argument.
|
||||||
|
self.window.close()
|
||||||
|
+ else:
|
||||||
|
+ self.window.show()
|
||||||
|
+ return True
|
||||||
|
+ print(f"warning: settings module {sidepage_name} not found.")
|
||||||
|
+ return False
|
||||||
|
+
|
||||||
|
+ def load_ccc_modules(self):
|
||||||
|
+ """Loads all Cinnamon Control Center settings modules."""
|
||||||
|
+ for item in CONTROL_CENTER_MODULES:
|
||||||
|
+ ccmodule = SettingsWidgets.CCModule(item[0], item[1], item[2], item[3], item[4], self.content_box)
|
||||||
|
+ if ccmodule.process(self.c_manager):
|
||||||
|
+ self.sidePages.append(SidePageData(ccmodule.sidePage, ccmodule.name, ccmodule.category))
|
||||||
|
else:
|
||||||
|
- self.search_entry.grab_focus()
|
||||||
|
- self.window.show()
|
||||||
|
- else:
|
||||||
|
- self.search_entry.grab_focus()
|
||||||
|
- self.window.connect("key-press-event", self.on_keypress)
|
||||||
|
- self.window.connect("button-press-event", self.on_buttonpress)
|
||||||
|
+ print("warning: failed to process CCC module", item[1])
|
||||||
|
+
|
||||||
|
+ def load_standalone_modules(self):
|
||||||
|
+ """Loads all standalone settings modules."""
|
||||||
|
+ for item in STANDALONE_MODULES:
|
||||||
|
+ samodule = SettingsWidgets.SAModule(item[0], item[1], item[2], item[3], item[4], self.content_box)
|
||||||
|
+ if samodule.process():
|
||||||
|
+ self.sidePages.append(SidePageData(samodule.sidePage, samodule.name, samodule.category))
|
||||||
|
+ # else:
|
||||||
|
+ # print(f"note: skipped standalone module {samodule.name} (not found in PATH).")
|
||||||
|
+
|
||||||
|
+ def load_python_modules(self, only_module: str = None) -> bool:
|
||||||
|
+ """Loads all or only a given settings module(s) written in python.
|
||||||
|
+
|
||||||
|
+ :param only_module: (optional) module name to be loaded exclusively
|
||||||
|
+ :return: True if successful, False otherwise
|
||||||
|
+ """
|
||||||
|
+ # Standard setting pages... this can be expanded to include applet dirs maybe?
|
||||||
|
+ mod_files = glob.glob(os.path.join(config.currentPath, 'modules', 'cs_*.py'))
|
||||||
|
+ if len(mod_files) == 0:
|
||||||
|
+ print("warning: no python settings modules found!!", file=sys.stderr)
|
||||||
|
+ return False
|
||||||
|
+
|
||||||
|
+ to_import = [os.path.splitext(os.path.basename(x))[0] for x in mod_files]
|
||||||
|
+
|
||||||
|
+ if only_module is not None:
|
||||||
|
+ to_import = filter(lambda mod: only_module in mod, to_import)
|
||||||
|
|
||||||
|
- self.window.show()
|
||||||
|
+ for module in map(__import__, to_import):
|
||||||
|
+ try:
|
||||||
|
+ mod = module.Module(self.content_box)
|
||||||
|
+ if self.loadCheck(mod) and self.setParentRefs(mod):
|
||||||
|
+ self.sidePages.append(SidePageData(mod.sidePage, mod.name, mod.category))
|
||||||
|
+ except:
|
||||||
|
+ print(f"failed to load python module {module}", file=sys.stderr)
|
||||||
|
+ traceback.print_exc()
|
||||||
|
+ return True
|
||||||
|
|
||||||
|
# If there are no arguments, do_active() is called, otherwise do_open().
|
||||||
|
def do_activate(self):
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Patches taken at 2022-05-05 / see https://github.com/linuxmint/cinnamon/pull/10735
|
||||||
|
|
||||||
Reference in New Issue
Block a user