gtk: Add GtkMediaControls
authorBenjamin Otte <otte@redhat.com>
Mon, 26 Feb 2018 00:32:01 +0000 (01:32 +0100)
committerBenjamin Otte <otte@redhat.com>
Sun, 18 Mar 2018 20:01:23 +0000 (21:01 +0100)
This is a basic first implementation of controls for a GtkMediaStream,
modeled after Totem's UI.

gtk/gtk.h
gtk/gtkmediacontrols.c [new file with mode: 0644]
gtk/gtkmediacontrols.h [new file with mode: 0644]
gtk/meson.build
gtk/ui/gtkmediacontrols.ui [new file with mode: 0644]

index bc1c62c180ebc9b6cfe923575fd703f894267dca..afe849c292de174bd81a023d83707ef1dfbee4b8 100644 (file)
--- a/gtk/gtk.h
+++ b/gtk/gtk.h
 #include <gtk/gtkliststore.h>
 #include <gtk/gtklockbutton.h>
 #include <gtk/gtkmain.h>
+#include <gtk/gtkmediacontrols.h>
 #include <gtk/gtkmediafile.h>
 #include <gtk/gtkmediastream.h>
 #include <gtk/gtkmenu.h>
diff --git a/gtk/gtkmediacontrols.c b/gtk/gtkmediacontrols.c
new file mode 100644 (file)
index 0000000..1f299bd
--- /dev/null
@@ -0,0 +1,501 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#include "config.h"
+
+#include "gtkmediacontrols.h"
+
+#include "gtkadjustment.h"
+#include "gtkbutton.h"
+#include "gtkintl.h"
+#include "gtklabel.h"
+
+/**
+ * SECTION:gtkmediacontrols
+ * @title: GtkMediaControls
+ * @short_description: A widget showing controls for a media stream
+ *
+ * GtkMediaControls is a widget to show controls for a #GtkMediaStream
+ * and giving users a way to use it.
+ */
+
+struct _GtkMediaControls
+{
+  GtkWidget parent_instance;
+
+  GtkMediaStream *stream;
+
+  GtkAdjustment *time_adjustment;
+  GtkAdjustment *volume_adjustment;
+  GtkWidget *box;
+  GtkWidget *play_button;
+  GtkWidget *time_box;
+  GtkWidget *time_label;
+  GtkWidget *seek_scale;
+  GtkWidget *duration_label;
+};
+
+enum
+{
+  PROP_0,
+  PROP_MEDIA_STREAM,
+
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GtkMediaControls, gtk_media_controls, GTK_TYPE_WIDGET)
+
+static GParamSpec *properties[N_PROPS] = { NULL, };
+
+/* FIXME: Remove
+ * See https://bugzilla.gnome.org/show_bug.cgi?id=679850 */
+static char *
+totem_time_to_string (gint64   usecs,
+                     gboolean remaining,
+                     gboolean force_hour)
+{
+       int sec, min, hour, _time;
+
+       _time = (int) (usecs / G_USEC_PER_SEC);
+       /* When calculating the remaining time,
+        * we want to make sure that:
+        * current time + time remaining = total run time */
+       if (remaining)
+               _time++;
+
+       sec = _time % 60;
+       _time = _time - sec;
+       min = (_time % (60*60)) / 60;
+       _time = _time - (min * 60);
+       hour = _time / (60*60);
+
+       if (hour > 0 || force_hour) {
+               if (!remaining) {
+                       /* hour:minutes:seconds */
+                       /* Translators: This is a time format, like "-9:05:02" for 9
+                        * hours, 5 minutes, and 2 seconds. You may change ":" to
+                        * the separator that your locale uses or use "%Id" instead
+                        * of "%d" if your locale uses localized digits.
+                        */
+                       return g_strdup_printf (C_("long time format", "%d:%02d:%02d"), hour, min, sec);
+               } else {
+                       /* -hour:minutes:seconds */
+                       /* Translators: This is a time format, like "-9:05:02" for 9
+                        * hours, 5 minutes, and 2 seconds playback remaining. You may
+                        * change ":" to the separator that your locale uses or use
+                        * "%Id" instead of "%d" if your locale uses localized digits.
+                        */
+                       return g_strdup_printf (C_("long time format", "-%d:%02d:%02d"), hour, min, sec);
+               }
+       }
+
+       if (remaining) {
+               /* -minutes:seconds */
+               /* Translators: This is a time format, like "-5:02" for 5
+                * minutes and 2 seconds playback remaining. You may change
+                * ":" to the separator that your locale uses or use "%Id"
+                * instead of "%d" if your locale uses localized digits.
+                */
+               return g_strdup_printf (C_("short time format", "-%d:%02d"), min, sec);
+       }
+
+       /* minutes:seconds */
+       /* Translators: This is a time format, like "5:02" for 5
+        * minutes and 2 seconds. You may change ":" to the
+        * separator that your locale uses or use "%Id" instead of
+        * "%d" if your locale uses localized digits.
+        */
+       return g_strdup_printf (C_("short time format", "%d:%02d"), min, sec);
+}
+
+static void
+time_adjustment_changed (GtkAdjustment    *adjustment,
+                         GtkMediaControls *controls)
+{
+  if (controls->stream == NULL)
+    return;
+
+  /* We just updated the adjustment and it's correct now */
+  if (gtk_adjustment_get_value (adjustment) == (double) gtk_media_stream_get_timestamp (controls->stream) / G_USEC_PER_SEC)
+    return;
+
+  gtk_media_stream_seek (controls->stream, 
+                         gtk_adjustment_get_value (adjustment) * G_USEC_PER_SEC + 0.5);
+}
+
+static void
+volume_adjustment_changed (GtkAdjustment    *adjustment,
+                           GtkMediaControls *controls)
+{
+  if (controls->stream == NULL)
+    return;
+
+  /* We just updated the adjustment and it's correct now */
+  if (gtk_adjustment_get_value (adjustment) == gtk_media_stream_get_volume (controls->stream))
+    return;
+
+  gtk_media_stream_set_muted (controls->stream, gtk_adjustment_get_value (adjustment) == 0.0);
+  gtk_media_stream_set_volume (controls->stream, gtk_adjustment_get_value (adjustment));
+}
+
+static void
+play_button_clicked (GtkWidget        *button,
+                     GtkMediaControls *controls)
+{
+  if (controls->stream == NULL)
+    return;
+
+  gtk_media_stream_set_playing (controls->stream,
+                                !gtk_media_stream_get_playing (controls->stream));
+}
+
+static void
+gtk_media_controls_measure (GtkWidget      *widget,
+                            GtkOrientation  orientation,
+                            int             for_size,
+                            int            *minimum,
+                            int            *natural,
+                            int            *minimum_baseline,
+                            int            *natural_baseline)
+{
+  GtkMediaControls *controls = GTK_MEDIA_CONTROLS (widget);
+
+  gtk_widget_measure (controls->box,
+                      orientation,
+                      for_size,
+                      minimum, natural,
+                      minimum_baseline, natural_baseline);
+}
+
+static void
+gtk_media_controls_size_allocate (GtkWidget           *widget,
+                                  const GtkAllocation *allocation,
+                                  int                  baseline,
+                                  GtkAllocation       *out_clip)
+{
+  GtkMediaControls *controls = GTK_MEDIA_CONTROLS (widget);
+
+  gtk_widget_size_allocate (controls->box, allocation, baseline, out_clip);
+}
+
+static void
+gtk_media_controls_dispose (GObject *object)
+{
+  GtkMediaControls *controls = GTK_MEDIA_CONTROLS (object);
+
+  gtk_media_controls_set_media_stream (controls, NULL);
+
+  g_clear_pointer (&controls->box, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (gtk_media_controls_parent_class)->dispose (object);
+}
+
+static void
+gtk_media_controls_get_property (GObject    *object,
+                                 guint       property_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  GtkMediaControls *controls = GTK_MEDIA_CONTROLS (object);
+
+  switch (property_id)
+    {
+    case PROP_MEDIA_STREAM:
+      g_value_set_object (value, controls->stream);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_media_controls_set_property (GObject      *object,
+                                 guint         property_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  GtkMediaControls *controls = GTK_MEDIA_CONTROLS (object);
+
+  switch (property_id)
+    {
+    case PROP_MEDIA_STREAM:
+      gtk_media_controls_set_media_stream (controls, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+gtk_media_controls_class_init (GtkMediaControlsClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  widget_class->measure = gtk_media_controls_measure;
+  widget_class->size_allocate = gtk_media_controls_size_allocate;
+
+  gobject_class->dispose = gtk_media_controls_dispose;
+  gobject_class->get_property = gtk_media_controls_get_property;
+  gobject_class->set_property = gtk_media_controls_set_property;
+
+  /**
+   * GtkMediaControls:media-stream:
+   *
+   * The media-stream managed by this object or %NULL if none.
+   */
+  properties[PROP_MEDIA_STREAM] =
+    g_param_spec_object ("media-stream",
+                         P_("Media Stream"),
+                         P_("The media stream managed"),
+                         GTK_TYPE_MEDIA_STREAM,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (gobject_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/ui/gtkmediacontrols.ui");
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_adjustment);
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, volume_adjustment);
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, box);
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, play_button);
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_box);
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, time_label);
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, seek_scale);
+  gtk_widget_class_bind_template_child (widget_class, GtkMediaControls, duration_label);
+
+  gtk_widget_class_bind_template_callback (widget_class, play_button_clicked);
+  gtk_widget_class_bind_template_callback (widget_class, time_adjustment_changed);
+  gtk_widget_class_bind_template_callback (widget_class, volume_adjustment_changed);
+
+  gtk_widget_class_set_css_name (widget_class, I_("controls"));
+}
+
+static void
+gtk_media_controls_init (GtkMediaControls *controls)
+{
+  gtk_widget_init_template (GTK_WIDGET (controls));
+  gtk_widget_set_has_window (GTK_WIDGET (controls), FALSE);
+}
+
+/**
+ * gtk_media_controls_new:
+ * @stream: (allow-none) (transfer none): a #GtkMediaStream to
+ *     manage or %NULL for none.
+ *
+ * Creates a new #GtkMediaControls managing the @stream passed to it.
+ *
+ * Returns: a new #GtkMediaControls
+ **/
+GtkWidget *
+gtk_media_controls_new (GtkMediaStream *stream)
+{
+  return g_object_new (GTK_TYPE_MEDIA_CONTROLS,
+                       "media-stream", stream,
+                       NULL);
+}
+
+/**
+ * gtk_media_controls_get_media_stream:
+ * @controls: a #GtkMediaControls
+ *
+ * Gets the media stream managed by @controls or %NULL if none.
+ *
+ * Returns: (nullable): The media stream managed by @controls
+ **/
+GtkMediaStream *
+gtk_media_controls_get_media_stream (GtkMediaControls *controls)
+{
+  g_return_val_if_fail (GTK_IS_MEDIA_CONTROLS (controls), NULL);
+
+  return controls->stream;
+}
+
+static void
+update_timestamp (GtkMediaControls *controls)
+{
+  gint64 timestamp, duration;
+  char *time_string;
+
+  if (controls->stream)
+    {
+      timestamp = gtk_media_stream_get_timestamp (controls->stream);
+      duration = gtk_media_stream_get_duration (controls->stream);
+    }
+  else
+    {
+      timestamp = 0;
+      duration = 0;
+    }
+
+  time_string = totem_time_to_string (timestamp, FALSE, FALSE);
+  gtk_label_set_text (GTK_LABEL (controls->time_label), time_string);
+  g_free (time_string);
+
+  if (duration > 0)
+    {
+      time_string = totem_time_to_string (duration > timestamp ? duration - timestamp : 0, TRUE, FALSE);
+      gtk_label_set_text (GTK_LABEL (controls->duration_label), time_string);
+      g_free (time_string);
+
+      gtk_adjustment_set_value (controls->time_adjustment, (double) timestamp / G_USEC_PER_SEC);
+    }
+}
+
+static void
+update_duration (GtkMediaControls *controls)
+{
+  gint64 timestamp, duration;
+  char *time_string;
+
+  if (controls->stream)
+    {
+      timestamp = gtk_media_stream_get_timestamp (controls->stream);
+      duration = gtk_media_stream_get_duration (controls->stream);
+    }
+  else
+    {
+      timestamp = 0;
+      duration = 0;
+    }
+
+  time_string = totem_time_to_string (duration > timestamp ? duration - timestamp : 0, TRUE, FALSE);
+  gtk_label_set_text (GTK_LABEL (controls->duration_label), time_string);
+  gtk_widget_set_visible (controls->duration_label, duration > 0);
+  g_free (time_string);
+
+  gtk_adjustment_set_upper (controls->time_adjustment,
+                            gtk_adjustment_get_page_size (controls->time_adjustment)
+                            + (double) duration / G_USEC_PER_SEC);
+  gtk_adjustment_set_value (controls->time_adjustment, (double) timestamp / G_USEC_PER_SEC);
+}
+
+static void
+update_playing (GtkMediaControls *controls)
+{
+  gboolean playing;
+  const char *icon_name;
+
+  if (controls->stream)
+    playing = gtk_media_stream_get_playing (controls->stream);
+  else
+    playing = FALSE;
+
+  if (playing)
+    icon_name = "media-playback-pause-symbolic";
+  else
+    icon_name = "media-playback-start-symbolic";
+
+  gtk_button_set_icon_name (GTK_BUTTON (controls->play_button), icon_name);
+}
+
+static void
+update_seekable (GtkMediaControls *controls)
+{
+  gboolean seekable;
+
+  if (controls->stream)
+    seekable = gtk_media_stream_is_seekable (controls->stream);
+  else
+    seekable = FALSE;
+
+  gtk_widget_set_sensitive (controls->seek_scale, seekable);
+}
+
+static void
+update_volume (GtkMediaControls *controls)
+{
+  double volume;
+
+  if (controls->stream == NULL)
+    volume = 1.0;
+  else if (gtk_media_stream_get_muted (controls->stream))
+    volume = 0.0;
+  else
+    volume = gtk_media_stream_get_volume (controls->stream);
+
+  gtk_adjustment_set_value (controls->volume_adjustment, volume);
+}
+
+static void
+update_all (GtkMediaControls *controls)
+{
+  update_timestamp (controls);
+  update_duration (controls);
+  update_playing (controls);
+  update_seekable (controls);
+  update_volume (controls);
+}
+
+static void
+gtk_media_controls_notify_cb (GtkMediaStream   *stream,
+                              GParamSpec       *pspec,
+                              GtkMediaControls *controls)
+{
+  if (g_str_equal (pspec->name, "timestamp"))
+    update_timestamp (controls);
+  else if (g_str_equal (pspec->name, "duration"))
+    update_duration (controls);
+  else if (g_str_equal (pspec->name, "playing"))
+    update_playing (controls);
+  else if (g_str_equal (pspec->name, "seekable"))
+    update_seekable (controls);
+  else if (g_str_equal (pspec->name, "muted"))
+    update_volume (controls);
+  else if (g_str_equal (pspec->name, "volume"))
+    update_volume (controls);
+}
+
+void
+gtk_media_controls_set_media_stream (GtkMediaControls *controls,
+                                     GtkMediaStream   *stream)
+{
+  g_return_if_fail (GTK_IS_MEDIA_CONTROLS (controls));
+  g_return_if_fail (stream == NULL || GTK_IS_MEDIA_STREAM (stream));
+
+  if (controls->stream == stream)
+    return;
+
+  if (controls->stream)
+    {
+      g_signal_handlers_disconnect_by_func (controls->stream,
+                                            gtk_media_controls_notify_cb,
+                                            controls);
+      g_object_unref (controls->stream);
+      controls->stream = NULL;
+    }
+
+  if (stream)
+    {
+      controls->stream = g_object_ref (stream);
+      g_signal_connect (controls->stream,
+                        "notify",
+                        G_CALLBACK (gtk_media_controls_notify_cb),
+                        controls);
+    }
+
+  update_all (controls);
+  gtk_widget_set_sensitive (controls->box, stream != NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (controls), properties[PROP_MEDIA_STREAM]);
+}
+
diff --git a/gtk/gtkmediacontrols.h b/gtk/gtkmediacontrols.h
new file mode 100644 (file)
index 0000000..be39936
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2018 Benjamin Otte
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Benjamin Otte <otte@gnome.org>
+ */
+
+#ifndef __GTK_MEDIA_CONTROLS_H__
+#define __GTK_MEDIA_CONTROLS_H__
+
+#include <gtk/gtkmediastream.h>
+#include <gtk/gtkwidget.h>
+
+G_BEGIN_DECLS
+
+#define GTK_TYPE_MEDIA_CONTROLS         (gtk_media_controls_get_type ())
+
+GDK_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (GtkMediaControls, gtk_media_controls, GTK, MEDIA_CONTROLS, GtkWidget)
+
+GDK_AVAILABLE_IN_ALL
+GtkWidget      *gtk_media_controls_new                  (GtkMediaStream         *stream);
+
+GDK_AVAILABLE_IN_ALL
+GtkMediaStream *gtk_media_controls_get_media_stream     (GtkMediaControls       *controls);
+GDK_AVAILABLE_IN_ALL
+void            gtk_media_controls_set_media_stream     (GtkMediaControls       *controls,
+                                                         GtkMediaStream         *stream);
+
+
+G_END_DECLS
+
+#endif  /* __GTK_MEDIA_CONTROLS_H__ */
index f62afe4c3d8d6a30a80b05cc2e3d59fbf44a53d6..22d849f2e9426d175de083cfa5aa97e33b5939a9 100644 (file)
@@ -251,6 +251,7 @@ gtk_public_sources = files([
   'gtkliststore.c',
   'gtklockbutton.c',
   'gtkmain.c',
+  'gtkmediacontrols.c',
   'gtkmediafile.c',
   'gtkmediastream.c',
   'gtkmenu.c',
@@ -483,6 +484,7 @@ gtk_public_headers = files([
   'gtkliststore.h',
   'gtklockbutton.h',
   'gtkmain.h',
+  'gtkmediacontrols.h',
   'gtkmediafile.h',
   'gtkmediastream.h',
   'gtkmenu.h',
diff --git a/gtk/ui/gtkmediacontrols.ui b/gtk/ui/gtkmediacontrols.ui
new file mode 100644 (file)
index 0000000..895259d
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="gtk40">
+  <!-- interface-requires gtk+ 3.6 -->
+  <object class="GtkAdjustment" id="time_adjustment">
+    <property name="upper">10</property>
+    <property name="step-increment">1</property>
+    <property name="page-increment">10</property>
+    <signal name="value-changed" handler="time_adjustment_changed" object="GtkMediaControls" swapped="no"/>
+  </object>
+  <object class="GtkAdjustment" id="volume_adjustment">
+    <property name="upper">1</property>
+    <property name="step-increment">0.1</property>
+    <property name="page-increment">1</property>
+    <property name="value">1</property>
+    <signal name="value-changed" handler="volume_adjustment_changed" object="GtkMediaControls" swapped="no"/>
+  </object>
+  <template class="GtkMediaControls" parent="GtkWidget">
+    <child>
+      <object class="GtkBox" id="box">
+        <property name="hexpand">0</property>
+        <property name="sensitive">0</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkButton" id="play_button">
+            <property name="can-focus">1</property>
+            <property name="receives-default">1</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="icon-name">media-playback-start-symbolic</property>
+            <signal name="clicked" handler="play_button_clicked" object="GtkMediaControls" swapped="no"/>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="time_box">
+            <child>
+              <object class="GtkLabel" id="time_label">
+                <property name="can_focus">False</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScale" id="seek_scale">
+                <property name="adjustment">time_adjustment</property>
+                <property name="can_focus">True</property>
+                <property name="draw_value">False</property>
+                <property name="restrict-to-fill-level">False</property>
+                <property name="hexpand">True</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="duration_label">
+                <property name="can_focus">False</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkVolumeButton" id="volume_button">
+            <property name="adjustment">volume_adjustment</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>