mirror of
https://github.com/vale981/clay
synced 2025-03-04 17:11:41 -05:00
Merge pull request #20 from and3rson/configurable-keybinds
Configurable keybinds
This commit is contained in:
commit
98c96bd462
15 changed files with 503 additions and 188 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -104,3 +104,7 @@ ENV/
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
#*
|
||||||
|
*~
|
||||||
|
|
|
@ -4,6 +4,7 @@ max-args = 8
|
||||||
|
|
||||||
[messages control]
|
[messages control]
|
||||||
disable =
|
disable =
|
||||||
|
duplicate-code,
|
||||||
too-few-public-methods,
|
too-few-public-methods,
|
||||||
too-many-public-methods,
|
too-many-public-methods,
|
||||||
too-many-instance-attributes,
|
too-many-instance-attributes,
|
||||||
|
|
125
clay/app.py
125
clay/app.py
|
@ -24,59 +24,7 @@ from clay.pages.settings import SettingsPage
|
||||||
from clay.settings import settings
|
from clay.settings import settings
|
||||||
from clay.notifications import notification_area
|
from clay.notifications import notification_area
|
||||||
from clay.gp import gp
|
from clay.gp import gp
|
||||||
|
from clay.hotkeys import hotkey_manager
|
||||||
|
|
||||||
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'),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AppWidget(urwid.Frame):
|
class AppWidget(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
|
@ -84,20 +32,6 @@ class AppWidget(urwid.Frame):
|
||||||
|
|
||||||
Handles tab switches, global keypresses etc.
|
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):
|
class Tab(urwid.Text):
|
||||||
"""
|
"""
|
||||||
Represents a single tab in header tabbar.
|
Represents a single tab in header tabbar.
|
||||||
|
@ -138,12 +72,7 @@ class AppWidget(urwid.Frame):
|
||||||
SearchPage(self),
|
SearchPage(self),
|
||||||
SettingsPage(self)
|
SettingsPage(self)
|
||||||
]
|
]
|
||||||
self.tabs = [
|
self.tabs = [AppWidget.Tab(page) for page in self.pages]
|
||||||
AppWidget.Tab(page)
|
|
||||||
for page
|
|
||||||
in self.pages
|
|
||||||
]
|
|
||||||
|
|
||||||
self.current_page = None
|
self.current_page = None
|
||||||
self.loop = None
|
self.loop = None
|
||||||
|
|
||||||
|
@ -176,11 +105,8 @@ class AppWidget(urwid.Frame):
|
||||||
|
|
||||||
Request user authorization.
|
Request user authorization.
|
||||||
"""
|
"""
|
||||||
username, password, device_id, authtoken = [
|
authtoken, device_id, _, password, username = settings.get_section("play_settings").values()
|
||||||
settings.get(x)
|
|
||||||
for x
|
|
||||||
in ('username', 'password', 'device_id', 'authtoken')
|
|
||||||
]
|
|
||||||
if self._login_notification:
|
if self._login_notification:
|
||||||
self._login_notification.close()
|
self._login_notification.close()
|
||||||
if use_token and authtoken:
|
if use_token and authtoken:
|
||||||
|
@ -238,7 +164,7 @@ class AppWidget(urwid.Frame):
|
||||||
return
|
return
|
||||||
|
|
||||||
with settings.edit() as config:
|
with settings.edit() as config:
|
||||||
config['authtoken'] = gp.get_authtoken()
|
config['play_settings']['authtoken'] = gp.get_authtoken()
|
||||||
|
|
||||||
self._login_notification.close()
|
self._login_notification.close()
|
||||||
|
|
||||||
|
@ -297,11 +223,7 @@ class AppWidget(urwid.Frame):
|
||||||
self.set_page(tab.page.__class__.__name__)
|
self.set_page(tab.page.__class__.__name__)
|
||||||
return
|
return
|
||||||
|
|
||||||
method_name = AppWidget.KEYBINDS.get(key)
|
hotkey_manager.keypress("global", self, super(AppWidget, self), size, key)
|
||||||
if method_name:
|
|
||||||
getattr(self, method_name)()
|
|
||||||
else:
|
|
||||||
super(AppWidget, self).keypress(size, key)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def seek_start():
|
def seek_start():
|
||||||
|
@ -324,6 +246,13 @@ class AppWidget(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
player.next(True)
|
player.next(True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prev_song():
|
||||||
|
"""
|
||||||
|
Play the previous song.
|
||||||
|
"""
|
||||||
|
player.prev(True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def seek_backward():
|
def seek_backward():
|
||||||
"""
|
"""
|
||||||
|
@ -352,14 +281,14 @@ class AppWidget(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
player.set_repeat_one(not player.get_is_repeat_one())
|
player.set_repeat_one(not player.get_is_repeat_one())
|
||||||
|
|
||||||
@staticmethod
|
def quit(self):
|
||||||
def quit():
|
|
||||||
"""
|
"""
|
||||||
Quit app.
|
Quit app.
|
||||||
"""
|
"""
|
||||||
|
self.loop = None
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def handle_escape_action(self):
|
def handle_escape(self):
|
||||||
"""
|
"""
|
||||||
Run escape actions. If none are pending, close newest notification.
|
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
|
An argparser action for multiple lines so we can display the copyright notice
|
||||||
Based on: https://stackoverflow.com/a/41147122
|
Based on: https://stackoverflow.com/a/41147122
|
||||||
"""
|
"""
|
||||||
version = "0.6.2"
|
|
||||||
author = "Andrew Dunai"
|
|
||||||
|
|
||||||
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
def __init__(self, option_strings, dest, nargs=None, **kwargs):
|
||||||
if nargs is not None:
|
if nargs is not None:
|
||||||
raise ValueError("nargs not allowed")
|
raise ValueError("nargs not allowed")
|
||||||
|
@ -404,15 +330,17 @@ def main():
|
||||||
|
|
||||||
parser.add_argument("-v", "--version", action=MultilineVersionAction)
|
parser.add_argument("-v", "--version", action=MultilineVersionAction)
|
||||||
|
|
||||||
parser.add_argument(
|
keybinds_group = parser.add_mutually_exclusive_group()
|
||||||
|
|
||||||
|
keybinds_group.add_argument(
|
||||||
"--with-x-keybinds",
|
"--with-x-keybinds",
|
||||||
help="define global X keybinds (requires Keybinder and PyGObject)",
|
help="define global X keybinds (requires Keybinder and PyGObject)",
|
||||||
action='store_true'
|
action='store_true'
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
keybinds_group.add_argument(
|
||||||
"--transparent",
|
"--without-x-keybinds",
|
||||||
help="use transparent background",
|
help="Don't define global keybinds (overrides configuration file)",
|
||||||
action='store_true'
|
action='store_true'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -421,12 +349,17 @@ def main():
|
||||||
if args.version:
|
if args.version:
|
||||||
exit(0)
|
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()
|
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
|
# Run the actual program
|
||||||
app_widget = AppWidget()
|
app_widget = AppWidget()
|
||||||
loop = urwid.MainLoop(app_widget, create_palette(args.transparent))
|
loop = urwid.MainLoop(app_widget, palette)
|
||||||
app_widget.set_loop(loop)
|
app_widget.set_loop(loop)
|
||||||
loop.screen.set_terminal_properties(256)
|
loop.screen.set_terminal_properties(256)
|
||||||
loop.run()
|
loop.run()
|
||||||
|
|
95
clay/colours.yaml
Normal file
95
clay/colours.yaml
Normal 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
58
clay/config.yaml
Normal 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:
|
|
@ -12,7 +12,6 @@ from gmusicapi.clients import Mobileclient
|
||||||
from clay.eventhook import EventHook
|
from clay.eventhook import EventHook
|
||||||
from clay.log import logger
|
from clay.log import logger
|
||||||
|
|
||||||
|
|
||||||
def asynchronous(func):
|
def asynchronous(func):
|
||||||
"""
|
"""
|
||||||
Decorates a function to become asynchronous.
|
Decorates a function to become asynchronous.
|
||||||
|
@ -45,6 +44,7 @@ def asynchronous(func):
|
||||||
callback(result, None, **extra)
|
callback(result, None, **extra)
|
||||||
|
|
||||||
Thread(target=process).start()
|
Thread(target=process).start()
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -44,14 +44,9 @@ class _HotkeyManager(object):
|
||||||
Manages configs.
|
Manages configs.
|
||||||
Runs Gtk main loop in a thread.
|
Runs Gtk main loop in a thread.
|
||||||
"""
|
"""
|
||||||
DEFAULT_HOTKEYS = {
|
|
||||||
'play_pause': 'XF86AudioPlay',
|
|
||||||
'next': 'XF86AudioNext',
|
|
||||||
'prev': 'XF86AudioPrev'
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.hotkeys = {}
|
self._x_hotkeys = {}
|
||||||
|
self._hotkeys = self._parse_hotkeys()
|
||||||
self.config = None
|
self.config = None
|
||||||
|
|
||||||
self.play_pause = EventHook()
|
self.play_pause = EventHook()
|
||||||
|
@ -61,7 +56,6 @@ class _HotkeyManager(object):
|
||||||
if IS_INIT:
|
if IS_INIT:
|
||||||
Keybinder.init()
|
Keybinder.init()
|
||||||
self.initialize()
|
self.initialize()
|
||||||
|
|
||||||
threading.Thread(target=Gtk.main).start()
|
threading.Thread(target=Gtk.main).start()
|
||||||
else:
|
else:
|
||||||
logger.debug("Not loading the global shortcuts.")
|
logger.debug("Not loading the global shortcuts.")
|
||||||
|
@ -72,24 +66,92 @@ class _HotkeyManager(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@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', {})
|
key = key.strip()
|
||||||
for operation, default_key in _HotkeyManager.DEFAULT_HOTKEYS.items():
|
|
||||||
if operation not in hotkeys or not hotkeys[operation]:
|
if key == "meta":
|
||||||
hotkeys[operation] = default_key
|
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
|
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):
|
def initialize(self):
|
||||||
"""
|
"""
|
||||||
Unbind previous hotkeys, re-read config & bind new hotkeys.
|
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)
|
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)
|
Keybinder.bind(key, self.fire_hook, operation)
|
||||||
|
|
||||||
def fire_hook(self, key, operation):
|
def fire_hook(self, key, operation):
|
||||||
|
|
|
@ -7,6 +7,7 @@ from clay.pages.page import AbstractPage
|
||||||
from clay.log import logger
|
from clay.log import logger
|
||||||
from clay.clipboard import copy
|
from clay.clipboard import copy
|
||||||
from clay.gp import gp
|
from clay.gp import gp
|
||||||
|
from clay.hotkeys import hotkey_manager
|
||||||
|
|
||||||
|
|
||||||
class DebugItem(urwid.AttrMap):
|
class DebugItem(urwid.AttrMap):
|
||||||
|
@ -36,10 +37,12 @@ class DebugItem(urwid.AttrMap):
|
||||||
"""
|
"""
|
||||||
Handle heypress.
|
Handle heypress.
|
||||||
"""
|
"""
|
||||||
if key == 'enter':
|
return hotkey_manager.keypress("debug_page", self, None, None, key)
|
||||||
copy(self.log_record.formatted_message)
|
|
||||||
return None
|
def copy_message(self):
|
||||||
return key
|
"""Copy the selected error message to the clipboard"""
|
||||||
|
copy(self.log_record.formatted_message)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class DebugPage(urwid.Pile, AbstractPage):
|
class DebugPage(urwid.Pile, AbstractPage):
|
||||||
|
|
|
@ -7,6 +7,7 @@ from clay.gp import gp
|
||||||
from clay.songlist import SongListBox
|
from clay.songlist import SongListBox
|
||||||
from clay.notifications import notification_area
|
from clay.notifications import notification_area
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
|
from clay.hotkeys import hotkey_manager
|
||||||
|
|
||||||
|
|
||||||
class MyPlaylistListItem(urwid.Columns):
|
class MyPlaylistListItem(urwid.Columns):
|
||||||
|
@ -33,10 +34,15 @@ class MyPlaylistListItem(urwid.Columns):
|
||||||
"""
|
"""
|
||||||
Handle keypress.
|
Handle keypress.
|
||||||
"""
|
"""
|
||||||
if key == 'enter':
|
return hotkey_manager.keypress("playlist_page", self, super(MyPlaylistListItem, self),
|
||||||
urwid.emit_signal(self, 'activate', self)
|
size, key)
|
||||||
return None
|
|
||||||
return super(MyPlaylistListItem, self).keypress(size, key)
|
def start_playlist(self):
|
||||||
|
"""
|
||||||
|
Start playing the selected playlist
|
||||||
|
"""
|
||||||
|
urwid.emit_signal(self, 'activate', self)
|
||||||
|
return None
|
||||||
|
|
||||||
def get_tracks(self):
|
def get_tracks(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -6,6 +6,7 @@ import urwid
|
||||||
from clay.gp import gp
|
from clay.gp import gp
|
||||||
from clay.songlist import SongListBox
|
from clay.songlist import SongListBox
|
||||||
from clay.notifications import notification_area
|
from clay.notifications import notification_area
|
||||||
|
from clay.hotkeys import hotkey_manager
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,10 +36,14 @@ class SearchBox(urwid.Columns):
|
||||||
"""
|
"""
|
||||||
Handle keypress.
|
Handle keypress.
|
||||||
"""
|
"""
|
||||||
if key == 'enter':
|
return hotkey_manager.keypress("search_page", self, super(SearchBox, self), size, key)
|
||||||
urwid.emit_signal(self, 'search-requested', self.query.edit_text)
|
|
||||||
return None
|
def send_query(self):
|
||||||
return super(SearchBox, self).keypress(size, key)
|
"""
|
||||||
|
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):
|
class SearchPage(urwid.Pile, AbstractPage):
|
||||||
|
|
|
@ -6,6 +6,7 @@ import urwid
|
||||||
from clay.pages.page import AbstractPage
|
from clay.pages.page import AbstractPage
|
||||||
from clay.settings import settings
|
from clay.settings import settings
|
||||||
from clay.player import player
|
from clay.player import player
|
||||||
|
from clay.hotkeys import hotkey_manager
|
||||||
|
|
||||||
|
|
||||||
class Slider(urwid.Widget):
|
class Slider(urwid.Widget):
|
||||||
|
@ -80,18 +81,27 @@ class Slider(urwid.Widget):
|
||||||
"""
|
"""
|
||||||
Handle equalizer band modification.
|
Handle equalizer band modification.
|
||||||
"""
|
"""
|
||||||
if key == '+':
|
return hotkey_manager.keypress("settings_page", self, None, None, key)
|
||||||
if self.value < self.max_value:
|
|
||||||
self.value += 1
|
def equalizer_up(self):
|
||||||
self.update()
|
"""
|
||||||
return None
|
Turn the equalizer band up
|
||||||
elif key == '-':
|
"""
|
||||||
if self.value > -self.max_value:
|
if self.value < self.max_value:
|
||||||
self.value -= 1
|
self.value += 1
|
||||||
self.update()
|
self.update()
|
||||||
return None
|
|
||||||
else:
|
return None
|
||||||
return key
|
|
||||||
|
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):
|
def update(self):
|
||||||
"""
|
"""
|
||||||
|
@ -131,17 +141,17 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
||||||
def __init__(self, app):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.username = urwid.Edit(
|
self.username = urwid.Edit(
|
||||||
edit_text=settings.get('username', '')
|
edit_text=settings.get('username', 'play_settings')
|
||||||
)
|
)
|
||||||
self.password = urwid.Edit(
|
self.password = urwid.Edit(
|
||||||
mask='*', edit_text=settings.get('password', '')
|
mask='*', edit_text=settings.get('password', 'play_settings')
|
||||||
)
|
)
|
||||||
self.device_id = urwid.Edit(
|
self.device_id = urwid.Edit(
|
||||||
edit_text=settings.get('device_id', '')
|
edit_text=settings.get('device_id', 'play_settings')
|
||||||
)
|
)
|
||||||
self.download_tracks = urwid.CheckBox(
|
self.download_tracks = urwid.CheckBox(
|
||||||
'Download tracks before playback',
|
'Download tracks before playback',
|
||||||
state=settings.get('download_tracks', False)
|
state=settings.get('download_tracks', 'play_settings')
|
||||||
)
|
)
|
||||||
self.equalizer = Equalizer()
|
self.equalizer = Equalizer()
|
||||||
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
|
super(SettingsPage, self).__init__([urwid.ListBox(urwid.SimpleListWalker([
|
||||||
|
@ -170,10 +180,10 @@ class SettingsPage(urwid.Columns, AbstractPage):
|
||||||
Called when "Save" button is pressed.
|
Called when "Save" button is pressed.
|
||||||
"""
|
"""
|
||||||
with settings.edit() as config:
|
with settings.edit() as config:
|
||||||
config['username'] = self.username.edit_text
|
config['play_settings']['username'] = self.username.edit_text
|
||||||
config['password'] = self.password.edit_text
|
config['play_settings']['password'] = self.password.edit_text
|
||||||
config['device_id'] = self.device_id.edit_text
|
config['play_settings']['device_id'] = self.device_id.edit_text
|
||||||
config['download_tracks'] = self.download_tracks.state
|
config['play_settings']['download_tracks'] = self.download_tracks.state
|
||||||
|
|
||||||
self.app.set_page('MyLibraryPage')
|
self.app.set_page('MyLibraryPage')
|
||||||
self.app.log_in()
|
self.app.log_in()
|
||||||
|
|
|
@ -4,6 +4,7 @@ Media player built using libVLC.
|
||||||
# pylint: disable=too-many-instance-attributes
|
# pylint: disable=too-many-instance-attributes
|
||||||
# pylint: disable=too-many-public-methods
|
# pylint: disable=too-many-public-methods
|
||||||
from random import randint
|
from random import randint
|
||||||
|
from ctypes import CFUNCTYPE, c_void_p, c_int, c_char_p
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ class _Queue(object):
|
||||||
self.repeat_one = False
|
self.repeat_one = False
|
||||||
|
|
||||||
self.tracks = []
|
self.tracks = []
|
||||||
|
self._played_tracks = []
|
||||||
self.current_track_index = None
|
self.current_track_index = None
|
||||||
|
|
||||||
def load(self, tracks, current_track_index=None):
|
def load(self, tracks, current_track_index=None):
|
||||||
|
@ -91,6 +93,8 @@ class _Queue(object):
|
||||||
if not self.tracks:
|
if not self.tracks:
|
||||||
return None
|
return None
|
||||||
self.current_track_index = self.tracks[0]
|
self.current_track_index = self.tracks[0]
|
||||||
|
else:
|
||||||
|
self._played_tracks.append(self.current_track_index)
|
||||||
|
|
||||||
if self.repeat_one and not force:
|
if self.repeat_one and not force:
|
||||||
return self.get_current_track()
|
return self.get_current_track()
|
||||||
|
@ -105,12 +109,39 @@ class _Queue(object):
|
||||||
|
|
||||||
return self.get_current_track()
|
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):
|
def get_tracks(self):
|
||||||
"""
|
"""
|
||||||
Return current queue, i.e. a list of :class:`Track` instances.
|
Return current queue, i.e. a list of :class:`Track` instances.
|
||||||
"""
|
"""
|
||||||
return self.tracks
|
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):
|
class _Player(object):
|
||||||
"""
|
"""
|
||||||
|
@ -129,6 +160,15 @@ class _Player(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.instance = vlc.Instance()
|
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(
|
self.instance.set_user_agent(
|
||||||
meta.APP_NAME,
|
meta.APP_NAME,
|
||||||
meta.USER_AGENT
|
meta.USER_AGENT
|
||||||
|
@ -154,9 +194,7 @@ class _Player(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
self.equalizer = vlc.libvlc_audio_equalizer_new()
|
self.equalizer = vlc.libvlc_audio_equalizer_new()
|
||||||
|
|
||||||
self.media_player.set_equalizer(self.equalizer)
|
self.media_player.set_equalizer(self.equalizer)
|
||||||
|
|
||||||
self._create_station_notification = None
|
self._create_station_notification = None
|
||||||
self._is_loading = False
|
self._is_loading = False
|
||||||
self.queue = _Queue()
|
self.queue = _Queue()
|
||||||
|
@ -331,8 +369,10 @@ class _Player(object):
|
||||||
self.broadcast_state()
|
self.broadcast_state()
|
||||||
self.track_changed.fire(track)
|
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)
|
path = settings.get_cached_file_path(track.filename)
|
||||||
|
|
||||||
if path is None:
|
if path is None:
|
||||||
logger.debug('Track %s not in cache, downloading...', track.store_id)
|
logger.debug('Track %s not in cache, downloading...', track.store_id)
|
||||||
track.get_url(callback=self._download_track)
|
track.get_url(callback=self._download_track)
|
||||||
|
@ -425,6 +465,14 @@ class _Player(object):
|
||||||
self.queue.next(force)
|
self.queue.next(force)
|
||||||
self._play()
|
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):
|
def get_current_track(self):
|
||||||
"""
|
"""
|
||||||
Return currently played track.
|
Return currently played track.
|
||||||
|
|
|
@ -6,8 +6,8 @@ import os
|
||||||
import copy
|
import copy
|
||||||
import errno
|
import errno
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
import appdirs
|
import appdirs
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
|
||||||
class _SettingsEditor(dict):
|
class _SettingsEditor(dict):
|
||||||
|
@ -42,6 +42,7 @@ class _Settings(object):
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._config = {}
|
self._config = {}
|
||||||
|
self._default_config = {}
|
||||||
self._cached_files = set()
|
self._cached_files = set()
|
||||||
|
|
||||||
self._config_dir = None
|
self._config_dir = None
|
||||||
|
@ -58,6 +59,7 @@ class _Settings(object):
|
||||||
"""
|
"""
|
||||||
self._config_dir = appdirs.user_config_dir('clay', 'Clay')
|
self._config_dir = appdirs.user_config_dir('clay', 'Clay')
|
||||||
self._config_file_path = os.path.join(self._config_dir, 'config.yaml')
|
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:
|
try:
|
||||||
os.makedirs(self._config_dir)
|
os.makedirs(self._config_dir)
|
||||||
|
@ -83,6 +85,17 @@ class _Settings(object):
|
||||||
with open(self._config_file_path, 'r') as settings_file:
|
with open(self._config_file_path, 'r') as settings_file:
|
||||||
self._config = yaml.load(settings_file.read())
|
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):
|
def _load_cache(self):
|
||||||
"""
|
"""
|
||||||
Load cached files.
|
Load cached files.
|
||||||
|
@ -100,11 +113,45 @@ class _Settings(object):
|
||||||
with open(self._config_file_path, 'w') as settings_file:
|
with open(self._config_file_path, 'w') as settings_file:
|
||||||
settings_file.write(yaml.dump(self._config, default_flow_style=False))
|
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):
|
def edit(self):
|
||||||
"""
|
"""
|
||||||
|
|
107
clay/songlist.py
107
clay/songlist.py
|
@ -17,6 +17,7 @@ from clay.player import player
|
||||||
from clay.gp import gp
|
from clay.gp import gp
|
||||||
from clay.clipboard import copy
|
from clay.clipboard import copy
|
||||||
from clay.settings import settings
|
from clay.settings import settings
|
||||||
|
from clay.hotkeys import hotkey_manager
|
||||||
|
|
||||||
|
|
||||||
class SongListItem(urwid.Pile):
|
class SongListItem(urwid.Pile):
|
||||||
|
@ -143,19 +144,7 @@ class SongListItem(urwid.Pile):
|
||||||
"""
|
"""
|
||||||
Handle keypress.
|
Handle keypress.
|
||||||
"""
|
"""
|
||||||
if key == 'enter':
|
return hotkey_manager.keypress("library_item", self, super(SongListItem, self), size, key)
|
||||||
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)
|
|
||||||
|
|
||||||
def mouse_event(self, size, event, button, col, row, focus):
|
def mouse_event(self, size, event, button, col, row, focus):
|
||||||
"""
|
"""
|
||||||
|
@ -166,6 +155,40 @@ class SongListItem(urwid.Pile):
|
||||||
return None
|
return None
|
||||||
return super(SongListItem, self).mouse_event(size, event, button, col, row, focus)
|
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
|
@property
|
||||||
def is_currently_played(self):
|
def is_currently_played(self):
|
||||||
"""
|
"""
|
||||||
|
@ -504,7 +527,7 @@ class SongListBox(urwid.Frame):
|
||||||
"""
|
"""
|
||||||
Hide context menu.
|
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.contents['body'] = (self.content, None)
|
||||||
self.app.unregister_cancel_action(self.popup.close)
|
self.app.unregister_cancel_action(self.popup.close)
|
||||||
self.popup = None
|
self.popup = None
|
||||||
|
@ -582,33 +605,51 @@ class SongListBox(urwid.Frame):
|
||||||
songlistitem.set_index(i)
|
songlistitem.set_index(i)
|
||||||
|
|
||||||
def keypress(self, size, key):
|
def keypress(self, size, key):
|
||||||
if self._is_filtering and key in ('up', 'down', 'home', 'end'):
|
if key in ascii_letters + digits + ' _-.,?!()[]/':
|
||||||
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 + ' _-.,?!()[]/':
|
|
||||||
self.perform_filtering(key)
|
self.perform_filtering(key)
|
||||||
elif key == 'backspace':
|
elif key == 'backspace':
|
||||||
self.perform_filtering(key)
|
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):
|
return None
|
||||||
"""
|
|
||||||
Handle up/down/home/end keypress while in fitering mode.
|
def _get_filtered(self):
|
||||||
"""
|
"""Get filtered list of items"""
|
||||||
matches = self.get_filtered_items()
|
matches = self.get_filtered_items()
|
||||||
|
|
||||||
if not matches:
|
if not matches:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
_, index = self.walker.get_focus()
|
_, index = self.walker.get_focus()
|
||||||
if key == 'home':
|
|
||||||
self.list_box.set_focus(matches[0].index, 'below')
|
return (matches, index)
|
||||||
elif key == 'end':
|
|
||||||
self.list_box.set_focus(matches[-1].index, 'above')
|
def move_to_beginning(self):
|
||||||
elif key == 'up':
|
"""Move to the focus to beginning of the songlist"""
|
||||||
self.list_box.set_focus(*self.get_prev_item(matches, index))
|
matches, _ = self._get_filtered()
|
||||||
else:
|
self.list_box.set_focus(matches[0].index, 'below')
|
||||||
self.list_box.set_focus(*self.get_next_item(matches, index))
|
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
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
6
setup.py
6
setup.py
|
@ -21,6 +21,8 @@ setup(
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'clay=clay.app:main'
|
'clay=clay.app:main'
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
package_data={
|
||||||
|
'clay': ['config.yaml', 'colours.yaml'],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue