1#!/usr/local/bin/python3.8 2 3from gi.repository import Gtk, GObject, GLib 4 5from util import trackers 6 7class _fixedViewport(Gtk.Viewport): 8 """ 9 This is needed by MarqueeLabel to restrict the size of 10 our label, and cause the viewport to actually be functional. 11 Otherwise, the text is trimmed, but no scrolling occurs. 12 """ 13 def __init__(self): 14 super(_fixedViewport, self).__init__() 15 16 self.set_shadow_type(Gtk.ShadowType.NONE) 17 18 def do_get_preferred_width(self): 19 return (400, 400) 20 21class MarqueeLabel(Gtk.Stack): 22 """ 23 A scrolling label for the PlayerControl - it uses the defined 24 pattern as a mapping between elapsed time and the hadjustment 25 of the _fixedViewport. If the label text is wider than the 26 widget's actual width, we will scroll according to this map 27 over the course of 15 seconds. 28 29 It roughly translates to: 30 0.0 - 2.0 seconds: no movement. 31 2.0 - 10.0 seconds: gradually scroll to max adjustment of the viewport. 32 10.0 - 12.0 seconds: no movement. 33 12.0 - 15.0 seconds: scroll back to the starting position. 34 35 This widget also implements a stack (similar to the one in MonitorView) which 36 provides for a smooth label crossfade when track info changes. 37 """ 38 PATTERN = [( 0.0, 0.0), 39 ( 2.0, 0.0), 40 (10.0, 1.0), 41 (12.0, 1.0), 42 (15.0, 0.0)] 43 44 LENGTH = len(PATTERN) 45 46 def __init__(self, text): 47 super(MarqueeLabel, self).__init__() 48 49 self.set_transition_type(Gtk.StackTransitionType.CROSSFADE) 50 self.set_transition_duration(300) 51 52 self.tick_id = 0 53 54 self.current = self._make_label(text) 55 56 self.add(self.current) 57 self.set_visible_child(self.current) 58 59 def _make_label(self, text): 60 vp = _fixedViewport() 61 62 label = Gtk.Label(text) 63 label.set_halign(Gtk.Align.START) 64 65 vp.add(label) 66 vp.show_all() 67 68 return vp 69 70 def set_text(self, text): 71 if self.current.get_child().get_text() == text: 72 return 73 74 self.cancel_tick() 75 76 self.queued = self._make_label(text) 77 78 self.add(self.queued) 79 self.set_visible_child(self.queued) 80 81 tmp = self.current 82 self.current = self.queued 83 self.queued = None 84 85 GObject.idle_add(tmp.destroy) 86 87 if not self.current.get_realized(): 88 trackers.con_tracker_get().connect(self.current, 89 "realize", 90 self.on_current_realized) 91 else: 92 GObject.idle_add(self._marquee_idle) 93 94 def on_current_realized(self, widget, data=None): 95 GObject.idle_add(self._marquee_idle) 96 97 trackers.con_tracker_get().disconnect(widget, 98 "realize", 99 self.on_current_realized) 100 101 def cancel_tick(self): 102 if self.tick_id > 0: 103 self.remove_tick_callback(self.tick_id) 104 self.tick_id = 0 105 106 def _marquee_idle(self): 107 self.hadjust = self.current.get_hadjustment() 108 109 if (self.hadjust.get_upper() == self.hadjust.get_page_size()) == self.get_allocated_width(): 110 return False 111 112 self.start_time = self.get_frame_clock().get_frame_time() 113 self.end_time = self.start_time + (self.PATTERN[self.LENGTH - 1][0] * 1000 * 1000) # sec to ms to μs 114 115 if self.tick_id == 0: 116 self.tick_id = self.add_tick_callback(self._on_marquee_tick) 117 118 self._marquee_step(self.start_time) 119 120 return GLib.SOURCE_REMOVE 121 122 def _on_marquee_tick(self, widget, clock, data=None): 123 now = clock.get_frame_time() 124 125 self._marquee_step(now) 126 127 if now >= self.end_time: 128 self.start_time = self.end_time 129 self.end_time += (self.PATTERN[self.LENGTH - 1][0] * 1000 * 1000) # sec to ms to μs 130 131 return GLib.SOURCE_CONTINUE 132 133 def interpolate_point(self, now): 134 point = ((now - self.start_time) / 1000 / 1000) 135 136 i = 0 137 while i < self.LENGTH: 138 cindex, cval = self.PATTERN[i] 139 140 if point > cindex: 141 i += 1 142 continue 143 144 if point == cindex: 145 return cval 146 147 pindex, pval = self.PATTERN[i - 1] 148 diff = cval - pval 149 duration = cindex - pindex 150 151 ratio = diff / duration 152 additive = (point - pindex) * ratio 153 return pval + additive 154 155 def _marquee_step(self, now): 156 if now < self.end_time: 157 t = self.interpolate_point(now) 158 else: 159 t = self.PATTERN[self.LENGTH - 1][1] 160 161 new_position = ((self.hadjust.get_upper() - self.hadjust.get_page_size()) * t) 162 163 self.hadjust.set_value(new_position) 164 self.queue_draw() 165