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