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