rind: Implement a custom spin button for automatic pause controls

We have to do this because the "input" signal attached to a real
Gtk.SpinButton is broken, and likes to increment from -1 to 1, bypassing
0 all together.

Of course, this is only a problem when I try to have "This Track" and
"Next Track" text displayed instead of numbers, but I think everything
looks nicer this way.

Signed-off-by: Anna Schumaker <Anna@NoWheyCreamery.com>
This commit is contained in:
Anna Schumaker 2019-04-10 17:51:22 -04:00
parent a226305623
commit e09a4ed6ae
5 changed files with 206 additions and 22 deletions

View File

@ -7,11 +7,6 @@
<mime-type>inode/directory</mime-type>
</mime-types>
</object>
<object class="GtkAdjustment" id="pause_adjustment">
<property name="upper">99</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkAdjustment" id="progress_adjustment">
<property name="upper">100</property>
<property name="step_increment">5</property>
@ -663,16 +658,73 @@ audio-volume-medium-symbolic</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="pause_after">
<property name="can_focus">True</property>
<object class="GtkBox" id="pause_box">
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">media-playback-pause</property>
<property name="placeholder_text" translatable="yes">Keep Playing</property>
<property name="adjustment">pause_adjustment</property>
<child>
<object class="GtkEntry" id="pause_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">Paused</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">media-playback-pause</property>
<property name="primary_icon_activatable">False</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="pause_down">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-remove-symbolic</property>
</object>
</child>
<accelerator key="minus" signal="activate" modifiers="GDK_CONTROL_MASK"/>
<accelerator key="KP_Subtract" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="pause_up">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-add-symbolic</property>
</object>
</child>
<accelerator key="plus" signal="activate" modifiers="GDK_CONTROL_MASK"/>
<accelerator key="KP_Add" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="left_attach">2</property>

View File

@ -3,16 +3,22 @@ from . import gtk
import curds
import datetime
import gi
import re
import sys
gi.require_version("Gst", "1.0")
from gi.repository import Gst, GLib, Gdk
from gi.repository import Gst, GLib, Gdk, Gtk
Gst.init(sys.argv)
Adjustment = gtk.Builder.get_object("progress_adjustment")
Artist = gtk.Builder.get_object("artist")
Duration = gtk.Builder.get_object("duration")
NextButton = gtk.Builder.get_object("next_button")
PauseAdj = gtk.Builder.get_object("pause_adjustment")
PauseButton = gtk.Builder.get_object("pause_button")
PauseDown = gtk.Builder.get_object("pause_down")
PauseEntry = gtk.Builder.get_object("pause_entry")
PauseIcon = Gtk.EntryIconPosition.PRIMARY
PauseUp = gtk.Builder.get_object("pause_up")
PlayButton = gtk.Builder.get_object("play_button")
Position = gtk.Builder.get_object("position")
PrevButton = gtk.Builder.get_object("previous_button")
@ -23,9 +29,10 @@ VolumeAdj = gtk.Builder.get_object("volume_adjustment")
class EmmentalAudio:
def __init__(self):
self.playbin = Gst.ElementFactory.make("playbin")
self.bus = self.playbin.get_bus()
self.have_next = False
self.playbin = Gst.ElementFactory.make("playbin")
self.bus = self.playbin.get_bus()
self.have_next = False
self.pause_count = -1
self.bus.add_signal_watch()
@ -35,6 +42,9 @@ class EmmentalAudio:
self.prev_cb = PrevButton.connect( "clicked", self.previous)
self.next_cb = NextButton.connect( "clicked", self.next)
self.seek_cb = ProgScale.connect("change-value", self.seek)
self.up_cb = PauseUp.connect( "clicked", self.pause_inc)
self.down_cb = PauseDown.connect("clicked", self.pause_dec)
self.input_cb = PauseEntry.connect("activate", self.pause_input)
self.vol_cb = Volume.connect("value-changed", self.set_volume)
self.finish_cb = self.playbin.connect("about-to-finish",
@ -51,6 +61,8 @@ class EmmentalAudio:
self.playbin.disconnect(self.finish_cb)
self.bus.disconnect(self.bus_cb)
PauseButton.disconnect(self.pause_cb)
PauseUp.disconnect(self.up_cb)
PauseDown.disconnect(self.down_cb)
PlayButton.disconnect(self.play_cb)
NextButton.disconnect(self.next_cb)
PrevButton.disconnect(self.prev_cb)
@ -110,6 +122,39 @@ class EmmentalAudio:
def pause(self, *args):
self.playbin.set_state(Gst.State.PAUSED)
def pause_changed(self, state):
text = f"{self.pause_count} Tracks"
icon = "media-playback-pause"
if self.pause_count == -1:
if state == Gst.State.PLAYING:
text = "Keep Playing"
icon = "media-playback-start"
else:
text = "Paused"
elif self.pause_count == 0:
text = "This Track"
elif self.pause_count == 1:
text = "Next Track"
PauseEntry.set_text(text)
PauseEntry.set_icon_from_icon_name(PauseIcon, icon)
PauseDown.set_sensitive(self.pause_count > -1)
PauseUp.set_sensitive(self.pause_count < 99)
def pause_input(self, entry):
match = re.search("-?\d+", entry.get_text())
if match:
self.pause_count = max(-1, min(99, int(match.group(0))))
self.pause_changed(self.playbin.get_state(Gst.Format.TIME)[1])
def pause_dec(self, *args):
self.pause_count = max(-1, self.pause_count - 1)
self.pause_changed(self.playbin.get_state(Gst.Format.TIME)[1])
def pause_inc(self, *args):
self.pause_count = min(99, self.pause_count + 1)
self.pause_changed(self.playbin.get_state(Gst.Format.TIME)[1])
def play(self, *args):
self.playbin.set_state(Gst.State.PLAYING)
@ -152,6 +197,8 @@ class EmmentalAudio:
if state == Gst.State.READY:
Title.set_markup("<big>Emmental</big>")
Artist.set_markup("<big>The Cheesy Music Player</big>")
elif state in (Gst.State.PLAYING, Gst.State.PAUSED):
self.pause_changed(state)
curds.notify.notify("state-changed", state)
def update_progress(self):

View File

@ -10,7 +10,7 @@ Builder.add_from_file("emmental.ui")
UpArrow = Builder.get_object("up_arrow")
DownArrow = Builder.get_object("down_arrow")
PauseAfter = Builder.get_object("pause_after")
PauseBox = Builder.get_object("pause_box")
PlistSearch = Builder.get_object("playlist_search")
ShowMore = Builder.get_object("show_more")
TrackSearch = Builder.get_object("track_search")
@ -72,7 +72,7 @@ def show_more_toggled(self, *args):
active = ShowMore.get_active()
UpArrow.set_visible(active)
DownArrow.set_visible(not active)
PauseAfter.set_visible(active)
PauseBox.set_visible(active)
PlistSearch.set_visible(active)
TrackSearch.set_visible(active)

View File

@ -25,6 +25,8 @@ class TestGst(unittest.TestCase):
def tearDown(self):
self.audio.playbin.set_state(Gst.State.READY)
self.audio.seek(value=0)
self.audio.pause_count = -1
self.audio.pause_changed(Gst.State.PAUSED)
gtk.main_loop(delay=0.1)
self.audio.disconnect()
@ -41,7 +43,7 @@ class TestGst(unittest.TestCase):
def on_stream_start(self):
self.stream = True
def test_gst_init(self):
def test_gst_types(self):
self.assertIsInstance(self.audio.playbin, Gst.Element)
self.assertIsInstance(self.audio.bus, Gst.Bus)
@ -50,6 +52,9 @@ class TestGst(unittest.TestCase):
self.assertIsInstance(gst.Duration, Gtk.Label)
self.assertIsInstance(gst.NextButton, Gtk.Button)
self.assertIsInstance(gst.PauseButton, Gtk.Button)
self.assertIsInstance(gst.PauseDown, Gtk.Button)
self.assertIsInstance(gst.PauseEntry, Gtk.Entry)
self.assertIsInstance(gst.PauseUp, Gtk.Button)
self.assertIsInstance(gst.PlayButton, Gtk.Button)
self.assertIsInstance(gst.Position, Gtk.Label)
self.assertIsInstance(gst.PrevButton, Gtk.Button)
@ -58,6 +63,9 @@ class TestGst(unittest.TestCase):
self.assertIsInstance(gst.Volume, Gtk.VolumeButton)
self.assertIsInstance(gst.VolumeAdj, Gtk.Adjustment)
self.assertEqual(gst.PauseIcon, Gtk.EntryIconPosition.PRIMARY)
def test_gst_init(self):
self.assertEqual(gst.Title.get_text(), "Emmental")
self.assertEqual(gst.Artist.get_text(), "The Cheesy Music Player")
@ -119,6 +127,9 @@ class TestGst(unittest.TestCase):
curds.playlist.library.join()
self.assertEqual(curds.PlaylistManager.lookup("Collection").current, -1)
self.assertEqual(gst.PauseEntry.get_text(), "Paused")
self.assertEqual(gst.PauseEntry.get_icon_name(gst.PauseIcon), "media-playback-pause")
gst.NextButton.clicked()
track1 = curds.PlaylistManager.track
self.assertIsNotNone(track1)
@ -133,17 +144,23 @@ class TestGst(unittest.TestCase):
self.assertTrue( gst.PauseButton.is_visible())
self.assertFalse(gst.PlayButton.is_visible())
self.assertEqual(curds.PlaylistManager.track, track2)
self.assertEqual(gst.PauseEntry.get_text(), "Keep Playing")
self.assertEqual(gst.PauseEntry.get_icon_name(gst.PauseIcon), "media-playback-start")
gst.PauseButton.clicked()
self.main_loop_until(Gst.State.PAUSED)
self.assertFalse(gst.PauseButton.is_visible())
self.assertTrue( gst.PlayButton.is_visible())
self.assertEqual(curds.PlaylistManager.track, track2)
self.assertEqual(gst.PauseEntry.get_text(), "Paused")
self.assertEqual(gst.PauseEntry.get_icon_name(gst.PauseIcon), "media-playback-pause")
gst.PlayButton.clicked()
self.main_loop_until(Gst.State.PLAYING)
self.assertTrue( gst.PauseButton.is_visible())
self.assertFalse(gst.PlayButton.is_visible())
self.assertEqual(gst.PauseEntry.get_text(), "Keep Playing")
self.assertEqual(gst.PauseEntry.get_icon_name(gst.PauseIcon), "media-playback-start")
gst.NextButton.clicked()
self.main_loop_until(Gst.State.PLAYING)
@ -166,6 +183,74 @@ class TestGst(unittest.TestCase):
self.assertEqual(curds.PlaylistManager.lookup("Collection").current, 3)
self.assertNotEqual(curds.PlaylistManager.track, track2)
def test_gst_automatic_pause_up(self):
self.assertEqual(self.audio.pause_count, -1)
self.assertFalse(gst.PauseDown.is_sensitive())
self.assertTrue( gst.PauseUp.is_sensitive())
gst.PauseUp.clicked()
self.assertEqual(self.audio.pause_count, 0)
self.assertTrue( gst.PauseDown.is_sensitive())
self.assertTrue( gst.PauseUp.is_sensitive())
self.assertEqual(gst.PauseEntry.get_text(), "This Track")
self.assertEqual(gst.PauseEntry.get_icon_name(gst.PauseIcon), "media-playback-pause")
gst.PauseUp.clicked()
self.assertEqual(self.audio.pause_count, 1)
self.assertEqual(gst.PauseEntry.get_text(), "Next Track")
self.audio.pause_count = 98
gst.PauseUp.clicked()
self.assertEqual(self.audio.pause_count, 99)
self.assertEqual(gst.PauseEntry.get_text(), "99 Tracks")
self.assertFalse(gst.PauseUp.is_sensitive())
gst.PauseUp.clicked()
self.assertEqual(self.audio.pause_count, 99)
def test_gst_automatic_pause_down(self):
self.assertEqual(self.audio.pause_count, -1)
self.assertFalse(gst.PauseDown.is_sensitive())
self.assertTrue( gst.PauseUp.is_sensitive())
self.audio.pause_count = 2
gst.PauseDown.clicked()
self.assertEqual(self.audio.pause_count, 1)
self.assertTrue( gst.PauseDown.is_sensitive())
self.assertEqual(gst.PauseEntry.get_text(), "Next Track")
gst.PauseDown.clicked()
self.assertEqual(self.audio.pause_count, 0)
self.assertEqual(gst.PauseEntry.get_text(), "This Track")
gst.PauseDown.clicked()
self.assertEqual(self.audio.pause_count, -1)
self.assertEqual(gst.PauseEntry.get_text(), "Paused")
self.assertFalse( gst.PauseDown.is_sensitive())
gst.PauseDown.clicked()
self.assertEqual(self.audio.pause_count, -1)
def test_gst_automatic_pause_entry(self):
gst.PauseEntry.set_text("-5")
gst.PauseEntry.activate()
self.assertEqual(self.audio.pause_count, -1)
self.assertEqual(gst.PauseEntry.get_text(), "Paused")
self.assertTrue( gst.PauseUp.is_sensitive())
self.assertFalse(gst.PauseDown.is_sensitive())
gst.PauseEntry.set_text("3")
gst.PauseEntry.activate()
self.assertEqual(self.audio.pause_count, 3)
self.assertEqual(gst.PauseEntry.get_text(), "3 Tracks")
gst.PauseEntry.set_text("100")
gst.PauseEntry.activate()
self.assertEqual(self.audio.pause_count, 99)
self.assertEqual(gst.PauseEntry.get_text(), "99 Tracks")
self.assertFalse(gst.PauseUp.is_sensitive())
self.assertTrue( gst.PauseDown.is_sensitive())
def test_gst_seek(self):
track = curds.Track.lookup(os.path.join(test_album, "10 - Test Track 10.ogg"))

View File

@ -47,7 +47,7 @@ class TestGtk(unittest.TestCase):
self.assertIsInstance(gtk.ShowMore, Gtk.ToggleButton)
self.assertIsInstance(gtk.UpArrow, Gtk.Image)
self.assertIsInstance(gtk.DownArrow, Gtk.Image)
self.assertIsInstance(gtk.PauseAfter, Gtk.SpinButton)
self.assertIsInstance(gtk.PauseBox, Gtk.Box)
self.assertIsInstance(gtk.PlistSearch, Gtk.SearchEntry)
self.assertIsInstance(gtk.TrackSearch, Gtk.SearchEntry)
@ -56,7 +56,7 @@ class TestGtk(unittest.TestCase):
self.assertTrue( gtk.DownArrow.is_visible())
self.assertFalse(gtk.PlistSearch.is_visible())
self.assertFalse(gtk.TrackSearch.is_visible())
self.assertFalse(gtk.PauseAfter.is_visible())
self.assertFalse(gtk.PauseBox.is_visible())
gtk.ShowMore.set_active(True)
self.assertTrue( gtk.ShowMore.get_active())
@ -64,7 +64,7 @@ class TestGtk(unittest.TestCase):
self.assertFalse(gtk.DownArrow.is_visible())
self.assertTrue( gtk.PlistSearch.is_visible())
self.assertTrue( gtk.TrackSearch.is_visible())
self.assertTrue(gtk.PauseAfter.is_visible())
self.assertTrue(gtk.PauseBox.is_visible())
gtk.ShowMore.set_active(False)
self.assertFalse(gtk.ShowMore.get_active())
@ -72,7 +72,7 @@ class TestGtk(unittest.TestCase):
self.assertTrue( gtk.DownArrow.is_visible())
self.assertFalse(gtk.PlistSearch.is_visible())
self.assertFalse(gtk.TrackSearch.is_visible())
self.assertFalse(gtk.PauseAfter.is_visible())
self.assertFalse(gtk.PauseBox.is_visible())
def test_accel(self):
play_button = gtk.Builder.get_object("play_button")