Skip to content

Handle layout switching thru cinnamon, remove use of XApp. #477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ Depends:
gir1.2-gobject-2.0,
gir1.2-gtk-3.0,
gir1.2-pango-1.0,
gir1.2-xapp-1.0,
iso-flag-png,
libxdo3,
python3,
Expand Down
85 changes: 78 additions & 7 deletions src/dbusdepot/cinnamonClient.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,98 @@
#!/usr/bin/python3

from gi.repository import Gio, CScreensaver
from gi.repository import Gio, CScreensaver, GObject

from dbusdepot.baseClient import BaseClient
from util import trackers

# see cinnamon/files/usr/share/cinnamon/cinnamon-settings/bin/InputSources.py
class CurrentInputSource:
def __init__(self, source):
self.type, self.id, self.index, \
self.display_name, self.short_name, \
self.flag_name, self.xkbid, \
self.xkb_layout, self.xkb_variant, \
self.preferences, \
self.dupe_id, self.active \
= source

class CinnamonClient(BaseClient):
"""
Simple client to talk to Cinnamon's dbus interface. Currently
its only use is for attempting to force an exit from overview
and expo mode (both of which do a fullscreen grab and would prevent
the screensaver from acquiring one.)
Client to talk to Cinnamon's dbus interface.

Used to deactivate special modal cinnamon states and deal with
keyboard layout info.
"""
CINNAMON_SERVICE = "org.Cinnamon"
CINNAMON_PATH = "/org/Cinnamon"

__gsignals__ = {
'current-input-source-changed': (GObject.SignalFlags.RUN_LAST, None, ()),
'input-sources-changed': (GObject.SignalFlags.RUN_LAST, None, ())
}
def __init__(self):
super(CinnamonClient, self).__init__(Gio.BusType.SESSION,
CScreensaver.CinnamonProxy,
self.CINNAMON_SERVICE,
self.CINNAMON_PATH)
self.sources = []

def on_client_setup_complete(self):
pass
trackers.con_tracker_get().connect(self.proxy, "g-signal", self.on_cinnamon_signal)
self.update_layout_sources()

def on_cinnamon_signal(self, proxy, sender, signal, params, data=None):
if signal == "CurrentInputSourceChanged":
self.update_current_layout(params[0])
elif signal == "InputSourcesChanged":
self.update_layout_sources()

def update_layout_sources(self):
self.proxy.GetInputSources(result_handler=self.get_input_sources_callback,
error_handler=self.get_input_sources_error)

def get_input_sources_callback(self, proxy, sources, data=None):
self.sources = []

for source in sources:
input_source = CurrentInputSource(source)
if input_source.type == "xkb":
self.sources.append(input_source)
Copy link
Preview

Copilot AI Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_input_sources_callback method appends new input sources without clearing the existing list, which may cause duplicate entries on subsequent updates. Consider clearing self.sources (e.g., self.sources.clear()) before appending new entries.

Copilot uses AI. Check for mistakes.

self.emit("input-sources-changed")

def get_input_sources_error(self, proxy, error, data=None):
print("Failed to get keyboard layouts from Cinnamon - multiple layouts will not be available: %s" % error.message)

def has_multiple_keyboard_layouts(self):
return len(self.sources) > 1

def update_current_layout(self, layout):
for source in self.sources:
source.active = source.id == layout
self.emit("current-input-source-changed")

def get_current_layout_source(self):
for source in self.sources:
if source.active:
return source
return None

def activate_layout_index(self, index):
self.proxy.ActivateInputSourceIndex("(i)", index)

def activate_next_layout(self):
current = 0

for i in range(0, len(self.sources)):
source = self.sources[i]
if source.active:
current = i
break

new = current + 1
if new > len(self.sources) - 1:
new = 0

self.proxy.ActivateInputSourceIndex("(i)", self.sources[new].index)

def exit_expo_and_overview(self):
if self.ensure_proxy_alive():
Expand Down
128 changes: 60 additions & 68 deletions src/passwordEntry.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,13 @@ def __init__(self):
trackers.con_tracker_get().connect(self, "icon-press", self.on_icon_pressed)

self.placeholder_text = placeholder_text
self.current_icon_name = None
self.current_flag_id = 0
self.original_group = 0
self.lockscreen_layout_source = None
self.system_layout_source = None

self.keyboard_controller = singletons.KeyboardLayoutController
trackers.con_tracker_get().connect(self.keyboard_controller,
"config-changed",
self.on_config_changed)

trackers.con_tracker_get().connect(self.keyboard_controller,
"layout-changed",
self.on_layout_changed)

self.set_lockscreen_keyboard_layout()
self.cinnamon = singletons.CinnamonClient
self.cinnamon.connect("current-input-source-changed", self.on_current_layout_changed)
self.cinnamon.connect("input-sources-changed", self.on_layout_sources_changed)
self.on_layout_sources_changed(self.cinnamon)

trackers.con_tracker_get().connect(self,
"destroy",
Expand All @@ -59,25 +52,18 @@ def on_draw(self, widget, cr, data=None):
update_layout_icon(), just so GtkEntry thinks there's an icon there,
that way it allocates space for it, and responds to clicks in the area.
"""
if not self.keyboard_controller.get_enabled():
return False

icon_rect = widget.get_icon_area(Gtk.EntryIconPosition.PRIMARY)
x = icon_rect.x
y = icon_rect.y + 2
width = (icon_rect.width // 2) * 2
height = icon_rect.height - 4

handled = False

if settings.get_show_flags():
name = self.keyboard_controller.get_current_icon_name()

ui_scale = self.get_scale_factor()

if name:
filename = "/usr/share/iso-flag-png/%s.png" % name

if self.lockscreen_layout_source.flag_name != "":
filename = "/usr/share/iso-flag-png/%s.png" % self.lockscreen_layout_source.flag_name
try:
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, -1, height * ui_scale)

Expand All @@ -98,26 +84,37 @@ def on_draw(self, widget, cr, data=None):

cr.paint()

self.keyboard_controller.render_cairo_subscript(cr,
render_x + (logical_width / 2),
render_y + (logical_height / 2),
logical_width / 2,
logical_height / 2,
self.keyboard_controller.get_current_flag_id())
if self.lockscreen_layout_source.dupe_id > 0:
x = render_x + logical_width / 2
y = render_y + logical_height / 2
width = logical_width / 2 + 2
height = logical_height / 2 + 2

cr.set_source_rgba(0, 0, 0, 0.5)
cr.rectangle(x, y, width, height)
cr.fill()

cr.set_source_rgba(1.0, 1.0, 1.0, 0.8)
cr.rectangle(x + 1, y + 1, width - 2, height - 2)
cr.fill()

cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
cr.select_font_face("sans", cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
cr.set_font_size(height - 2.0)

dupe_str = str(self.lockscreen_layout_source.dupe_id)

ext = cr.text_extents(dupe_str)
cr.move_to((x + (width / 2.0) - (ext.width / 2.0)),
(y + (height / 2.0) + (ext.height / 2.0)))
cr.show_text(dupe_str)

handled = True
except GLib.Error:
pass

if not handled:
if settings.get_use_layout_variant_names():
name = self.keyboard_controller.get_current_variant_label()
else:
name = self.keyboard_controller.get_current_short_group_label()

if settings.get_show_upper_case_layout():
name = name.upper()

name = self.lockscreen_layout_source.short_name
ctx = widget.get_style_context()
ctx.save()

Expand Down Expand Up @@ -167,16 +164,27 @@ def pulse(self):
self.progress_pulse()
return True

def on_layout_changed(self, controller, layout):
def on_current_layout_changed(self, cinnamon):
if not self.cinnamon.has_multiple_keyboard_layouts():
return

self.lockscreen_layout_source = self.cinnamon.get_current_layout_source()

self.grab_focus()
self.update_layout_icon()

def on_config_changed(self, controller):
def on_layout_sources_changed(self, cinnamon):
if not self.cinnamon.has_multiple_keyboard_layouts():
return

self.system_layout_source = self.cinnamon.get_current_layout_source()
self.lockscreen_layout_source = self.system_layout_source

self.set_lockscreen_keyboard_layout()

def on_icon_pressed(self, entry, icon_pos, event):
if icon_pos == Gtk.EntryIconPosition.PRIMARY:
self.keyboard_controller.next_group()
self.cinnamon.activate_next_layout()
elif icon_pos == Gtk.EntryIconPosition.SECONDARY:
if self.get_input_purpose() == Gtk.InputPurpose.FREE_FORM:
self.set_visibility(False)
Expand All @@ -195,62 +203,46 @@ def update_layout_icon(self):
also ensures a redraw at the correct time to update the flag image.
"""
self.set_icon_from_icon_name(Gtk.EntryIconPosition.PRIMARY, "screensaver-blank")
self.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, self.keyboard_controller.get_current_name())

self.update_saved_group(self.keyboard_controller.get_current_group())
self.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, self.lockscreen_layout_source.display_name)

def on_destroy(self, widget, data=None):
self.stop_progress()

trackers.con_tracker_get().disconnect(self.keyboard_controller,
"config-changed",
self.on_config_changed)

trackers.con_tracker_get().disconnect(self.keyboard_controller,
"layout-changed",
self.on_layout_changed)

self.restore_original_layout()

def set_lockscreen_keyboard_layout(self):
if not self.keyboard_controller.get_enabled():
return

# If there are multiple keyboard layouts, we want to store
# the one the user ends up using in the unlock widget, as they'll
# want to use the same one each time, at least until they change
# their password.

saved_group = settings.get_kb_group()
self.original_group = self.keyboard_controller.get_current_group()

new_group = 0
saved_index = settings.get_kb_group()
new_index = 0

if saved_group == -1:
new_group = self.original_group
if saved_index == -1:
new_index = self.system_layout_source.index
settings.set_kb_group(new_index)
else:
new_group = saved_group
new_index = saved_index

if new_index != self.system_layout_source.index:
self.cinnamon.activate_layout_index(new_index)

self.keyboard_controller.set_current_group(new_group)
self.update_saved_group(new_group)
self.update_layout_icon()

trackers.con_tracker_get().connect_after(self,
"draw",
self.on_draw)

def update_saved_group(self, group):
settings.set_kb_group(group)

def restore_original_layout(self):
"""
Called when the unlock dialog is destroyed, restores
the group that was active before the screensaver was activated.
"""
if not self.keyboard_controller.get_enabled():
return
if settings.get_kb_group() != self.lockscreen_layout_source.index:
settings.set_kb_group(self.lockscreen_layout_source.index)

self.keyboard_controller.set_current_group(self.original_group)
self.cinnamon.activate_layout_index(self.system_layout_source.index)

def grab_focus(self):
Gtk.Widget.grab_focus(self)
Expand Down
6 changes: 0 additions & 6 deletions src/singletons.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,6 @@
Backgrounds.load_from_preferences(settings.bg_settings)
settings.bg_settings.connect("changed", lambda s,k: Backgrounds.load_from_preferences(s))

# We use XAppKbdLayoutController as a wrapper around libgnomekbd to supply the icon theme
# with icons, as well as providing correct group names.
gi.require_version('XApp', '1.0')
from gi.repository import XApp
KeyboardLayoutController = XApp.KbdLayoutController()

# This sets up synchronously, as we need to know the fractional scaling state before
# setting up the screensaver.
MuffinClient = _MuffinClient()
Expand Down
Loading