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
40struct _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
57enum
58{
59 PROP_0,
60 PROP_MEDIA_STREAM,
61
62 N_PROPS
63};
64
65G_DEFINE_TYPE (GtkMediaControls, gtk_media_controls, GTK_TYPE_WIDGET)
66
67static GParamSpec *properties[N_PROPS] = { NULL, };
68
69/* FIXME: Remove
70 * See https://bugzilla.gnome.org/show_bug.cgi?id=679850 */
71static char *
72totem_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
130static void
131time_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 (self: controls->stream) / G_USEC_PER_SEC)
139 return;
140
141 gtk_media_stream_seek (self: controls->stream,
142 timestamp: gtk_adjustment_get_value (adjustment) * G_USEC_PER_SEC + 0.5);
143}
144
145static void
146volume_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 (self: controls->stream))
154 return;
155
156 gtk_media_stream_set_muted (self: controls->stream, muted: gtk_adjustment_get_value (adjustment) == 0.0);
157 gtk_media_stream_set_volume (self: controls->stream, volume: gtk_adjustment_get_value (adjustment));
158}
159
160static void
161play_button_clicked (GtkWidget *button,
162 GtkMediaControls *controls)
163{
164 if (controls->stream == NULL)
165 return;
166
167 gtk_media_stream_set_playing (self: controls->stream,
168 playing: !gtk_media_stream_get_playing (self: controls->stream));
169}
170
171static void
172gtk_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 (ptr: widget);
181
182 gtk_widget_measure (widget: controls->box,
183 orientation,
184 for_size,
185 minimum, natural,
186 minimum_baseline, natural_baseline);
187}
188
189static void
190gtk_media_controls_size_allocate (GtkWidget *widget,
191 int width,
192 int height,
193 int baseline)
194{
195 GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: widget);
196
197 gtk_widget_size_allocate (widget: controls->box,
198 allocation: &(GtkAllocation) {
199 0, 0,
200 width, height
201 }, baseline);
202}
203
204static void
205gtk_media_controls_dispose (GObject *object)
206{
207 GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: 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
216static void
217gtk_media_controls_get_property (GObject *object,
218 guint property_id,
219 GValue *value,
220 GParamSpec *pspec)
221{
222 GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: object);
223
224 switch (property_id)
225 {
226 case PROP_MEDIA_STREAM:
227 g_value_set_object (value, v_object: controls->stream);
228 break;
229
230 default:
231 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
232 break;
233 }
234}
235
236static void
237gtk_media_controls_set_property (GObject *object,
238 guint property_id,
239 const GValue *value,
240 GParamSpec *pspec)
241{
242 GtkMediaControls *controls = GTK_MEDIA_CONTROLS (ptr: object);
243
244 switch (property_id)
245 {
246 case PROP_MEDIA_STREAM:
247 gtk_media_controls_set_media_stream (controls, stream: 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
256static void
257gtk_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 (name: "media-stream",
276 P_("Media Stream"),
277 P_("The media stream managed"),
278 GTK_TYPE_MEDIA_STREAM,
279 flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
280
281 g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties);
282
283 gtk_widget_class_set_template_from_resource (widget_class, resource_name: "/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
301static void
302gtk_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 */
315GtkWidget *
316gtk_media_controls_new (GtkMediaStream *stream)
317{
318 return g_object_new (GTK_TYPE_MEDIA_CONTROLS,
319 first_property_name: "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 */
331GtkMediaStream *
332gtk_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
339static void
340update_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 (self: controls->stream);
348 duration = gtk_media_stream_get_duration (self: controls->stream);
349 }
350 else
351 {
352 timestamp = 0;
353 duration = 0;
354 }
355
356 time_string = totem_time_to_string (usecs: timestamp, FALSE, FALSE);
357 gtk_label_set_text (GTK_LABEL (controls->time_label), str: time_string);
358 g_free (mem: time_string);
359
360 if (duration > 0)
361 {
362 time_string = totem_time_to_string (usecs: duration > timestamp ? duration - timestamp : 0, TRUE, FALSE);
363 gtk_label_set_text (GTK_LABEL (controls->duration_label), str: time_string);
364 g_free (mem: time_string);
365
366 gtk_adjustment_set_value (adjustment: controls->time_adjustment, value: (double) timestamp / G_USEC_PER_SEC);
367 }
368}
369
370static void
371update_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 (self: controls->stream);
379 duration = gtk_media_stream_get_duration (self: controls->stream);
380 }
381 else
382 {
383 timestamp = 0;
384 duration = 0;
385 }
386
387 time_string = totem_time_to_string (usecs: duration > timestamp ? duration - timestamp : 0, TRUE, FALSE);
388 gtk_label_set_text (GTK_LABEL (controls->duration_label), str: time_string);
389 gtk_widget_set_visible (widget: controls->duration_label, visible: duration > 0);
390 g_free (mem: time_string);
391
392 gtk_adjustment_set_upper (adjustment: controls->time_adjustment,
393 upper: gtk_adjustment_get_page_size (adjustment: controls->time_adjustment)
394 + (double) duration / G_USEC_PER_SEC);
395 gtk_adjustment_set_value (adjustment: controls->time_adjustment, value: (double) timestamp / G_USEC_PER_SEC);
396}
397
398static void
399update_playing (GtkMediaControls *controls)
400{
401 gboolean playing;
402 const char *icon_name;
403
404 if (controls->stream)
405 playing = gtk_media_stream_get_playing (self: 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
417static void
418update_seekable (GtkMediaControls *controls)
419{
420 gboolean seekable;
421
422 if (controls->stream)
423 seekable = gtk_media_stream_is_seekable (self: controls->stream);
424 else
425 seekable = FALSE;
426
427 gtk_widget_set_sensitive (widget: controls->seek_scale, sensitive: seekable);
428}
429
430static void
431update_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 (self: controls->stream))
438 volume = 0.0;
439 else
440 volume = gtk_media_stream_get_volume (self: controls->stream);
441
442 gtk_adjustment_set_value (adjustment: controls->volume_adjustment, value: volume);
443
444 gtk_widget_set_sensitive (widget: controls->volume_button,
445 sensitive: controls->stream == NULL ||
446 gtk_media_stream_has_audio (self: controls->stream));
447}
448
449static void
450update_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
459static void
460gtk_media_controls_notify_cb (GtkMediaStream *stream,
461 GParamSpec *pspec,
462 GtkMediaControls *controls)
463{
464 if (g_str_equal (v1: pspec->name, v2: "timestamp"))
465 update_timestamp (controls);
466 else if (g_str_equal (v1: pspec->name, v2: "duration"))
467 update_duration (controls);
468 else if (g_str_equal (v1: pspec->name, v2: "playing"))
469 update_playing (controls);
470 else if (g_str_equal (v1: pspec->name, v2: "seekable"))
471 update_seekable (controls);
472 else if (g_str_equal (v1: pspec->name, v2: "muted"))
473 update_volume (controls);
474 else if (g_str_equal (v1: pspec->name, v2: "volume"))
475 update_volume (controls);
476 else if (g_str_equal (v1: pspec->name, v2: "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 */
487void
488gtk_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 (object: 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 (widget: controls->box, sensitive: stream != NULL);
517
518 g_object_notify_by_pspec (G_OBJECT (controls), pspec: properties[PROP_MEDIA_STREAM]);
519}
520

source code of gtk/gtk/gtkmediacontrols.c