1 /*
2  * Copyright © 2018 Benjamin Otte
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors: Benjamin Otte <otte@gnome.org>
18  */
19 
20 #include "config.h"
21 
22 #include "gtkmediacontrols.h"
23 
24 #include "gtkadjustment.h"
25 #include "gtkbutton.h"
26 #include "gtkintl.h"
27 #include "gtklabel.h"
28 #include "gtkwidgetprivate.h"
29 
30 /**
31  * GtkMediaControls:
32  *
33  * `GtkMediaControls` is a widget to show controls for a video.
34  *
35  * ![An example GtkMediaControls](media-controls.png)
36  *
37  * Usually, `GtkMediaControls` is used as part of [class@Gtk.Video].
38  */
39 
40 struct _GtkMediaControls
41 {
42   GtkWidget parent_instance;
43 
44   GtkMediaStream *stream;
45 
46   GtkAdjustment *time_adjustment;
47   GtkAdjustment *volume_adjustment;
48   GtkWidget *box;
49   GtkWidget *play_button;
50   GtkWidget *time_box;
51   GtkWidget *time_label;
52   GtkWidget *seek_scale;
53   GtkWidget *duration_label;
54   GtkWidget *volume_button;
55 };
56 
57 enum
58 {
59   PROP_0,
60   PROP_MEDIA_STREAM,
61 
62   N_PROPS
63 };
64 
65 G_DEFINE_TYPE (GtkMediaControls, gtk_media_controls, GTK_TYPE_WIDGET)
66 
67 static GParamSpec *properties[N_PROPS] = { NULL, };
68 
69 /* FIXME: Remove
70  * See https://bugzilla.gnome.org/show_bug.cgi?id=679850 */
71 static char *
totem_time_to_string(gint64 usecs,gboolean remaining,gboolean force_hour)72 totem_time_to_string (gint64   usecs,
73 		      gboolean remaining,
74 		      gboolean force_hour)
75 {
76 	int sec, min, hour, _time;
77 
78 	_time = (int) (usecs / G_USEC_PER_SEC);
79 	/* When calculating the remaining time,
80 	 * we want to make sure that:
81 	 * current time + time remaining = total run time */
82 	if (remaining)
83 		_time++;
84 
85 	sec = _time % 60;
86 	_time = _time - sec;
87 	min = (_time % (60*60)) / 60;
88 	_time = _time - (min * 60);
89 	hour = _time / (60*60);
90 
91 	if (hour > 0 || force_hour) {
92 		if (!remaining) {
93 			/* hour:minutes:seconds */
94 			/* Translators: This is a time format, like "9:05:02" for 9
95 			 * hours, 5 minutes, and 2 seconds. You may change ":" to
96 			 * the separator that your locale uses or use "%Id" instead
97 			 * of "%d" if your locale uses localized digits.
98 			 */
99 			return g_strdup_printf (C_("long time format", "%d:%02d:%02d"), hour, min, sec);
100 		} else {
101 			/* -hour:minutes:seconds */
102 			/* Translators: This is a time format, like "-9:05:02" for 9
103 			 * hours, 5 minutes, and 2 seconds playback remaining. You may
104 			 * change ":" to the separator that your locale uses or use
105 			 * "%Id" instead of "%d" if your locale uses localized digits.
106 			 */
107 			return g_strdup_printf (C_("long time format", "-%d:%02d:%02d"), hour, min, sec);
108 		}
109 	}
110 
111 	if (remaining) {
112 		/* -minutes:seconds */
113 		/* Translators: This is a time format, like "-5:02" for 5
114 		 * minutes and 2 seconds playback remaining. You may change
115 		 * ":" to the separator that your locale uses or use "%Id"
116 		 * instead of "%d" if your locale uses localized digits.
117 		 */
118 		return g_strdup_printf (C_("short time format", "-%d:%02d"), min, sec);
119 	}
120 
121 	/* minutes:seconds */
122 	/* Translators: This is a time format, like "5:02" for 5
123 	 * minutes and 2 seconds. You may change ":" to the
124 	 * separator that your locale uses or use "%Id" instead of
125 	 * "%d" if your locale uses localized digits.
126 	 */
127 	return g_strdup_printf (C_("short time format", "%d:%02d"), min, sec);
128 }
129 
130 static void
time_adjustment_changed(GtkAdjustment * adjustment,GtkMediaControls * controls)131 time_adjustment_changed (GtkAdjustment    *adjustment,
132                          GtkMediaControls *controls)
133 {
134   if (controls->stream == NULL)
135     return;
136 
137   /* We just updated the adjustment and it's correct now */
138   if (gtk_adjustment_get_value (adjustment) == (double) gtk_media_stream_get_timestamp (controls->stream) / G_USEC_PER_SEC)
139     return;
140 
141   gtk_media_stream_seek (controls->stream,
142                          gtk_adjustment_get_value (adjustment) * G_USEC_PER_SEC + 0.5);
143 }
144 
145 static void
volume_adjustment_changed(GtkAdjustment * adjustment,GtkMediaControls * controls)146 volume_adjustment_changed (GtkAdjustment    *adjustment,
147                            GtkMediaControls *controls)
148 {
149   if (controls->stream == NULL)
150     return;
151 
152   /* We just updated the adjustment and it's correct now */
153   if (gtk_adjustment_get_value (adjustment) == gtk_media_stream_get_volume (controls->stream))
154     return;
155 
156   gtk_media_stream_set_muted (controls->stream, gtk_adjustment_get_value (adjustment) == 0.0);
157   gtk_media_stream_set_volume (controls->stream, gtk_adjustment_get_value (adjustment));
158 }
159 
160 static void
play_button_clicked(GtkWidget * button,GtkMediaControls * controls)161 play_button_clicked (GtkWidget        *button,
162                      GtkMediaControls *controls)
163 {
164   if (controls->stream == NULL)
165     return;
166 
167   gtk_media_stream_set_playing (controls->stream,
168                                 !gtk_media_stream_get_playing (controls->stream));
169 }
170 
171 static void
gtk_media_controls_measure(GtkWidget * widget,GtkOrientation orientation,int for_size,int * minimum,int * natural,int * minimum_baseline,int * natural_baseline)172 gtk_media_controls_measure (GtkWidget      *widget,
173                             GtkOrientation  orientation,
174                             int             for_size,
175                             int            *minimum,
176                             int            *natural,
177                             int            *minimum_baseline,
178                             int            *natural_baseline)
179 {
180   GtkMediaControls *controls = GTK_MEDIA_CONTROLS (widget);
181 
182   gtk_widget_measure (controls->box,
183                       orientation,
184                       for_size,
185                       minimum, natural,
186                       minimum_baseline, natural_baseline);
187 }
188 
189 static void
gtk_media_controls_size_allocate(GtkWidget * widget,int width,int height,int baseline)190 gtk_media_controls_size_allocate (GtkWidget *widget,
191                                   int        width,
192                                   int        height,
193                                   int        baseline)
194 {
195   GtkMediaControls *controls = GTK_MEDIA_CONTROLS (widget);
196 
197   gtk_widget_size_allocate (controls->box,
198                             &(GtkAllocation) {
199                               0, 0,
200                               width, height
201                             }, baseline);
202 }
203 
204 static void
gtk_media_controls_dispose(GObject * object)205 gtk_media_controls_dispose (GObject *object)
206 {
207   GtkMediaControls *controls = GTK_MEDIA_CONTROLS (object);
208 
209   gtk_media_controls_set_media_stream (controls, NULL);
210 
211   g_clear_pointer (&controls->box, gtk_widget_unparent);
212 
213   G_OBJECT_CLASS (gtk_media_controls_parent_class)->dispose (object);
214 }
215 
216 static void
gtk_media_controls_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)217 gtk_media_controls_get_property (GObject    *object,
218                                  guint       property_id,
219                                  GValue     *value,
220                                  GParamSpec *pspec)
221 {
222   GtkMediaControls *controls = GTK_MEDIA_CONTROLS (object);
223 
224   switch (property_id)
225     {
226     case PROP_MEDIA_STREAM:
227       g_value_set_object (value, controls->stream);
228       break;
229 
230     default:
231       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
232       break;
233     }
234 }
235 
236 static void
gtk_media_controls_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)237 gtk_media_controls_set_property (GObject      *object,
238                                  guint         property_id,
239                                  const GValue *value,
240                                  GParamSpec   *pspec)
241 {
242   GtkMediaControls *controls = GTK_MEDIA_CONTROLS (object);
243 
244   switch (property_id)
245     {
246     case PROP_MEDIA_STREAM:
247       gtk_media_controls_set_media_stream (controls, g_value_get_object (value));
248       break;
249 
250     default:
251       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
252       break;
253     }
254 }
255 
256 static void
gtk_media_controls_class_init(GtkMediaControlsClass * klass)257 gtk_media_controls_class_init (GtkMediaControlsClass *klass)
258 {
259   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
260   GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
261 
262   widget_class->measure = gtk_media_controls_measure;
263   widget_class->size_allocate = gtk_media_controls_size_allocate;
264 
265   gobject_class->dispose = gtk_media_controls_dispose;
266   gobject_class->get_property = gtk_media_controls_get_property;
267   gobject_class->set_property = gtk_media_controls_set_property;
268 
269   /**
270    * GtkMediaControls:media-stream: (attributes org.gtk.Property.get=gtk_media_controls_get_media_stream org.gtk.Property.set=gtk_media_controls_set_media_stream)
271    *
272    * The media-stream managed by this object or %NULL if none.
273    */
274   properties[PROP_MEDIA_STREAM] =
275     g_param_spec_object ("media-stream",
276                          P_("Media Stream"),
277                          P_("The media stream managed"),
278                          GTK_TYPE_MEDIA_STREAM,
279                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
280 
281   g_object_class_install_properties (gobject_class, N_PROPS, properties);
282 
283   gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkmediacontrols.ui");
284   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_adjustment);
285   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, volume_adjustment);
286   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, box);
287   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, play_button);
288   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_box);
289   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_label);
290   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, seek_scale);
291   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, duration_label);
292   gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, volume_button);
293 
294   gtk_widget_class_bind_template_callback (widget_class, play_button_clicked);
295   gtk_widget_class_bind_template_callback (widget_class, time_adjustment_changed);
296   gtk_widget_class_bind_template_callback (widget_class, volume_adjustment_changed);
297 
298   gtk_widget_class_set_css_name (widget_class, I_("controls"));
299 }
300 
301 static void
gtk_media_controls_init(GtkMediaControls * controls)302 gtk_media_controls_init (GtkMediaControls *controls)
303 {
304   gtk_widget_init_template (GTK_WIDGET (controls));
305 }
306 
307 /**
308  * gtk_media_controls_new:
309  * @stream: (nullable) (transfer none): a `GtkMediaStream` to manage
310  *
311  * Creates a new `GtkMediaControls` managing the @stream passed to it.
312  *
313  * Returns: a new `GtkMediaControls`
314  */
315 GtkWidget *
gtk_media_controls_new(GtkMediaStream * stream)316 gtk_media_controls_new (GtkMediaStream *stream)
317 {
318   return g_object_new (GTK_TYPE_MEDIA_CONTROLS,
319                        "media-stream", stream,
320                        NULL);
321 }
322 
323 /**
324  * gtk_media_controls_get_media_stream: (attributes org.gtk.Method.get_property=media-stream)
325  * @controls: a `GtkMediaControls`
326  *
327  * Gets the media stream managed by @controls or %NULL if none.
328  *
329  * Returns: (nullable) (transfer none): The media stream managed by @controls
330  */
331 GtkMediaStream *
gtk_media_controls_get_media_stream(GtkMediaControls * controls)332 gtk_media_controls_get_media_stream (GtkMediaControls *controls)
333 {
334   g_return_val_if_fail (GTK_IS_MEDIA_CONTROLS (controls), NULL);
335 
336   return controls->stream;
337 }
338 
339 static void
update_timestamp(GtkMediaControls * controls)340 update_timestamp (GtkMediaControls *controls)
341 {
342   gint64 timestamp, duration;
343   char *time_string;
344 
345   if (controls->stream)
346     {
347       timestamp = gtk_media_stream_get_timestamp (controls->stream);
348       duration = gtk_media_stream_get_duration (controls->stream);
349     }
350   else
351     {
352       timestamp = 0;
353       duration = 0;
354     }
355 
356   time_string = totem_time_to_string (timestamp, FALSE, FALSE);
357   gtk_label_set_text (GTK_LABEL (controls->time_label), time_string);
358   g_free (time_string);
359 
360   if (duration > 0)
361     {
362       time_string = totem_time_to_string (duration > timestamp ? duration - timestamp : 0, TRUE, FALSE);
363       gtk_label_set_text (GTK_LABEL (controls->duration_label), time_string);
364       g_free (time_string);
365 
366       gtk_adjustment_set_value (controls->time_adjustment, (double) timestamp / G_USEC_PER_SEC);
367     }
368 }
369 
370 static void
update_duration(GtkMediaControls * controls)371 update_duration (GtkMediaControls *controls)
372 {
373   gint64 timestamp, duration;
374   char *time_string;
375 
376   if (controls->stream)
377     {
378       timestamp = gtk_media_stream_get_timestamp (controls->stream);
379       duration = gtk_media_stream_get_duration (controls->stream);
380     }
381   else
382     {
383       timestamp = 0;
384       duration = 0;
385     }
386 
387   time_string = totem_time_to_string (duration > timestamp ? duration - timestamp : 0, TRUE, FALSE);
388   gtk_label_set_text (GTK_LABEL (controls->duration_label), time_string);
389   gtk_widget_set_visible (controls->duration_label, duration > 0);
390   g_free (time_string);
391 
392   gtk_adjustment_set_upper (controls->time_adjustment,
393                             gtk_adjustment_get_page_size (controls->time_adjustment)
394                             + (double) duration / G_USEC_PER_SEC);
395   gtk_adjustment_set_value (controls->time_adjustment, (double) timestamp / G_USEC_PER_SEC);
396 }
397 
398 static void
update_playing(GtkMediaControls * controls)399 update_playing (GtkMediaControls *controls)
400 {
401   gboolean playing;
402   const char *icon_name;
403 
404   if (controls->stream)
405     playing = gtk_media_stream_get_playing (controls->stream);
406   else
407     playing = FALSE;
408 
409   if (playing)
410     icon_name = "media-playback-pause-symbolic";
411   else
412     icon_name = "media-playback-start-symbolic";
413 
414   gtk_button_set_icon_name (GTK_BUTTON (controls->play_button), icon_name);
415 }
416 
417 static void
update_seekable(GtkMediaControls * controls)418 update_seekable (GtkMediaControls *controls)
419 {
420   gboolean seekable;
421 
422   if (controls->stream)
423     seekable = gtk_media_stream_is_seekable (controls->stream);
424   else
425     seekable = FALSE;
426 
427   gtk_widget_set_sensitive (controls->seek_scale, seekable);
428 }
429 
430 static void
update_volume(GtkMediaControls * controls)431 update_volume (GtkMediaControls *controls)
432 {
433   double volume;
434 
435   if (controls->stream == NULL)
436     volume = 1.0;
437   else if (gtk_media_stream_get_muted (controls->stream))
438     volume = 0.0;
439   else
440     volume = gtk_media_stream_get_volume (controls->stream);
441 
442   gtk_adjustment_set_value (controls->volume_adjustment, volume);
443 
444   gtk_widget_set_sensitive (controls->volume_button,
445                             controls->stream == NULL ||
446                             gtk_media_stream_has_audio (controls->stream));
447 }
448 
449 static void
update_all(GtkMediaControls * controls)450 update_all (GtkMediaControls *controls)
451 {
452   update_timestamp (controls);
453   update_duration (controls);
454   update_playing (controls);
455   update_seekable (controls);
456   update_volume (controls);
457 }
458 
459 static void
gtk_media_controls_notify_cb(GtkMediaStream * stream,GParamSpec * pspec,GtkMediaControls * controls)460 gtk_media_controls_notify_cb (GtkMediaStream   *stream,
461                               GParamSpec       *pspec,
462                               GtkMediaControls *controls)
463 {
464   if (g_str_equal (pspec->name, "timestamp"))
465     update_timestamp (controls);
466   else if (g_str_equal (pspec->name, "duration"))
467     update_duration (controls);
468   else if (g_str_equal (pspec->name, "playing"))
469     update_playing (controls);
470   else if (g_str_equal (pspec->name, "seekable"))
471     update_seekable (controls);
472   else if (g_str_equal (pspec->name, "muted"))
473     update_volume (controls);
474   else if (g_str_equal (pspec->name, "volume"))
475     update_volume (controls);
476   else if (g_str_equal (pspec->name, "has-audio"))
477     update_volume (controls);
478 }
479 
480 /**
481  * gtk_media_controls_set_media_stream: (attributes org.gtk.Method.set_property=media-stream)
482  * @controls: a `GtkMediaControls` widget
483  * @stream: (nullable):  a `GtkMediaStream`
484  *
485  * Sets the stream that is controlled by @controls.
486  */
487 void
gtk_media_controls_set_media_stream(GtkMediaControls * controls,GtkMediaStream * stream)488 gtk_media_controls_set_media_stream (GtkMediaControls *controls,
489                                      GtkMediaStream   *stream)
490 {
491   g_return_if_fail (GTK_IS_MEDIA_CONTROLS (controls));
492   g_return_if_fail (stream == NULL || GTK_IS_MEDIA_STREAM (stream));
493 
494   if (controls->stream == stream)
495     return;
496 
497   if (controls->stream)
498     {
499       g_signal_handlers_disconnect_by_func (controls->stream,
500                                             gtk_media_controls_notify_cb,
501                                             controls);
502       g_object_unref (controls->stream);
503       controls->stream = NULL;
504     }
505 
506   if (stream)
507     {
508       controls->stream = g_object_ref (stream);
509       g_signal_connect (controls->stream,
510                         "notify",
511                         G_CALLBACK (gtk_media_controls_notify_cb),
512                         controls);
513     }
514 
515   update_all (controls);
516   gtk_widget_set_sensitive (controls->box, stream != NULL);
517 
518   g_object_notify_by_pspec (G_OBJECT (controls), properties[PROP_MEDIA_STREAM]);
519 }
520