Merge pull request #20 from and3rson/configurable-keybinds

Configurable keybinds
This commit is contained in:
Andrew Dunai 2018-03-23 11:59:03 +02:00 committed by GitHub
commit 98c96bd462
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 503 additions and 188 deletions

4
.gitignore vendored
View file

@ -104,3 +104,7 @@ ENV/
# mypy
.mypy_cache/
# Editor
#*
*~

View file

@ -4,6 +4,7 @@ max-args = 8
[messages control]
disable =
duplicate-code,
too-few-public-methods,
too-many-public-methods,
too-many-instance-attributes,

View file

@ -24,59 +24,7 @@ from clay.pages.settings import SettingsPage
from clay.settings import settings
from clay.notifications import notification_area
from clay.gp import gp
def create_palette(transparent=False):
"""
Return urwid palette.
"""
if transparent:
bgcolor = ''
else:
bgcolor = '#222'
return [
(None, '', '', '', '#FFF', bgcolor),
('default', '', '', '', '#FFF', bgcolor),
('logo', '', '', '', '#F54', bgcolor),
('bg', '', '', '', '#FFF', '#222'),
('primary', '', '', '', '#F54', '#FFF'),
('secondary', '', '', '', '#17F', '#FFF'),
('selected', '', '', '', '#FFF', '#444'),
('primary_inv', '', '', '', '#FFF', '#17F'),
('secondary_inv', '', '', '', '#FFF', '#F17'),
('progress', '', '', '', '#FFF', '#F54'),
('progress_remaining', '', '', '', '#FFF', '#444'),
('progressbar_done', '', '', '', '#F54', bgcolor),
('progressbar_done_paused', '', '', '', '', bgcolor),
('progressbar_remaining', '', '', '', '#222', bgcolor),
('title-idle', '', '', '', '', bgcolor),
('title-playing', '', '', '', '#F54', bgcolor),
('panel', '', '', '', '#FFF', '#222'),
('panel_focus', '', '', '', '#FFF', '#F54'),
('panel_divider', '', '', '', '#444', '#222'),
('panel_divider_focus', '', '', '', '#444', '#F54'),
('line1', '', '', '', '#FFF', bgcolor),
('line1_focus', '', '', '', '#FFF', '#333'),
('line1_active', '', '', '', '#F54', bgcolor),
('line1_active_focus', '', '', '', '#F54', '#333'),
('line2', '', '', '', '#AAA', bgcolor),
('line2_focus', '', '', '', '#AAA', '#333'),
('input', '', '', '', '#FFF', '#444'),
('input_focus', '', '', '', '#FFF', '#F54'),
('flag', '', '', '', '#AAA', bgcolor),
('flag-active', '', '', '', '#F54', bgcolor),
('notification', '', '', '', '#F54', '#222'),
]
from clay.hotkeys import hotkey_manager
class AppWidget(urwid.Frame):
"""
@ -84,20 +32,6 @@ class AppWidget(urwid.Frame):
Handles tab switches, global keypresses etc.
"""
KEYBINDS = {
'ctrl q': 'seek_start',
'ctrl w': 'play_pause',
'ctrl e': 'next_song',
'shift left': 'seek_backward',
'shift right': 'seek_forward',
'ctrl s': 'toggle_shuffle',
'ctrl r': 'toggle_repeat_one',
'ctrl x': 'quit',
'esc': 'handle_escape_action',
'ctrl _': 'handle_escape_action'
}
class Tab(urwid.Text):
"""
Represents a single tab in header tabbar.
@ -138,12 +72,7 @@ class AppWidget(urwid.Frame):
SearchPage(self),
SettingsPage(self)
]
self.tabs = [
AppWidget.Tab(page)
for page
in self.pages
]
self.tabs = [AppWidget.Tab(page) for page in self.pages]
self.current_page = None
self.loop = None
@ -176,11 +105,8 @@ class AppWidget(urwid.Frame):
Request user authorization.
"""
username, password, device_id, authtoken = [
settings.get(x)
for x
in ('username', 'password', 'device_id', 'authtoken')
]
authtoken, device_id, _, password, username = settings.get_section("play_settings").values()
if self._login_notification:
self._login_notification.close()
if use_token and authtoken:
@ -238,7 +164,7 @@ class AppWidget(urwid.Frame):
return
with settings.edit() as config:
config['authtoken'] = gp.get_authtoken()
config['play_settings']['authtoken'] = gp.get_authtoken()
self._login_notification.close()
@ -297,11 +223,7 @@ class AppWidget(urwid.Frame):
self.set_page(tab.page.__class__.__name__)
return
method_name = AppWidget.KEYBINDS.get(key)
if method_name:
getattr(self, method_name)()
else:
super(AppWidget, self).keypress(size, key)
hotkey_manager.keypress("global", self, super(AppWidget, self), size, key)
@staticmethod
def seek_start():
@ -324,6 +246,13 @@ class AppWidget(urwid.Frame):
"""
player.next(True)
@staticmethod
def prev_song():
"""
Play the previous song.
"""
player.prev(True)
@staticmethod
def seek_backward():
"""
@ -352,14 +281,14 @@ class AppWidget(urwid.Frame):
"""
player.set_repeat_one(not player.get_is_repeat_one())
@staticmethod
def quit():
def quit(self):
"""
Quit app.
"""
self.loop = None
sys.exit(0)
def handle_escape_action(self):
def handle_escape(self):
"""
Run escape actions. If none are pending, close newest notification.
"""
@ -376,9 +305,6 @@ class MultilineVersionAction(argparse.Action):
An argparser action for multiple lines so we can display the copyright notice
Based on: https://stackoverflow.com/a/41147122
"""
version = "0.6.2"
author = "Andrew Dunai"
def __init__(self, option_strings, dest, nargs=None, **kwargs):
if nargs is not None:
raise ValueError("nargs not allowed")
@ -404,15 +330,17 @@ def main():
parser.add_argument("-v", "--version", action=MultilineVersionAction)
parser.add_argument(
keybinds_group = parser.add_mutually_exclusive_group()
keybinds_group.add_argument(
"--with-x-keybinds",
help="define global X keybinds (requires Keybinder and PyGObject)",
action='store_true'
)
parser.add_argument(
"--transparent",
help="use transparent background",
keybinds_group.add_argument(
"--without-x-keybinds",
help="Don't define global keybinds (overrides configuration file)",
action='store_true'
)
@ -421,12 +349,17 @@ def main():
if args.version:
exit(0)
if args.with_x_keybinds:
if (args.with_x_keybinds or settings.get('x_keybinds', 'clay_settings')) \
and not args.without_x_keybinds:
player.enable_xorg_bindings()
# Create a 256 colour palette.
palette = [(name, '', '', '', res['foreground'], res['background'])
for name, res in settings.colours_config.items()]
# Run the actual program
app_widget = AppWidget()
loop = urwid.MainLoop(app_widget, create_palette(args.transparent))
loop = urwid.MainLoop(app_widget, palette)
app_widget.set_loop(loop)
loop.screen.set_terminal_properties(256)
loop.run()

95
clay/colours.yaml Normal file
View file

@ -0,0 +1,95 @@
default: &default
foreground: "#FFF"
background: "#222"
primary: &primary
foreground: "#F54"
background: "#FFF"
primary_inv: &primary_inv
foreground: "#FFF"
background: "#F54"
secondary: &secondary
foreground: "#17F"
background: "#FFF"
secondary_inv: &secondary_inv
foreground: "#FFF"
background: "#17F"
background: *default
None: *default
'': *default
logo: *primary
progress: *primary
progressbar_done: *primary
selected: *primary_inv
progress_remaining:
foreground: "#FFF"
background: "#444"
progressbar_done_paused:
<<: *default
foreground: null
progressbar_remaining:
<<: *default
foreground: "#222"
title-idle:
<<: *default
foreground: null
title-playing: *primary
panel:
foreground: "#FFF"
background: "#222"
panel_focus: *primary_inv
panel_divider:
foreground: "#444"
background: "#222"
panel_divider_focus:
foreground: "#444"
background: '#F54'
line1: *default
line1_focus:
foreground: "#FFF"
background: "#333"
line1_active: *primary
line1_active_focus:
foreground: "#F54"
background: "#333"
line2:
<<: *default
foreground: "#AAA"
line2_focus:
foreground: "#AAA"
background: "#333"
input:
foreground: "#FFF"
background: "#444"
input_focus: *primary_inv
flag:
<<: *default
foreground: "#AAA"
flag-active: *primary
notification:
foreground: "#F54"
background: "#222"

58
clay/config.yaml Normal file
View file

@ -0,0 +1,58 @@
#: pylint:skip-file
hotkeys:
mod_key: ctrl
x_hotkeys:
play_pause: XF86AudioPlay
next: XF86AudioNext
prev: XF86AudioPrev
clay_hotkeys:
global:
seek_start: mod + q
play_pause: mod + w
seek_backward: shift + left
seek_forward: shift + right
quit: mod + x
toggle_shuffle: mod + s
next_song: mod + d
prev_song: mod + a
toggle_repeat_one: mod + r
handle_escape: esc, mod + _
library_item:
play: enter
append: mod + a
unappend: mod + u
request_station: mod + p
show_context_menu: meta + p
library_view:
move_to_beginning: home
move_to_end: end
move_up: up
move_down: down
hide_context_menu: meta + p
playlist_page:
start_playlist: enter
debug_page:
copy_message: enter
search_page:
send_query: enter
settings_page:
equalizer_up: "+"
equalizer_down: "-"
clay_settings:
x_keybinds: false
play_settings:
authtoken:
device_id:
download_tracks: false
password:
username:

View file

@ -12,7 +12,6 @@ from gmusicapi.clients import Mobileclient
from clay.eventhook import EventHook
from clay.log import logger
def asynchronous(func):
"""
Decorates a function to become asynchronous.
@ -45,6 +44,7 @@ def asynchronous(func):
callback(result, None, **extra)
Thread(target=process).start()
return wrapper

View file

@ -44,14 +44,9 @@ class _HotkeyManager(object):
Manages configs.
Runs Gtk main loop in a thread.
"""
DEFAULT_HOTKEYS = {
'play_pause': 'XF86AudioPlay',
'next': 'XF86AudioNext',
'prev': 'XF86AudioPrev'
}
def __init__(self):
self.hotkeys = {}
self._x_hotkeys = {}
self._hotkeys = self._parse_hotkeys()
self.config = None
self.play_pause = EventHook()
@ -61,7 +56,6 @@ class _HotkeyManager(object):
if IS_INIT:
Keybinder.init()
self.initialize()
threading.Thread(target=Gtk.main).start()
else:
logger.debug("Not loading the global shortcuts.")
@ -72,24 +66,92 @@ class _HotkeyManager(object):
)
@staticmethod
def load_keys():
def _to_gtk_modifier(key):
"""
Load hotkey config from settings.
Translates the modifies to the way that GTK likes them.
"""
hotkeys = settings.get('hotkeys', {})
for operation, default_key in _HotkeyManager.DEFAULT_HOTKEYS.items():
if operation not in hotkeys or not hotkeys[operation]:
hotkeys[operation] = default_key
key = key.strip()
if key == "meta":
key = "<alt>"
elif key in ("ctrl", "alt", "shift"):
key = "<" + key + ">"
else:
key = key
return key
def _parse_x_hotkeys(self):
"""
Reads out them configuration file and parses them into hotkeys readable by GTK.
"""
hotkey_default_config = settings.get_default_config_section('hotkeys', 'x_hotkeys')
mod_key = settings.get('mod_key', 'hotkeys')
hotkeys = {}
for action in hotkey_default_config:
key_seq = settings.get(action, 'hotkeys', 'x_hotkeys')
for key in key_seq.split(', '):
hotkey = key.split(' + ')
if hotkey[0].strip() == 'mod':
hotkey[0] = mod_key
hotkey = [self._to_gtk_modifier(key) for key in hotkey]
hotkeys[action] = ''.join(hotkey)
return hotkeys
def _parse_hotkeys(self):
"""
Reads out the configuration file and parse them into a hotkeys for urwid.
"""
hotkey_config = settings.get_default_config_section('hotkeys', 'clay_hotkeys')
mod_key = settings.get('mod_key', 'hotkeys')
hotkeys = {}
for hotkey_name, hotkey_dict in hotkey_config.items():
hotkeys[hotkey_name] = {}
for action in hotkey_dict.keys():
key_seq = settings.get(action, 'hotkeys', 'clay_hotkeys', hotkey_name)
for key in key_seq.split(', '):
hotkey = key.split(' + ')
if hotkey[0].strip() == 'mod':
hotkey[0] = mod_key
hotkeys[hotkey_name][' '.join(hotkey)] = action
return hotkeys
def keypress(self, name, caller, super_, size, key):
"""
Process the pressed key by looking it up in the configuration file
"""
method_name = self._hotkeys[name].get(key)
if method_name:
ret = getattr(caller, method_name)()
elif super_ is not None:
ret = super_.keypress(size, key)
else:
ret = key
return ret
def initialize(self):
"""
Unbind previous hotkeys, re-read config & bind new hotkeys.
"""
for operation, key in self.hotkeys.items():
for operation, key in self._x_hotkeys.items():
Keybinder.unbind(key)
self.hotkeys = self.load_keys()
for operation, key in self.hotkeys.items():
self._x_hotkeys = self._parse_x_hotkeys()
for operation, key in self._x_hotkeys.items():
Keybinder.bind(key, self.fire_hook, operation)
def fire_hook(self, key, operation):

View file

@ -7,6 +7,7 @@ from clay.pages.page import AbstractPage
from clay.log import logger
from clay.clipboard import copy
from clay.gp import gp
from clay.hotkeys import hotkey_manager
class DebugItem(urwid.AttrMap):
@ -36,10 +37,12 @@ class DebugItem(urwid.AttrMap):
"""
Handle heypress.
"""
if key == 'enter':
copy(self.log_record.formatted_message)
return None
return key
return hotkey_manager.keypress("debug_page", self, None, None, key)
def copy_message(self):
"""Copy the selected error message to the clipboard"""
copy(self.log_record.formatted_message)
return None
class DebugPage(urwid.Pile, AbstractPage):

View file

@ -7,6 +7,7 @@ from clay.gp import gp
from clay.songlist import SongListBox
from clay.notifications import notification_area
from clay.pages.page import AbstractPage
from clay.hotkeys import hotkey_manager
class MyPlaylistListItem(urwid.Columns):
@ -33,10 +34,15 @@ class MyPlaylistListItem(urwid.Columns):
"""
Handle keypress.
"""
if key == 'enter':
urwid.emit_signal(self, 'activate', self)
return None
return super(MyPlaylistListItem, self).keypress(size, key)
return hotkey_manager.keypress("playlist_page", self, super(MyPlaylistListItem, self),
size, key)
def start_playlist(self):
"""
Start playing the selected playlist
"""
urwid.emit_signal(self, 'activate', self)
return None
def get_tracks(self):
"""

View file

@ -6,6 +6,7 @@ import urwid
from clay.gp import gp
from clay.songlist import SongListBox
from clay.notifications import notification_area
from clay.hotkeys import hotkey_manager
from clay.pages.page import AbstractPage
@ -35,10 +36,14 @@ class SearchBox(urwid.Columns):
"""
Handle keypress.
"""
if key == 'enter':
urwid.emit_signal(self, 'search-requested', self.query.edit_text)
return None
return super(SearchBox, self).keypress(size, key)
return hotkey_manager.keypress("search_page", self, super(SearchBox, self), size, key)
def send_query(self):
"""
Send a message to urwid to search the filled in search query
"""
urwid.emit_signal(self, 'search-requested', self.query.edit_text)
return None
class SearchPage(urwid.Pile, AbstractPage):

View file

@ -6,6 +6,7 @@ import urwid
from clay.pages.page import AbstractPage
from clay.settings import settings
from clay.player import player
from clay.hotkeys import hotkey_manager
class Slider(urwid.Widget):
@ -80,18 +81,27 @@ class Slider(urwid.Widget):
"""
Handle equalizer band modification.
"""
if key == '+':
if self.value < self.max_value:
self.value += 1
self.update()
return None
elif key == '-':
if self.value > -self.max_value:
self.value -= 1
self.update()
return None
else:
return key
return hotkey_manager.keypress("settings_page", self, None, None, key)
def equalizer_up(self):
"""
Turn the equalizer band up
"""
if self.value < self.max_value:
self.value += 1
self.update()
return None
def equalizer_down(self):
"""
Turn the equalizer band down
"""
if self.value > -self.max_value:
self.value -= 1
self.update()
return None
def update(self):
"""
@ -131,17 +141,17 @@ class SettingsPage(urwid.Columns, AbstractPage):
def __init__(self, app):
self.app = app
self.username = urwid.Edit(
edit_text=settings.get('username', '')
edit_text=settings.get('username', 'play_settings')
)
self.password = urwid.Edit(
mask='*', edit_text=settings.get('password', '')
mask='*', edit_text=settings.get('password', 'play_settings')
)
self.device_id = urwid.Edit(
edit_text=settings.get('device_id', '')
edit_text=settings.get('device_id', 'play_settings')
)
self.download_tracks = urwid.CheckBox(
'Download tracks before playback',
state=settings.get('download_tracks', False)
state=settings.get('download_tracks', 'play_settings')
)
self.equalizer = Equalizer()
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
@ -170,10 +180,10 @@ class SettingsPage(urwid.Columns, AbstractPage):
Called when "Save" button is pressed.
"""
with settings.edit() as config:
config['username'] = self.username.edit_text
config['password'] = self.password.edit_text
config['device_id'] = self.device_id.edit_text
config['download_tracks'] = self.download_tracks.state
config['play_settings']['username'] = self.username.edit_text
config['play_settings']['password'] = self.password.edit_text
config['play_settings']['device_id'] = self.device_id.edit_text
config['play_settings']['download_tracks'] = self.download_tracks.state
self.app.set_page('MyLibraryPage')
self.app.log_in()

View file

@ -4,6 +4,7 @@ Media player built using libVLC.
# pylint: disable=too-many-instance-attributes
# pylint: disable=too-many-public-methods
from random import randint
from ctypes import CFUNCTYPE, c_void_p, c_int, c_char_p
import json
import os
@ -35,6 +36,7 @@ class _Queue(object):
self.repeat_one = False
self.tracks = []
self._played_tracks = []
self.current_track_index = None
def load(self, tracks, current_track_index=None):
@ -91,6 +93,8 @@ class _Queue(object):
if not self.tracks:
return None
self.current_track_index = self.tracks[0]
else:
self._played_tracks.append(self.current_track_index)
if self.repeat_one and not force:
return self.get_current_track()
@ -105,12 +109,39 @@ class _Queue(object):
return self.get_current_track()
def prev(self, force=False):
"""
Revert to their last song and return it.
If *force* is ``True`` then tracks will be changed event if
tracks repition is enabled. Otherwise current tracks may be
yielded again.
Manual tracks switching calls this method with ``force=True``.
"""
if self._played_tracks == []:
return None
if self.repeat_one and not force:
return self.get_current_track()
self.current_track_index = self._played_tracks.pop()
return self.get_current_track()
def get_tracks(self):
"""
Return current queue, i.e. a list of :class:`Track` instances.
"""
return self.tracks
#+pylint: disable=unused-argument
def _dummy_log(data, level, ctx, fmt, args):
"""
A dummy callback function for VLC so it doesn't write to stdout.
Should probably do something in the future
"""
pass
#+pylint: disable=unused-argument
class _Player(object):
"""
@ -129,6 +160,15 @@ class _Player(object):
def __init__(self):
self.instance = vlc.Instance()
print_func = CFUNCTYPE(c_void_p,
c_void_p, # data
c_int, # level
c_void_p, # context
c_char_p, # fmt
c_void_p) #args
self.instance.log_set(print_func(_dummy_log), None)
self.instance.set_user_agent(
meta.APP_NAME,
meta.USER_AGENT
@ -154,9 +194,7 @@ class _Player(object):
)
self.equalizer = vlc.libvlc_audio_equalizer_new()
self.media_player.set_equalizer(self.equalizer)
self._create_station_notification = None
self._is_loading = False
self.queue = _Queue()
@ -331,8 +369,10 @@ class _Player(object):
self.broadcast_state()
self.track_changed.fire(track)
if settings.get('download_tracks', False) or settings.get_is_file_cached(track.filename):
if settings.get('download_tracks', 'play_settings') or \
settings.get_is_file_cached(track.filename):
path = settings.get_cached_file_path(track.filename)
if path is None:
logger.debug('Track %s not in cache, downloading...', track.store_id)
track.get_url(callback=self._download_track)
@ -425,6 +465,14 @@ class _Player(object):
self.queue.next(force)
self._play()
def prev(self, force=False):
"""
Advance to their previous track in their queue
seek :meth:`._Queue.prev`
"""
self.queue.prev(force)
self._play()
def get_current_track(self):
"""
Return currently played track.

View file

@ -6,8 +6,8 @@ import os
import copy
import errno
import yaml
import appdirs
import pkg_resources
class _SettingsEditor(dict):
@ -42,6 +42,7 @@ class _Settings(object):
"""
def __init__(self):
self._config = {}
self._default_config = {}
self._cached_files = set()
self._config_dir = None
@ -58,6 +59,7 @@ class _Settings(object):
"""
self._config_dir = appdirs.user_config_dir('clay', 'Clay')
self._config_file_path = os.path.join(self._config_dir, 'config.yaml')
self._colours_file_path = os.path.join(self._config_dir, 'colours.yaml')
try:
os.makedirs(self._config_dir)
@ -83,6 +85,17 @@ class _Settings(object):
with open(self._config_file_path, 'r') as settings_file:
self._config = yaml.load(settings_file.read())
# Load the configuration from Setuptools' ResourceManager API
self._default_config = yaml.load(pkg_resources.resource_string(__name__, "config.yaml"))
# We only either the user colour or the default colours to ease parsing logic.
if os.path.exists(self._colours_file_path):
with open(self._colours_file_path, 'r') as colours_file:
self.colours_config = yaml.load(colours_file.read())
else:
self.colours_config = yaml.load(pkg_resources.resource_string(__name__, "colours.yaml"))
def _load_cache(self):
"""
Load cached files.
@ -100,11 +113,45 @@ class _Settings(object):
with open(self._config_file_path, 'w') as settings_file:
settings_file.write(yaml.dump(self._config, default_flow_style=False))
def get(self, key, default=None):
def get(self, key, *sections):
"""
Return config value.
Return their configuration key in a specified section
By default it looks in play_settings.
"""
return self._config.get(key, default)
section = self.get_section(*sections)
try:
return section[key]
except (KeyError, TypeError):
section = self.get_default_config_section(*sections)
return section[key]
def _get_section(self, config, *sections):
config = config.copy()
for section in sections:
config = config[section]
return config
def get_section(self, *sections):
"""
Get a section from the user configuration file if it can find it,
else load it from the system config
"""
try:
return self._get_section(self._config, *sections)
except (KeyError, TypeError):
return self._get_section(self._default_config, *sections)
def get_default_config_section(self, *sections):
"""
Always get a section from the default/system configuration. You would use this whenever
you need to loop through all the values in a section. In the user config they might be
incomplete.
"""
return self._get_section(self._default_config, *sections)
def edit(self):
"""

View file

@ -17,6 +17,7 @@ from clay.player import player
from clay.gp import gp
from clay.clipboard import copy
from clay.settings import settings
from clay.hotkeys import hotkey_manager
class SongListItem(urwid.Pile):
@ -143,19 +144,7 @@ class SongListItem(urwid.Pile):
"""
Handle keypress.
"""
if key == 'enter':
urwid.emit_signal(self, 'activate', self)
return None
elif key == 'ctrl a':
urwid.emit_signal(self, 'append-requested', self)
elif key == 'ctrl u':
if not self.is_currently_played:
urwid.emit_signal(self, 'unappend-requested', self)
elif key == 'ctrl p':
urwid.emit_signal(self, 'station-requested', self)
elif key == 'meta m':
urwid.emit_signal(self, 'context-menu-requested', self)
return super(SongListItem, self).keypress(size, key)
return hotkey_manager.keypress("library_item", self, super(SongListItem, self), size, key)
def mouse_event(self, size, event, button, col, row, focus):
"""
@ -166,6 +155,40 @@ class SongListItem(urwid.Pile):
return None
return super(SongListItem, self).mouse_event(size, event, button, col, row, focus)
def _send_signal(self, signal):
urwid.emit_signal(self, signal, self)
def play(self):
"""
Play this song.
"""
self._send_signal("activate")
def append(self):
"""
Add this song to the queue.
"""
self._send_signal("append-requested")
def unappend(self):
"""
Remove this song from the queue.
"""
if not self.is_currently_played:
self._send_signal("unappend-requested")
def request_station(self):
"""
Create a Google Play Music radio for this song.
"""
self._send_signal("station-requested")
def show_context_menu(self):
"""
Display the context menu for this song.
"""
self._send_signal("context-menu-requested")
@property
def is_currently_played(self):
"""
@ -504,7 +527,7 @@ class SongListBox(urwid.Frame):
"""
Hide context menu.
"""
if self.popup is not None:
if self.popup is not None and self.is_context_menu_visible:
self.contents['body'] = (self.content, None)
self.app.unregister_cancel_action(self.popup.close)
self.popup = None
@ -582,33 +605,51 @@ class SongListBox(urwid.Frame):
songlistitem.set_index(i)
def keypress(self, size, key):
if self._is_filtering and key in ('up', 'down', 'home', 'end'):
return self.handle_filtered_keypress(key)
elif key == 'meta m' and self.is_context_menu_visible:
self.hide_context_menu()
return None
elif key in ascii_letters + digits + ' _-.,?!()[]/':
if key in ascii_letters + digits + ' _-.,?!()[]/':
self.perform_filtering(key)
elif key == 'backspace':
self.perform_filtering(key)
return super(SongListBox, self).keypress(size, key)
elif self._is_filtering:
return hotkey_manager.keypress("library_view", self, super(SongListBox, self),
size, key)
else:
return super(SongListBox, self).keypress(size, key)
def handle_filtered_keypress(self, key):
"""
Handle up/down/home/end keypress while in fitering mode.
"""
return None
def _get_filtered(self):
"""Get filtered list of items"""
matches = self.get_filtered_items()
if not matches:
return False
_, index = self.walker.get_focus()
if key == 'home':
self.list_box.set_focus(matches[0].index, 'below')
elif key == 'end':
self.list_box.set_focus(matches[-1].index, 'above')
elif key == 'up':
self.list_box.set_focus(*self.get_prev_item(matches, index))
else:
self.list_box.set_focus(*self.get_next_item(matches, index))
return (matches, index)
def move_to_beginning(self):
"""Move to the focus to beginning of the songlist"""
matches, _ = self._get_filtered()
self.list_box.set_focus(matches[0].index, 'below')
return False
def move_to_end(self):
"""Move to the focus to end of the songlist"""
matches, _ = self._get_filtered()
self.list_box.set_focus(matches[-1].index, 'above')
return False
def move_up(self):
"""Move the focus an item up in the playlist"""
matches, index = self._get_filtered()
self.list_box.set_focus(*self.get_prev_item(matches, index))
return False
def move_down(self):
"""Move the focus an item down in the playlist """
matches, index = self._get_filtered()
self.list_box.set_focus(*self.get_next_item(matches, index))
return False
@staticmethod

View file

@ -21,6 +21,8 @@ setup(
'console_scripts': [
'clay=clay.app:main'
]
}
},
package_data={
'clay': ['config.yaml', 'colours.yaml'],
},
)