Skip to content

Commit

Permalink
Allow assigning same keyboard shortcut to multiple ports (resolves #106)
Browse files Browse the repository at this point in the history
Bump version to 2.3.6
  • Loading branch information
yktoo committed Jun 18, 2021
1 parent f46e5f3 commit 8f93639
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Ubuntu] and its version
- Desktop environment [e.g. GNOME] and its version
- Sound Switcher Indicator version [e.g. 2.3.5]
- Sound Switcher Indicator version [e.g. 2.3.6]

**Indicator log:**
In order to fetch it, quit the indicator from the menu ("Quit"), open Terminal and start it again as follows:
Expand Down
6 changes: 6 additions & 0 deletions debian/changelog
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
indicator-sound-switcher (2.3.6) hirsute; urgency=low

* Allow assigning same keyboard shortcut to multiple ports (#106)

-- Dmitry Kann <yktooo@gmail.com> Fri, 18 Jun 2021 13:21:03 +0200

indicator-sound-switcher (2.3.5.2-1) focal; urgency=low

* Determine app version based on the installed package
Expand Down
61 changes: 34 additions & 27 deletions lib/indicator_sound_switcher/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __getitem__(self, key: [str, tuple]):
[1] Optional default value to return if the key isn't found. If not given, a new, empty Config instance will
be inserted and returned
:rtype : V
:return Configuration value corresponfing to the name or the default.
:return Configuration value corresponding to the name or the default.
"""
# Sort the arguments
default = None
Expand Down Expand Up @@ -113,10 +113,24 @@ def __init__(self, on_port_selected: callable):
"""Constructor.
:param on_port_selected: callback that receives the port once the corresponding shortcut has been pressed
"""
self.current_mappings = {} # Dictionary of tuples: shortcut => (device_name, port_name)
self.current_mappings = {} # Dictionary of list of tuples: shortcut => [(device_name, port_name), ...]
self.on_port_selected = on_port_selected
Keybinder.init()

def _bind_all(self):
"""Bind all key bindings according to the current_mappings."""
for shortcut, mapping in self.current_mappings.items():
if Keybinder.bind(shortcut, self.on_port_selected, mapping):
logging.debug(' - Bound keyboard shortcut `%s` to `%s`', shortcut, mapping)
else:
logging.warning('Failed to bind keyboard shortcut `%s` to `%s`', shortcut, mapping)

def _unbind_all(self):
"""Unbind all mapped key bindings."""
for shortcut in self.current_mappings.keys():
Keybinder.unbind(shortcut)
logging.debug(' - Unbound keyboard shortcut `%s`', shortcut)

def bind_keys(self, config: Config):
"""Updates key bindings based on the current configuration.
:param config: Config object to take keyboard shortcuts from
Expand All @@ -130,37 +144,30 @@ def bind_keys(self, config: Config):
# If there's a shortcut
shortcut = port_cfg['shortcut', None]
if shortcut:
new_mapping = (device_name, port_name)

# If the key combination is already mapped, move on to the next item
if shortcut not in self.current_mappings or self.current_mappings[shortcut] != new_mapping:
# Unmap the current assignment, if needed
if shortcut in self.current_mappings:
Keybinder.unbind(shortcut)
logging.debug(' - Keyboard shortcut `%s` is remapped, unbound', shortcut)
# Append the tuple to the mapping list for the shortcut
if shortcut not in new_mappings:
new_mappings[shortcut] = []
new_mappings[shortcut].append((device_name, port_name))

# Map the keyboard shortcut
if Keybinder.bind(shortcut, self.on_port_selected, new_mapping):
logging.debug(' - Bound keyboard shortcut `%s` to `%s`', shortcut, new_mapping)
else:
logging.warning('Failed to bind keyboard shortcut `%s` to `%s`', shortcut, new_mapping)
# Sort each list by device and port name
for m in new_mappings.values():
m.sort()

# Remember the mapping in new_mappings
new_mappings[shortcut] = new_mapping
# (Re)map all mappings
self._unbind_all()
self.current_mappings = new_mappings
self._bind_all()

# Remove key mappings that no longer exist
for shortcut in self.current_mappings.keys():
if shortcut not in new_mappings:
Keybinder.unbind(shortcut)
logging.debug(" - Keyboard shortcut `%s` isn't used anymore, unbound", shortcut)
def suspend(self):
"""Temporarily disable all shortcuts."""
self._unbind_all()

# Save the new mappings for future reference
self.current_mappings = new_mappings
def resume(self):
"""Restore all shortcuts disabled by a call to suspend()."""
self._bind_all()

def shutdown(self):
"""Remove all keyboard bindings."""
logging.debug('KeyboardManager.shutdown()')
for shortcut in self.current_mappings.keys():
Keybinder.unbind(shortcut)
logging.debug(' - Unbound keyboard shortcut `%s`', shortcut)
self._unbind_all()
self.current_mappings = {}
42 changes: 32 additions & 10 deletions lib/indicator_sound_switcher/indicator.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,20 +223,26 @@ def on_select_port(self, widget, data):
if widget.get_active():
self.activate_port(*data)

def on_port_keyboard_shortcut(self, shortcut, data):
def on_port_keyboard_shortcut(self, shortcut, data: list):
"""Signal handler: port selected by a keyboard shortcut."""
logging.debug('.on_port_keyboard_shortcut(`%s`, `%s`)', shortcut, data)
device_name, port_name = data

# Try to find the card by name
for idx_card in self.cards.keys():
if self.cards[idx_card].name == device_name:
# Found - activate the port
self.activate_port(idx_card, port_name)
return
# Iterate the (device, port) tuples to figure out if any of them is active
idx_active = -1
for idx in range(len(data)):
card, port = self.find_card_port_by_name(*data[idx])
if card and port and port.is_active:
idx_active = idx
logging.debug(' * card `%s`, port `%s` is currently active', card.name, port.name)
break

# If none of them is active, or the last is active, start over from 0. Otherwise pick the next one from the list
idx_active = 0 if idx_active < 0 or idx_active == len(data)-1 else idx_active+1

# Not found
logging.warning('Failed to find card `%s` among the available devices', device_name)
# Activate the port
card, port = self.find_card_port_by_name(*data[idx_active])
if card and port:
self.activate_port(card.index, port.name)

# ------------------------------------------------------------------------------------------------------------------
# PulseAudio callbacks
Expand Down Expand Up @@ -920,6 +926,22 @@ def activate_source(self, name: str):
for source in self.sources.values():
source.is_active = source.name == name

def find_card_port_by_name(self, card_name: str, port_name: str) -> tuple:
"""Find a card and its port by their names, and return both as a tuple, or (None, None), if not found."""
# Iterate known cards
for idx, card in self.cards.items():
if card.name == card_name:
# If the port isn't found for the card, return only the card
if port_name not in card.ports:
logging.warning('# Failed to find port `%` on card `%s`', port_name, card_name)
return card, None

# Return the card and the port
return card, card.ports[port_name]

# Card not found
logging.warning('Failed to find card `%s` among the available devices', card_name)

@staticmethod
def run():
"""Run the application."""
Expand Down
21 changes: 7 additions & 14 deletions lib/indicator_sound_switcher/prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,16 +263,6 @@ def get_current_port_config(self) -> Config:
device_cfg['ports'][row.port_name] = port_cfg
return port_cfg

def remove_shortcut_binding(self, shortcut: str):
"""Scan every port's config and remove the given shortcut if it's bound to it.
:param shortcut: shortcut whose binding is to be removed
"""
# Scan all ports of all devices and remove any current mapping of this shortcut
for device_cfg in self.indicator.config['devices'].values():
for port_cfg in device_cfg['ports'].values():
if port_cfg['shortcut', None] == shortcut:
port_cfg['shortcut'] = None

def on_destroy(self, dlg):
"""Signal handler: dialog destroying."""
logging.debug('PreferencesDialog.on_destroy()')
Expand Down Expand Up @@ -374,11 +364,18 @@ def on_port_set_shortcut_clicked(self, btn: Gtk.Button):
if cfg is None:
return

# Suspend the keyboard manager so that it doesn't interfere with key selection (in case the same key combination
# is reused)
self.indicator.keyboard_manager.suspend()

# Show a grab shortcut dialog
dlg = KeyboardShortcutDialog(self._dlg.prefs_dialog)
shortcut = dlg.run()
dlg.destroy()

# Restore the keyboard manager
self.indicator.keyboard_manager.resume()

# None means grabbing was canceled
if shortcut is None:
return
Expand All @@ -392,10 +389,6 @@ def on_port_set_shortcut_clicked(self, btn: Gtk.Button):
if key_name == 'BackSpace':
key_name = None

# Remove any current mapping of this shortcut
if key_name:
self.remove_shortcut_binding(key_name)

# Update the button and the port config
self.b_port_set_shortcut.set_label(key_name or _('(none)'))
cfg['shortcut'] = key_name
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


APP_ID = 'indicator-sound-switcher'
APP_VERSION = '2.3.5.2'
APP_VERSION = '2.3.6'


def compile_lang_files() -> list:
Expand Down

0 comments on commit 8f93639

Please sign in to comment.