You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
407 lines
12 KiB
407 lines
12 KiB
#!/usr/bin/env python3 |
|
# -*- coding: utf-8 -*- |
|
|
|
# Copyright (c) 2014-2015 Muges |
|
# |
|
# Permission is hereby granted, free of charge, to any person |
|
# obtaining a copy of this software and associated documentation files |
|
# (the "Software"), to deal in the Software without restriction, |
|
# including without limitation the rights to use, copy, modify, merge, |
|
# publish, distribute, sublicense, and/or sell copies of the Software, |
|
# and to permit persons to whom the Software is furnished to do so, |
|
# subject to the following conditions: |
|
# |
|
# The above copyright notice and this permission notice shall be |
|
# included in all copies or substantial portions of the Software. |
|
# |
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
|
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
|
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
# SOFTWARE. |
|
|
|
import curses |
|
import sys |
|
|
|
class OneLineWidget: |
|
""" |
|
Abstract class representing a one line widget |
|
""" |
|
def __init__(self, parent): |
|
""" |
|
Initialize the widget. |
|
|
|
- parent is the curses.window object in which the widget will be |
|
drawn |
|
""" |
|
self.parent = parent |
|
|
|
def draw(self, y, width, selected=False): |
|
""" |
|
Abstract method used to draw the widget. |
|
|
|
- y is the number of the line at which the widget will be drawn. |
|
- selected is a boolean indicating if the widget is selected if |
|
the parent is a ScrollableList |
|
""" |
|
raise NotImplementedError() |
|
|
|
def on_key(self, c, ui): |
|
""" |
|
Method called when the user types the key c and the widget is |
|
selected. |
|
""" |
|
return False |
|
|
|
class VolumeWidget(OneLineWidget): |
|
""" |
|
Widget used to set the volume of a Volume object |
|
""" |
|
def __init__(self, parent, volume, namesw): |
|
""" |
|
Initialize the object. |
|
|
|
- parent is the curses.window object in which the widget will be |
|
drawn |
|
- volume is a Volume object |
|
- namesw is the width taken by the names of the Volume objects |
|
""" |
|
OneLineWidget.__init__(self, parent) |
|
self.volume = volume |
|
self.namesw = namesw |
|
|
|
def draw(self, y, width, selected=False): |
|
# Highlight the name if the widget is selected |
|
if selected: |
|
attribute = curses.A_REVERSE |
|
else: |
|
attribute = 0 |
|
|
|
# Draw the name |
|
self.parent.addstr(y, 0, "│"+self.volume.name, attribute) |
|
|
|
# Position and width of the slider |
|
slidex = int(self.namesw+5) |
|
slidew = int(width-slidex-2-1) |
|
slidewleft = int((self.volume.get_volume()*slidew)/100) |
|
slidewright = int(slidew-slidewleft) |
|
|
|
# Draw the slider |
|
self.parent.addstr(y, slidex-2, "") |
|
self.parent.addstr(y, slidex+slidew, "│") |
|
|
|
self.parent.addstr(y, slidex-1, "▒"*slidewleft+"░") |
|
self.parent.addstr(y, slidex+slidewleft, " "*slidewright) |
|
|
|
def on_key(self, c, ui): |
|
if c in (curses.KEY_LEFT, ord('-')): |
|
# Increase the volume |
|
self.volume.inc_volume(-1) |
|
elif c in (curses.KEY_RIGHT, ord('+')): |
|
# Decrease the volume |
|
self.volume.inc_volume(1) |
|
elif c == ord('m'): |
|
# Mute |
|
self.volume.set_volume(0) |
|
# Full volume |
|
elif c == ord('0'): |
|
self.volume.set_volume(100) |
|
# Select volume |
|
elif chr(c) in "123456789": |
|
self.volume.set_volume(int(chr(c)) * 10) |
|
else: |
|
return False |
|
return True |
|
|
|
class ScrollableList: |
|
""" |
|
Object representing a list of OneLineWidgets that can be browsed and |
|
scrolled through |
|
""" |
|
def __init__(self): |
|
self.set_widgets([]) |
|
|
|
# Number of the line which is at the top of the widget (used for |
|
# scrolling) |
|
self.top = 0 |
|
|
|
self.pad = curses.newpad(1,1) |
|
|
|
def set_widgets(self, widgets, default=0): |
|
self.widgets = widgets |
|
self.set_selection(default) |
|
self.height = len(widgets) |
|
|
|
def get_selection(self): |
|
try: |
|
return self.widgets[self.selection] |
|
except IndexError: |
|
return None |
|
|
|
def set_selection(self, selection): |
|
""" |
|
Set the selection, ensuring that the selected widget exists. |
|
""" |
|
# Ensure that the widget is in the list |
|
selection = min(selection, len(self.widgets)-1) |
|
|
|
if selection < 0: |
|
selection = 0 |
|
else: |
|
# If the widget is None, find the first widget before the |
|
# selection that is not None |
|
while (selection >= 0 and self.widgets[selection] == None): |
|
selection -= 1 |
|
|
|
if selection < 0: |
|
# If there isn't one, find the first widget after the |
|
# selection that is not None |
|
selection = 0 |
|
while (selection < len(self.widgets) and self.widgets[selection] == None): |
|
selection += 1 |
|
|
|
# If there still isn't one (all the widgets are equal to |
|
# None, set the selection to 0 |
|
if selection >= len(self.widgets): |
|
selection = 0 |
|
|
|
self.selection = selection |
|
|
|
def select_previous_widget(self): |
|
""" |
|
Select the first widget different to None preceding the current |
|
selection |
|
""" |
|
selection = self.selection-1 |
|
|
|
while (selection >= 0 and self.widgets[selection] == None): |
|
selection -= 1 |
|
|
|
self.set_selection(selection) |
|
|
|
def select_next_widget(self): |
|
""" |
|
Select the first widget different to None following the current |
|
selection |
|
""" |
|
selection = self.selection+1 |
|
|
|
while (selection < len(self.widgets) and self.widgets[selection] == None): |
|
selection += 1 |
|
|
|
self.set_selection(selection) |
|
|
|
def select_first_widget(self): |
|
self.set_selection(0) |
|
|
|
def select_last_widget(self): |
|
self.set_selection(len(self.widgets) - 1) |
|
|
|
def draw(self, stop, sleft, sbottom, sright): |
|
""" |
|
Draw the list in the portion of the screen delimited by the |
|
coordinates (stop, sleft, sbottom, sright) |
|
""" |
|
height = sbottom-stop |
|
width = sright-sleft |
|
|
|
self.pad.clear() |
|
self.pad.resize(max(1, self.height), max(1, width)) |
|
|
|
# Draw each widget |
|
y = 0 |
|
for w in self.widgets: |
|
if w != None: |
|
w.draw(y, width, y == self.selection) |
|
y += 1 |
|
|
|
ptop = int(max(0, min(self.selection - height/2, self.height-height-1))) |
|
self.pad.refresh(ptop, 0, stop, sleft, sbottom, sright) |
|
|
|
def on_key(self, c, ui): |
|
if c == curses.KEY_DOWN: |
|
self.select_next_widget() |
|
elif c == curses.KEY_UP: |
|
self.select_previous_widget() |
|
elif c == curses.KEY_PPAGE: # page up |
|
self.select_first_widget() |
|
elif c == curses.KEY_NPAGE: # page down |
|
self.select_last_widget() |
|
else: |
|
selection = self.get_selection() |
|
if selection != None: |
|
return selection.on_key(c, ui) |
|
else: |
|
return False |
|
return True |
|
|
|
class VolumeList(ScrollableList): |
|
""" |
|
List of VolumeWidgets |
|
""" |
|
def __init__(self, mastervolume): |
|
ScrollableList.__init__(self) |
|
self.mastervolume = mastervolume |
|
|
|
sounds = mastervolume.get_sounds() |
|
namesw = max(max([len(s.name) for s in sounds]), len(mastervolume.name)) |
|
|
|
widgets = [] |
|
widgets.append(VolumeWidget(self.pad, mastervolume, namesw)) |
|
widgets.append(None) |
|
for sound in sounds: |
|
widgets.append(VolumeWidget(self.pad, sound, namesw)) |
|
|
|
self.set_widgets(widgets, 2) |
|
|
|
def on_key(self, c, ui): |
|
if not ScrollableList.on_key(self, c, ui): |
|
if c == ord("s"): |
|
self.mastervolume.save_preset() |
|
else: |
|
return False |
|
return True |
|
|
|
class MessageView: |
|
""" |
|
Display a message at the center of the screen |
|
""" |
|
def __init__(self, message): |
|
""" |
|
Initialise the MessageView |
|
|
|
- message is a string |
|
""" |
|
self.message = message.split("\n") |
|
self.pad = curses.newpad(1,1) |
|
|
|
def draw(self, stop, sleft, sbottom, sright): |
|
""" |
|
Draw the message in the portion of the screen delimited by the |
|
coordinates (stop, sleft, sbottom, sright) |
|
""" |
|
height = sbottom-stop |
|
width = sright-sleft |
|
|
|
self.pad.clear() |
|
self.pad.resize(height, width) |
|
|
|
messageheight = len(self.message) |
|
|
|
y = (height-messageheight)/2 |
|
for text in self.message: |
|
x = (width-len(text))/2 |
|
self.pad.addstr(int(y), int(x), text) |
|
y += 1 |
|
|
|
self.pad.refresh(0, 0, stop, sleft, sbottom, sright) |
|
|
|
class LoadingView(MessageView): |
|
def __init__(self): |
|
MessageView.__init__(self, "Loading sounds...") |
|
|
|
class UI: |
|
def start(self): |
|
""" |
|
Start the application |
|
""" |
|
# Initialize curses |
|
self.screen = curses.initscr() |
|
curses.noecho() |
|
curses.cbreak() |
|
self.screen.keypad(1) |
|
curses.curs_set(0) |
|
|
|
self.resize() |
|
|
|
self.loadingview = LoadingView() |
|
self.current = self.loadingview |
|
|
|
self.update() |
|
|
|
def end(self): |
|
""" |
|
Stop the application |
|
""" |
|
# End curses |
|
curses.nocbreak() |
|
self.screen.keypad(0) |
|
curses.echo() |
|
curses.endwin() |
|
|
|
def fatal_error(self, error): |
|
""" |
|
Prints an error string to stderr then exits the application |
|
""" |
|
self.end() |
|
sys.stderr.write("Error: " + error + "\n") |
|
sys.stderr.flush() |
|
sys.exit(1) |
|
|
|
def resize(self): |
|
""" |
|
Method executed when the terminal is resized : set some |
|
constants depending on the screen size |
|
""" |
|
# Screen size |
|
self.screenh, self.screenw = self.screen.getmaxyx() |
|
|
|
# Horizontal and vertical padding (space between the edge of the |
|
# terminal and the text) |
|
if self.screenh > 13 and self.screenw > 60: |
|
self.hpadding = 5 |
|
self.vpadding = 3 |
|
else: |
|
self.hpadding = 1 |
|
self.vpadding = 1 |
|
|
|
def update(self): |
|
""" |
|
Update the screen |
|
""" |
|
|
|
self.screen.clear() |
|
self.screen.refresh() |
|
self.current.draw(self.vpadding, self.hpadding, |
|
self.screenh-self.vpadding-1, |
|
self.screenw-self.hpadding-1) |
|
|
|
def run(self, mastervolume): |
|
""" |
|
Start the main loop |
|
""" |
|
if len(mastervolume.get_sounds()) == 0: |
|
self.fatal_error("no sounds found") |
|
|
|
self.volumelist = VolumeList(mastervolume) |
|
|
|
self.current = self.volumelist |
|
|
|
self.resize() |
|
self.update() |
|
|
|
while True: |
|
# Wait for user input and handle it |
|
self.on_key(self.screen.getch(), self) |
|
self.update() |
|
|
|
def on_key(self, c, ui): |
|
""" |
|
Callback called when a key is pressed |
|
""" |
|
if c == ord('q'): |
|
# Quit |
|
self.end() |
|
sys.exit(0) |
|
elif c == curses.KEY_HOME: |
|
self.current = self.volumelist |
|
elif c == curses.KEY_RESIZE: |
|
# The terminal has been resized, update the display |
|
self.resize() |
|
else: |
|
# Propagate the key to the current view |
|
self.current.on_key(c, ui) |
|
|
|
|