summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Saprykin <xelfium@gmail.com>2010-07-02 00:17:08 +0400
committerBastien Nocera <hadess@hadess.net>2010-08-09 16:16:03 +0100
commit66d1958a89affb91380777409bd863faf668e872 (patch)
treecfecf08fa483221e93b6c06776c046c1a3da080b
parent640088aa5e38e4da6f4484a96c6a0d417a1e2245 (diff)
downloadtotem-66d1958a89affb91380777409bd863faf668e872.tar.gz
Merge chapters plugin with a master branch
New chapters plugin Add option to preferences to auto-load external chapters
-rw-r--r--configure.in11
-rw-r--r--data/totem.schemas.in14
-rw-r--r--data/totem.ui95
-rw-r--r--po/POTFILES.in5
-rw-r--r--src/plugins/chapters/Makefile.am47
-rw-r--r--src/plugins/chapters/chapters-edit.ui35
-rw-r--r--src/plugins/chapters/chapters-list.ui237
-rw-r--r--src/plugins/chapters/chapters.totem-plugin.in9
-rw-r--r--src/plugins/chapters/totem-chapters-utils.c95
-rw-r--r--src/plugins/chapters/totem-chapters-utils.h33
-rw-r--r--src/plugins/chapters/totem-chapters.c1259
-rw-r--r--src/plugins/chapters/totem-cmml-parser.c826
-rw-r--r--src/plugins/chapters/totem-cmml-parser.h82
-rw-r--r--src/plugins/chapters/totem-edit-chapter.c145
-rw-r--r--src/plugins/chapters/totem-edit-chapter.h58
-rw-r--r--src/totem-preferences.c43
16 files changed, 2992 insertions, 2 deletions
diff --git a/configure.in b/configure.in
index 6f7d9db88..8f3c695b5 100644
--- a/configure.in
+++ b/configure.in
@@ -66,7 +66,7 @@ AC_SUBST(TOTEM_API_VERSION)
AC_DEFINE_UNQUOTED(TOTEM_API_VERSION, ["$TOTEM_API_VERSION"], [Define to the Totem plugin API version])
# The full list of plugins
-allowed_plugins="bemused brasero-disc-recorder coherence_upnp dbus-service galago gromit iplayer jamendo lirc media-player-keys mythtv ontop opensubtitles properties publish pythonconsole sample-python sample-vala screensaver screenshot sidebar-test skipto thumbnail tracker youtube"
+allowed_plugins="bemused brasero-disc-recorder chapters coherence_upnp dbus-service galago gromit iplayer jamendo lirc media-player-keys mythtv ontop opensubtitles properties publish pythonconsole sample-python sample-vala screensaver screenshot sidebar-test skipto thumbnail tracker youtube"
PLUGINDIR='${libdir}/totem/plugins'
AC_SUBST(PLUGINDIR)
@@ -537,6 +537,14 @@ for plugin in ${used_plugins}; do
add_plugin="0"
fi
;;
+ chapters)
+ PKG_CHECK_MODULES(CHAPTERS, libxml-2.0 >= 2.6.0 gtk+-3.0 glib-2.0 >= 2.15.0,
+ [BUILD_CHAPTERS=yes], [BUILD_CHAPTERS=no])
+ if test "${BUILD_CHAPTERS}" != "yes" ; then
+ plugin_error_or_ignore "you need gtk+-3.0, glib-2.0 >= 2.15.0 and libxml-2.0 >= 2.6.0 to use the chapters plugin"
+ add_plugin="0"
+ fi
+ ;;
esac
# Add the specified plugin
@@ -797,6 +805,7 @@ src/plugins/pythonconsole/Makefile
src/plugins/publish/Makefile
src/plugins/jamendo/Makefile
src/plugins/brasero-disc-recorder/Makefile
+src/plugins/chapters/Makefile
src/backend/Makefile
browser-plugin/Makefile
data/Makefile
diff --git a/data/totem.schemas.in b/data/totem.schemas.in
index 409042ebd..342c332a4 100644
--- a/data/totem.schemas.in
+++ b/data/totem.schemas.in
@@ -304,6 +304,20 @@
</schema>
<schema>
+ <key>/schemas/apps/totem/autoload_chapters</key>
+ <applyto>/apps/totem/autoload_chapters</applyto>
+ <owner>totem</owner>
+ <type>bool</type>
+ <default>true</default>
+ <locale name="C">
+ <short>Whether to autoload external chapter files when a movie is loaded</short>
+ <long>
+ Whether to autoload external chapter files when a movie is loaded.
+ </long>
+ </locale>
+ </schema>
+
+ <schema>
<key>/schemas/apps/totem/remember_position</key>
<applyto>/apps/totem/remember_position</applyto>
<owner>totem</owner>
diff --git a/data/totem.ui b/data/totem.ui
index dfcbaa69d..587aa3150 100644
--- a/data/totem.ui
+++ b/data/totem.ui
@@ -1109,6 +1109,101 @@
<property name="fill">True</property>
</packing>
</child>
+
+ <child>
+ <object class="GtkVBox" id="vbox7">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="spacing">6</property>
+ <property name="orientation">vertical</property>
+
+ <child>
+ <object class="GtkLabel" id="tpw_ext_chapters_label">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">External Chapters</property>
+ <property name="use_underline">False</property>
+ <property name="use_markup">True</property>
+ <property name="justify">GTK_JUSTIFY_LEFT</property>
+ <property name="wrap">False</property>
+ <property name="selectable">False</property>
+ <property name="xalign">0</property>
+ <property name="yalign">0.5</property>
+ <property name="xpad">0</property>
+ <property name="ypad">0</property>
+ <property name="ellipsize">PANGO_ELLIPSIZE_NONE</property>
+ <property name="width_chars">-1</property>
+ <property name="single_line_mode">False</property>
+ <property name="angle">0</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ </packing>
+ </child>
+
+ <child>
+ <object class="GtkAlignment" id="alignment3_2">
+ <property name="visible">True</property>
+ <property name="xalign">0.5</property>
+ <property name="yalign">0.5</property>
+ <property name="xscale">1</property>
+ <property name="yscale">1</property>
+ <property name="top_padding">0</property>
+ <property name="bottom_padding">0</property>
+ <property name="left_padding">12</property>
+ <property name="right_padding">0</property>
+
+ <child>
+ <object class="GtkTable" id="table3_2">
+ <property name="visible">True</property>
+ <property name="n_rows">1</property>
+ <property name="n_columns">2</property>
+ <property name="homogeneous">False</property>
+ <property name="row_spacing">6</property>
+ <property name="column_spacing">12</property>
+
+ <child>
+ <object class="GtkCheckButton" id="tpw_auto_chapters_checkbutton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="label" translatable="yes">Load _chapter files when movie is loaded</property>
+ <property name="use_underline">True</property>
+ <property name="relief">GTK_RELIEF_NORMAL</property>
+ <property name="focus_on_click">True</property>
+ <property name="active">False</property>
+ <property name="inconsistent">False</property>
+ <property name="draw_indicator">True</property>
+ <signal name="toggled" handler="auto_chapters_toggled_cb"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="right_attach">2</property>
+ <property name="top_attach">0</property>
+ <property name="bottom_attach">1</property>
+ <property name="x_options">fill</property>
+ <property name="y_options"/>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="padding">0</property>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
</object>
<packing>
<property name="tab_expand">False</property>
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 70b06a828..a61eb2fa7 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -40,6 +40,11 @@ src/plugins/totem-plugins-engine.c
src/plugins/bemused/totem-bemused.c
src/plugins/brasero-disc-recorder/totem-disc-recorder.c
[type: gettext/ini]src/plugins/brasero-disc-recorder/brasero-disc-recorder.totem-plugin.in
+[type: gettext/ini]src/plugins/chapters/chapters.totem-plugin.in
+[type: gettext/glade]src/plugins/chapters/chapters-edit.ui
+[type: gettext/glade]src/plugins/chapters/chapters-list.ui
+src/plugins/chapters/totem-chapters.c
+src/plugins/chapters/totem-cmml-parser.c
src/plugins/coherence_upnp/coherence_upnp.py
[type: gettext/ini]src/plugins/coherence_upnp/coherence_upnp.totem-plugin.in
[type: gettext/ini]src/plugins/dbus-service/dbus-service.totem-plugin.in
diff --git a/src/plugins/chapters/Makefile.am b/src/plugins/chapters/Makefile.am
new file mode 100644
index 000000000..b4f638fc5
--- /dev/null
+++ b/src/plugins/chapters/Makefile.am
@@ -0,0 +1,47 @@
+modules_flags = -export_dynamic -avoid-version -module
+
+plugindir = $(PLUGINDIR)/chapters
+plugin_LTLIBRARIES = libchapters.la
+
+plugin_in_files = chapters.totem-plugin.in
+
+%.totem-plugin: %.totem-plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.totem-plugin.in=.totem-plugin)
+
+uidir = $(plugindir)
+ui_DATA = chapters-list.ui chapters-edit.ui
+
+common_defines = \
+ -D_REENTRANT \
+ -DDBUS_API_SUBJECT_TO_CHANGE \
+ -DGNOMELOCALEDIR=\""$(datadir)/locale"\" \
+ -DGCONF_PREFIX=\""/apps/totem"\" \
+ -DDATADIR=\""$(datadir)"\" \
+ -DLIBEXECDIR=\""$(libexecdir)"\" \
+ -DBINDIR=\""$(bindir)"\" \
+ -DTOTEM_PLUGIN_DIR=\""$(libdir)/totem/plugins"\"\
+ $(DISABLE_DEPRECATED)
+
+libchapters_la_SOURCES = totem-chapters.c totem-cmml-parser.c totem-cmml-parser.h totem-chapters-utils.c totem-chapters-utils.h totem-edit-chapter.c totem-edit-chapter.h
+libchapters_la_LDFLAGS = $(modules_flags)
+libchapters_la_LIBADD = $(CHAPTERS_LIBS)
+libchapters_la_CPPFLAGS = $(common_defines)
+
+libchapters_la_CFLAGS = \
+ $(DEPENDENCY_CFLAGS) \
+ $(PEAS_CFLAGS) \
+ $(CHAPTERS_CFLAGS) \
+ $(WARN_CFLAGS) \
+ $(DBUS_CFLAGS) \
+ $(AM_CFLAGS) \
+ -I$(top_srcdir)/ \
+ -I$(top_srcdir)/src \
+ -I$(top_srcdir)/src/backend \
+ -I$(top_srcdir)/src/plugins
+
+EXTRA_DIST = $(plugin_in_files) $(ui_DATA)
+
+CLEANFILES = $(plugin_DATA) $(BUILT_SOURCES)
+DISTCLEANFILES = $(plugin_DATA)
+
diff --git a/src/plugins/chapters/chapters-edit.ui b/src/plugins/chapters/chapters-edit.ui
new file mode 100644
index 000000000..efa4a8f10
--- /dev/null
+++ b/src/plugins/chapters/chapters-edit.ui
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<interface>
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkVBox" id="main_vbox">
+ <property name="visible">True</property>
+ <property name="border_width">5</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkLabel" id="title_label">
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <property name="label" translatable="yes">Enter new name for a chapter:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="title_entry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="invisible_char">&#x25CF;</property>
+ <property name="activates_default">True</property>
+ <signal name="changed" handler="title_entry_changed_cb"/>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+</interface>
diff --git a/src/plugins/chapters/chapters-list.ui b/src/plugins/chapters/chapters-list.ui
new file mode 100644
index 000000000..ca2740405
--- /dev/null
+++ b/src/plugins/chapters/chapters-list.ui
@@ -0,0 +1,237 @@
+<?xml version="1.0"?>
+<interface>
+ <object class="GtkUIManager" id="totem-chapters-ui-manager">
+ <child>
+ <object class="GtkActionGroup" id="chapters-action-group">
+ <child>
+ <object class="GtkAction" id="remove">
+ <property name="label" translatable="yes">_Remove</property>
+ <property name="tooltip" translatable="yes">Remove chapter from the list</property>
+ <property name="stock-id">gtk-remove</property>
+ <signal name="activate" handler="popup_remove_action_cb"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkAction" id="goto">
+ <property name="label" translatable="yes">_Go to</property>
+ <property name="tooltip" translatable="yes">Go to chapter</property>
+ <property name="stock-id">gtk-jump-to</property>
+ <signal name="activate" handler="popup_goto_action_cb"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ <ui>
+ <popup name="totem-chapters-popup">
+ <menuitem name="remove" action="remove"/>
+ <menuitem name="goto" action="goto"/>
+ </popup>
+ </ui>
+ </object>
+
+ <requires lib="gtk+" version="2.16"/>
+ <!-- interface-naming-policy project-wide -->
+ <object class="GtkTreeStore" id="clip_tree_store">
+ <columns>
+ <!-- column-name gdkpixbuf -->
+ <column type="GdkPixbuf"/>
+ <!-- column-name gchararray -->
+ <column type="gchararray"/>
+ <!-- column-name gchararray1 -->
+ <column type="gchararray"/>
+ <!-- column-name gchararray2 -->
+ <column type="gchararray"/>
+ <!-- column-name gint1 -->
+ <column type="gint64"/>
+ </columns>
+ </object>
+ <object class="GtkVBox" id="main_vbox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hscrollbar_policy">automatic</property>
+ <property name="vscrollbar_policy">automatic</property>
+ <child>
+ <object class="GtkTreeView" id="chapters_tree_view">
+ <property name="visible">True</property>
+ <property name="sensitive">True</property>
+ <property name="can_focus">True</property>
+ <property name="model">clip_tree_store</property>
+ <property name="headers_visible">False</property>
+ <property name="show_expanders">False</property>
+ <property name="tooltip_column">2</property>
+ <signal name="button_press_event" handler="tree_view_button_press_cb"/>
+ <signal name="key_press_event" handler="tree_view_key_press_cb"/>
+ <signal name="row_activated" handler="tree_view_row_activated_cb"/>
+ <signal name="popup_menu" handler="tree_view_popup_menu_cb"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkHBox" id="hbox1">
+ <property name="visible">True</property>
+ <property name="spacing">6</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkButton" id="add_button">
+ <property name="visible">True</property>
+ <property name="sensitive">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Add...</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="add_button_clicked_cb"/>
+ <child>
+ <object class="GtkImage" id="image1">
+ <property name="visible">True</property>
+ <property name="stock">gtk-add</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="remove_button">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Remove</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="remove_button_clicked_cb"/>
+ <child>
+ <object class="GtkImage" id="image2">
+ <property name="visible">True</property>
+ <property name="stock">gtk-remove</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="goto_button">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Go to chapter</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="goto_button_clicked_cb"/>
+ <child>
+ <object class="GtkImage" id="image3">
+ <property name="visible">True</property>
+ <property name="stock">gtk-jump-to</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="save_button">
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Save Changes</property>
+ <property name="relief">none</property>
+ <signal name="clicked" handler="save_button_clicked_cb"/>
+ <child>
+ <object class="GtkImage" id="image4">
+ <property name="visible">True</property>
+ <property name="stock">gtk-save</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+
+ <object class="GtkVBox" id="load_vbox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <property name="homogeneous">True</property>
+ <child>
+ <object class="GtkAlignment" id="alignment1">
+ <property name="visible">True</property>
+ <property name="yalign">1</property>
+ <property name="yscale">0</property>
+ <child>
+ <object class="GtkLabel" id="chapters_label">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">No chapters data</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkAlignment" id="alignment2">
+ <property name="visible">True</property>
+ <property name="yalign">0</property>
+ <property name="xscale">0</property>
+ <property name="yscale">0</property>
+ <child>
+ <object class="GtkHBox" id="hbox2">
+ <property name="visible">True</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkButton" id="load_button">
+ <property name="label" translatable="yes">Load chapters...</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Load chapters from external file</property>
+ <signal name="clicked" handler="load_button_clicked_cb"/>
+ </object>
+ <packing>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="continue_button">
+ <property name="label" translatable="yes">Continue without</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Continue to watch movie without loaded chapters</property>
+ <signal name="clicked" handler="continue_button_clicked_cb"/>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+
+</interface>
diff --git a/src/plugins/chapters/chapters.totem-plugin.in b/src/plugins/chapters/chapters.totem-plugin.in
new file mode 100644
index 000000000..154d7c329
--- /dev/null
+++ b/src/plugins/chapters/chapters.totem-plugin.in
@@ -0,0 +1,9 @@
+[Totem Plugin]
+Module=chapters
+IAge=1
+Builtin=true
+_Name=Chapters
+_Description=Chapters support
+Authors=Alexander Saprykin
+Copyright=Copyright © 2010 Alexander Saprykin
+Website=http://www.gnome.org/projects/totem/
diff --git a/src/plugins/chapters/totem-chapters-utils.c b/src/plugins/chapters/totem-chapters-utils.c
new file mode 100644
index 000000000..c3aa49712
--- /dev/null
+++ b/src/plugins/chapters/totem-chapters-utils.c
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2010 Alexander Saprykin <xelfium@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ */
+
+/**
+ * SECTION:totem-chapters-utils
+ * @short_description: misc helper functions
+ * @stability: Unstable
+ * @include: totem-chapters-utils.h
+ *
+ * These functions are used for misc operations within chapters plugin.
+ **/
+
+#include <glib.h>
+
+#include "totem-chapters-utils.h"
+#include <string.h>
+
+/**
+ * totem_remove_file_extension:
+ * @filename: filename to remove extension from
+ *
+ * Removes extension from the @filename.
+ *
+ * Returns: filename without extension on success, %NULL otherwise; free with g_free ().
+ **/
+gchar *
+totem_remove_file_extension (const gchar *filename)
+{
+ gchar *p, *s;
+
+ g_return_val_if_fail (filename != NULL, NULL);
+ g_return_val_if_fail (strlen (filename) > 0, NULL);
+
+ p = g_strrstr (filename, ".");
+ if (G_UNLIKELY (p == NULL))
+ return NULL;
+
+ s = g_strrstr (p, G_DIR_SEPARATOR_S);
+ if (G_UNLIKELY (s != NULL))
+ return NULL;
+
+ return g_strndup (filename, ABS(p - filename));
+}
+
+/**
+ * totem_change_file_extension:
+ * @filename: filename to change extension in
+ * @ext: new extension for @filename
+ *
+ * Changes extension in @filename to @ext.
+ *
+ * Returns: filename with new extension on success, %NULL otherwise; free with g_free ().
+ **/
+gchar *
+totem_change_file_extension (const gchar *filename,
+ const gchar *ext)
+{
+ gchar *no_ext, *new_file;
+
+ g_return_val_if_fail (filename != NULL, NULL);
+ g_return_val_if_fail (strlen (filename) > 0, NULL);
+ g_return_val_if_fail (ext != NULL, NULL);
+ g_return_val_if_fail (strlen (ext) > 0, NULL);
+
+ no_ext = totem_remove_file_extension (filename);
+
+ if (no_ext == NULL)
+ return NULL;
+
+ new_file = g_strconcat (no_ext, ".", ext, NULL);
+ g_free (no_ext);
+
+ return new_file;
+}
diff --git a/src/plugins/chapters/totem-chapters-utils.h b/src/plugins/chapters/totem-chapters-utils.h
new file mode 100644
index 000000000..d10ffaaf9
--- /dev/null
+++ b/src/plugins/chapters/totem-chapters-utils.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 Alexander Saprykin <xelfium@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ */
+
+#ifndef TOTEM_CHAPTERS_UTILS_H_
+#define TOTEM_CHAPTERS_UTILS_H_
+
+#include <glib.h>
+
+gchar * totem_remove_file_extension (const gchar *filename);
+gchar * totem_change_file_extension (const gchar *filename, const gchar *ext);
+
+#endif /* TOTEM_CHAPTERS_UTILS_H_ */
diff --git a/src/plugins/chapters/totem-chapters.c b/src/plugins/chapters/totem-chapters.c
new file mode 100644
index 000000000..6ea0173e3
--- /dev/null
+++ b/src/plugins/chapters/totem-chapters.c
@@ -0,0 +1,1259 @@
+/*
+ * Copyright (C) 2010 Alexander Saprykin <xelfium@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib-object.h>
+#include <glib/gi18n-lib.h>
+#include <glib/gprintf.h>
+#include <gio/gio.h>
+#include <gmodule.h>
+#include <gdk-pixbuf/gdk-pixdata.h>
+#include <gdk/gdkkeysyms.h>
+#include <gconf/gconf-client.h>
+#include <unistd.h>
+#include <math.h>
+
+#include "bacon-video-widget.h"
+#include "totem-plugin.h"
+#include "totem-interface.h"
+#include "totem.h"
+#include "totem-cmml-parser.h"
+#include "totem-chapters-utils.h"
+#include "totem-edit-chapter.h"
+
+#define TOTEM_TYPE_CHAPTERS_PLUGIN (totem_chapters_plugin_get_type ())
+#define TOTEM_CHAPTERS_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TOTEM_TYPE_CHAPTERS_PLUGIN, TotemChaptersPlugin))
+#define TOTEM_CHAPTERS_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TOTEM_TYPE_CHAPTERS_PLUGIN, TotemChaptersPluginClass))
+#define TOTEM_IS_CHAPTERS_PLUGIN(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TOTEM_TYPE_CHAPTERS_PLUGIN))
+#define TOTEM_IS_CHAPTERS_PLUGIN_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TOTEM_TYPE_CHAPTERS_PLUGIN))
+#define TOTEM_CHAPTERS_PLUGIN_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TOTEM_TYPE_CHAPTERS_PLUGIN, TotemChaptersPluginClass))
+
+#define TOTEM_CHAPTERS_PLUGIN_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), TOTEM_TYPE_CHAPTERS_PLUGIN, TotemChaptersPluginPrivate))
+
+#define CHAPTER_TOOLTIP(title, start) g_strdup_printf ( _("<b>Title: </b>%s\n<b>Start time: </b>%s"), \
+ ( title ), ( start ) )
+
+#define CHAPTER_TITLE(title, start) g_strdup_printf ("<big>%s</big>\n" \
+ "<small><span foreground='grey'>%s" \
+ "</span></small>", \
+ ( title ), ( start ) )
+#define ICON_SCALE_RATIO 2
+
+typedef struct {
+ GtkWidget *tree;
+ GtkWidget *add_button,
+ *remove_button,
+ *save_button,
+ *load_button,
+ *goto_button,
+ *continue_button,
+ *list_box,
+ *load_box;
+ GtkActionGroup *action_group;
+ GtkUIManager *ui_manager;
+ gboolean was_played;
+ GdkPixbuf *last_frame;
+ gint64 last_time;
+ gchar *cmml_mrl;
+ gboolean autoload;
+ GCancellable *cancellable[2];
+ GConfClient *gconf;
+ guint autoload_handle_id;
+} TotemChaptersPluginPrivate;
+
+typedef struct {
+ PeasExtensionBase parent;
+ TotemObject *totem;
+ TotemEditChapter *edit_chapter;
+ TotemChaptersPluginPrivate *priv;
+} TotemChaptersPlugin;
+
+typedef struct {
+ PeasExtensionBaseClass parent_class;
+} TotemChaptersPluginClass;
+
+enum {
+ CHAPTERS_PIXBUF_COLUMN = 0,
+ CHAPTERS_TITLE_COLUMN,
+ CHAPTERS_TOOLTIP_COLUMN,
+ CHAPTERS_TITLE_PRIV_COLUMN,
+ CHAPTERS_TIME_PRIV_COLUMN,
+ CHAPTERS_N_COLUMNS
+};
+
+G_MODULE_EXPORT GType register_totem_plugin (GTypeModule *module);
+GType totem_chapters_plugin_get_type (void) G_GNUC_CONST;
+static void totem_chapters_plugin_finalize (GObject *object);
+static void totem_file_opened_async_cb (TotemObject *totem, const gchar *uri, TotemChaptersPlugin *plugin);
+static void totem_file_opened_result_cb (gpointer data, gpointer user_data);
+static void totem_file_closed_cb (TotemObject *totem, TotemChaptersPlugin *plugin);
+static void add_chapter_to_the_list (gpointer data, gpointer user_data);
+static void add_chapter_to_the_list_new (TotemChaptersPlugin *plugin, const gchar *title, gint64 time);
+static gboolean check_available_time (TotemChaptersPlugin *plugin, gint64 time);
+static GdkPixbuf * get_chapter_pixbuf (GdkPixbuf *src);
+static void chapter_edit_dialog_response_cb (GtkDialog *dialog, gint response, TotemChaptersPlugin *plugin);
+static void prepare_chapter_edit (GtkCellRenderer *renderer, GtkCellEditable *editable, gchar *path, gpointer user_data);
+static void finish_chapter_edit (GtkCellRendererText *renderer, gchar *path, gchar *new_text, gpointer user_data);
+static void chapter_selection_changed_cb (GtkTreeSelection *tree_selection, TotemChaptersPlugin *plugin);
+static void show_chapter_edit_dialog (TotemChaptersPlugin *plugin);
+static void save_chapters_result_cb (gpointer data, gpointer user_data);
+static GList * get_chapters_list (TotemChaptersPlugin *plugin);
+static gboolean show_popup_menu (TotemChaptersPlugin *plugin, GdkEventButton *event);
+static void gconf_autoload_changed_cb (GConfClient *gconf, guint cnx_id, GConfEntry *entry, TotemChaptersPlugin *plugin);
+static void load_chapters_from_file (const gchar *uri, gboolean from_dialog, TotemChaptersPlugin *plugin);
+static void set_no_data_visible (gboolean visible, gboolean show_buttons, TotemChaptersPlugin *plugin);
+
+/* GtkBuilder callbacks */
+void add_button_clicked_cb (GtkButton *button, TotemChaptersPlugin *plugin);
+void remove_button_clicked_cb (GtkButton *button, TotemChaptersPlugin *plugin);
+void save_button_clicked_cb (GtkButton *button, TotemChaptersPlugin *plugin);
+void goto_button_clicked_cb (GtkButton *button, TotemChaptersPlugin *plugin);
+void tree_view_row_activated_cb (GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, TotemChaptersPlugin *plugin);
+gboolean tree_view_button_press_cb (GtkTreeView *tree_view, GdkEventButton *event, TotemChaptersPlugin *plugin);
+gboolean tree_view_key_press_cb (GtkTreeView *tree_view, GdkEventKey *event, TotemChaptersPlugin *plugin);
+gboolean tree_view_popup_menu_cb (GtkTreeView *tree_view, TotemChaptersPlugin *plugin);
+void popup_remove_action_cb (GtkAction *action, TotemChaptersPlugin *plugin);
+void popup_goto_action_cb (GtkAction *action, TotemChaptersPlugin *plugin);
+void load_button_clicked_cb (GtkButton *button, TotemChaptersPlugin *plugin);
+void continue_button_clicked_cb (GtkButton *button, TotemChaptersPlugin *plugin);
+
+TOTEM_PLUGIN_REGISTER (TOTEM_TYPE_CHAPTERS_PLUGIN, TotemChaptersPlugin, totem_chapters_plugin)
+
+static void
+totem_chapters_plugin_class_init (TotemChaptersPluginClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ g_type_class_add_private (klass, sizeof (TotemChaptersPluginPrivate));
+
+ object_class->finalize = totem_chapters_plugin_finalize;
+}
+
+static void
+totem_chapters_plugin_init (TotemChaptersPlugin *plugin)
+{
+ plugin->priv = TOTEM_CHAPTERS_PLUGIN_GET_PRIVATE (plugin);
+}
+
+static void
+totem_chapters_plugin_finalize (GObject *object)
+{
+ G_OBJECT_CLASS (totem_chapters_plugin_parent_class)->finalize (object);
+}
+
+static GdkPixbuf *
+get_chapter_pixbuf (GdkPixbuf *src)
+{
+ GdkPixbuf *pixbuf;
+ gint width, height;
+ gfloat pix_width, pix_height;
+ gfloat ratio, new_width;
+
+ gtk_icon_size_lookup (GTK_ICON_SIZE_LARGE_TOOLBAR, &width, &height);
+ height *= ICON_SCALE_RATIO;
+
+ if (src != NULL) {
+ pix_width = (gfloat) gdk_pixbuf_get_width (src);
+ pix_height = (gfloat) gdk_pixbuf_get_height (src);
+
+ /* calc height ratio and apply it to width */
+ ratio = pix_height / height;
+ new_width = pix_width / ratio;
+ width = ceil (new_width);
+
+ /* scale video frame if need */
+ pixbuf = gdk_pixbuf_scale_simple (src, width, height, GDK_INTERP_BILINEAR);
+ } else {
+ /* 16:10 aspect ratio by default */
+ new_width = (gfloat) height * 1.6;
+ width = ceil (new_width);
+
+ pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE, 8, width, height);
+ gdk_pixbuf_fill (pixbuf, 0x00000000);
+ }
+
+ return pixbuf;
+}
+
+static void
+add_chapter_to_the_list (gpointer data,
+ gpointer user_data)
+{
+ TotemChaptersPlugin *plugin;
+ GdkPixbuf *pixbuf;
+ GtkTreeIter iter;
+ GtkWidget *tree;
+ GtkTreeStore *store;
+ TotemCmmlClip *clip;
+ gchar *text, *start, *tip;
+
+ g_return_if_fail (data != NULL);
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (user_data));
+
+ plugin = TOTEM_CHAPTERS_PLUGIN (user_data);
+ tree = plugin->priv->tree;
+ store = GTK_TREE_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree)));
+ clip = ((TotemCmmlClip *) data);
+
+ /* prepare tooltip data */
+ start = totem_cmml_convert_msecs_to_str (clip->time_start);
+ tip = CHAPTER_TOOLTIP (clip->title, start);
+
+ /* append clip to the sidebar list */
+ gtk_tree_store_append (store, &iter, NULL);
+ text = CHAPTER_TITLE (clip->title, start);
+
+ if (G_LIKELY (clip->pixbuf != NULL))
+ pixbuf = g_object_ref (clip->pixbuf);
+ else
+ pixbuf = get_chapter_pixbuf (NULL);
+
+ gtk_tree_store_set (store, &iter,
+ CHAPTERS_TITLE_COLUMN, text,
+ CHAPTERS_TOOLTIP_COLUMN, tip,
+ CHAPTERS_PIXBUF_COLUMN, pixbuf,
+ CHAPTERS_TITLE_PRIV_COLUMN, clip->title,
+ CHAPTERS_TIME_PRIV_COLUMN, clip->time_start,
+ -1);
+
+ g_object_unref (pixbuf);
+ g_free (text);
+ g_free (start);
+ g_free (tip);
+}
+
+static void
+add_chapter_to_the_list_new (TotemChaptersPlugin *plugin,
+ const gchar *title,
+ gint64 time)
+{
+ GdkPixbuf *pixbuf;
+ GtkTreeIter iter, cur_iter, res_iter;
+ GtkTreeModel *store;
+ gchar *text, *start, *tip;
+ gboolean valid;
+ gint64 cur_time, prev_time = 0;
+ gint iter_count = 0;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+ g_return_if_fail (title != NULL);
+ g_return_if_fail (time >= 0);
+
+ store = gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree));
+ valid = gtk_tree_model_get_iter_first (store, &cur_iter);
+
+ while (valid) {
+ gtk_tree_model_get (store, &cur_iter,
+ CHAPTERS_TIME_PRIV_COLUMN, &cur_time,
+ -1);
+
+ if (time < cur_time && time > prev_time)
+ break;
+
+ iter_count += 1;
+ res_iter = cur_iter;
+ prev_time = cur_time;
+
+ valid = gtk_tree_model_iter_next (store, &cur_iter);
+ }
+
+ /* prepare tooltip data */
+ start = totem_cmml_convert_msecs_to_str (time);
+ tip = CHAPTER_TOOLTIP (title, start);
+
+ /* insert clip into the sidebar list at proper position */
+ if (iter_count > 0)
+ gtk_tree_store_insert_after (GTK_TREE_STORE (store), &iter, NULL, &res_iter);
+ else
+ gtk_tree_store_insert_after (GTK_TREE_STORE (store), &iter, NULL, NULL);
+
+ text = CHAPTER_TITLE (title, start);
+ pixbuf = get_chapter_pixbuf (plugin->priv->last_frame);
+
+ gtk_tree_store_set (GTK_TREE_STORE (store), &iter,
+ CHAPTERS_TITLE_COLUMN, text,
+ CHAPTERS_TOOLTIP_COLUMN, tip,
+ CHAPTERS_PIXBUF_COLUMN, pixbuf,
+ CHAPTERS_TITLE_PRIV_COLUMN, title,
+ CHAPTERS_TIME_PRIV_COLUMN, time,
+ -1);
+
+ g_object_unref (pixbuf);
+ g_free (text);
+ g_free (start);
+ g_free (tip);
+}
+
+static gboolean
+check_available_time (TotemChaptersPlugin *plugin,
+ gint64 time)
+{
+ GtkTreeModel *store;
+ GtkTreeIter iter;
+ gboolean valid;
+ gint64 cur_time;
+
+ g_return_val_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin), FALSE);
+
+ store = gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree));
+
+ valid = gtk_tree_model_get_iter_first (store, &iter);
+ while (valid) {
+ gtk_tree_model_get (store, &iter,
+ CHAPTERS_TIME_PRIV_COLUMN, &cur_time,
+ -1);
+
+ if (cur_time == time)
+ return FALSE;
+
+ if (cur_time > time)
+ return TRUE;
+
+ valid = gtk_tree_model_iter_next (store, &iter);
+ }
+
+ return TRUE;
+}
+
+static void
+totem_file_opened_result_cb (gpointer data,
+ gpointer user_data)
+{
+ TotemCmmlAsyncData *adata;
+ TotemChaptersPlugin *plugin;
+
+ g_return_if_fail (data != NULL);
+
+ adata = (TotemCmmlAsyncData *) data;
+ plugin = TOTEM_CHAPTERS_PLUGIN (adata->user_data);
+
+ if (G_UNLIKELY (!adata->successful)) {
+ if (G_UNLIKELY (g_cancellable_is_cancelled (adata->cancellable))) {
+ /* if operation was cancelled due to plugin deactivation only clean up */
+ g_object_unref (adata->cancellable);
+ g_free (adata->file);
+ g_free (adata->error);
+ g_list_foreach (adata->list, (GFunc) totem_cmml_clip_free, NULL);
+ g_list_free (adata->list);
+ g_free (adata);
+ return;
+ } else
+ totem_action_error (plugin->totem, _("Error while reading file with chapters"),
+ adata->error);
+ }
+
+ if (adata->is_exists && adata->from_dialog) {
+ g_free (plugin->priv->cmml_mrl);
+ plugin->priv->cmml_mrl = g_strdup (adata->file);
+ }
+
+ g_list_foreach (adata->list, (GFunc) add_chapter_to_the_list, plugin);
+ g_list_foreach (adata->list, (GFunc) totem_cmml_clip_free, NULL);
+ g_list_free (adata->list);
+
+ /* do not show tree if read operation failed */
+ set_no_data_visible (!adata->successful || !adata->is_exists, TRUE, plugin);
+
+ g_object_unref (adata->cancellable);
+ g_free (adata->file);
+ g_free (adata->error);
+ g_free (adata);
+}
+
+static void
+totem_file_opened_async_cb (TotemObject *totem,
+ const gchar *uri,
+ TotemChaptersPlugin *plugin)
+{
+ gchar *cmml_file;
+
+ g_return_if_fail (TOTEM_IS_OBJECT (totem));
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+ g_return_if_fail (uri != NULL);
+
+ cmml_file = totem_change_file_extension (uri, "cmml");
+ /* if file has no extension - append it */
+ if (cmml_file == NULL)
+ cmml_file = g_strconcat (uri, ".cmml", NULL);
+
+ plugin->priv->cmml_mrl = cmml_file;
+
+ if (!plugin->priv->autoload)
+ set_no_data_visible (TRUE, TRUE, plugin);
+ else
+ load_chapters_from_file (cmml_file, FALSE, plugin);
+}
+
+static void
+totem_file_closed_cb (TotemObject *totem,
+ TotemChaptersPlugin *plugin)
+{
+ GtkTreeStore *store;
+
+ g_return_if_fail (TOTEM_IS_OBJECT (totem) && TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ store = GTK_TREE_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree)));
+
+ gtk_tree_store_clear (store);
+
+ if (G_UNLIKELY (plugin->edit_chapter != NULL))
+ gtk_widget_destroy (GTK_WIDGET (plugin->edit_chapter));
+
+ if (G_UNLIKELY (plugin->priv->last_frame != NULL))
+ g_object_unref (G_OBJECT (plugin->priv->last_frame));
+
+ g_free (plugin->priv->cmml_mrl);
+ plugin->priv->cmml_mrl = NULL;
+
+ gtk_widget_set_sensitive (plugin->priv->remove_button, FALSE);
+ gtk_widget_set_sensitive (plugin->priv->save_button, FALSE);
+
+ set_no_data_visible (TRUE, FALSE, plugin);
+}
+
+static void
+chapter_edit_dialog_response_cb (GtkDialog *dialog,
+ gint response,
+ TotemChaptersPlugin *plugin)
+{
+ gchar *title;
+
+ g_return_if_fail (TOTEM_IS_EDIT_CHAPTER (dialog));
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ if (response != GTK_RESPONSE_OK) {
+ gtk_widget_destroy (GTK_WIDGET (plugin->edit_chapter));
+
+ if (plugin->priv->last_frame != NULL)
+ g_object_unref (G_OBJECT (plugin->priv->last_frame));
+
+ if (plugin->priv->was_played)
+ totem_action_play (plugin->totem);
+ return;
+ }
+
+ gtk_widget_hide (GTK_WIDGET (dialog));
+
+ title = totem_edit_chapter_get_title (TOTEM_EDIT_CHAPTER (dialog));
+ add_chapter_to_the_list_new (plugin, title, plugin->priv->last_time);
+
+ gtk_widget_set_sensitive (plugin->priv->save_button, TRUE);
+
+ if (G_LIKELY (plugin->priv->last_frame != NULL))
+ g_object_unref (G_OBJECT (plugin->priv->last_frame));
+
+ g_free (title);
+ gtk_widget_destroy (GTK_WIDGET (plugin->edit_chapter));
+
+ if (plugin->priv->was_played)
+ totem_action_play (plugin->totem);
+}
+
+static void
+prepare_chapter_edit (GtkCellRenderer *renderer,
+ GtkCellEditable *editable,
+ gchar *path,
+ gpointer user_data)
+{
+ TotemChaptersPlugin *plugin;
+ GtkTreeModel *store;
+ GtkTreeIter iter;
+ gchar *title;
+ GtkEntry *entry;
+
+ g_return_if_fail (GTK_IS_ENTRY (editable));
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (user_data));
+ g_return_if_fail (path != NULL);
+
+ plugin = TOTEM_CHAPTERS_PLUGIN (user_data);
+ entry = GTK_ENTRY (editable);
+ store = gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree));
+
+ if (G_UNLIKELY (!gtk_tree_model_get_iter_from_string (store, &iter, path)))
+ return;
+
+ gtk_tree_model_get (store, &iter, CHAPTERS_TITLE_PRIV_COLUMN, &title, -1);
+ gtk_entry_set_text (entry, title);
+
+ g_free (title);
+ return;
+}
+
+static void
+finish_chapter_edit (GtkCellRendererText *renderer,
+ gchar *path,
+ gchar *new_text,
+ gpointer user_data)
+{
+ TotemChaptersPlugin *plugin;
+ GtkTreeModel *store;
+ GtkTreeIter iter;
+ gchar *time_str, *tip, *new_title, *old_title;
+ gint64 time;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (user_data));
+ g_return_if_fail (new_text != NULL);
+ g_return_if_fail (path != NULL);
+
+ plugin = TOTEM_CHAPTERS_PLUGIN (user_data);
+ store = gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree));
+
+ if (G_UNLIKELY (!gtk_tree_model_get_iter_from_string (store, &iter, path)))
+ return;
+
+ gtk_tree_model_get (store, &iter,
+ CHAPTERS_TIME_PRIV_COLUMN, &time,
+ CHAPTERS_TITLE_PRIV_COLUMN, &old_title,
+ -1);
+
+ if (g_strcmp0 (old_title, new_text) == 0) {
+ g_free (old_title);
+ return;
+ }
+
+ time_str = totem_cmml_convert_msecs_to_str (time);
+ new_title = CHAPTER_TITLE (new_text, time_str);
+ tip = CHAPTER_TOOLTIP (new_text, time_str);
+
+ gtk_tree_store_set (GTK_TREE_STORE (store), &iter,
+ CHAPTERS_TITLE_COLUMN, new_title,
+ CHAPTERS_TOOLTIP_COLUMN, tip,
+ CHAPTERS_TITLE_PRIV_COLUMN, new_text,
+ -1);
+
+ gtk_widget_set_sensitive (plugin->priv->save_button, TRUE);
+
+ g_free (old_title);
+ g_free (new_title);
+ g_free (tip);
+ g_free (time_str);
+}
+
+static void
+show_chapter_edit_dialog (TotemChaptersPlugin *plugin)
+{
+ GtkWindow *main_window;
+ BaconVideoWidget *bvw;
+ gint64 time;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ if (G_UNLIKELY (plugin->edit_chapter != NULL)) {
+ gtk_window_present (GTK_WINDOW (plugin->edit_chapter));
+ return;
+ }
+
+ main_window = totem_get_main_window (plugin->totem);
+ plugin->priv->was_played = totem_is_playing (plugin->totem);
+ totem_action_pause (plugin->totem);
+
+ /* adding a new one, check if it's time available */
+ g_object_get (G_OBJECT (plugin->totem), "current-time", &time, NULL);
+ if (G_UNLIKELY (!check_available_time (plugin, time))) {
+ totem_interface_error_blocking (_("Chapter with the same time already exists"),
+ _("Try another name or remove an existing chapter"),
+ main_window);
+ g_object_unref (main_window);
+ if (plugin->priv->was_played)
+ totem_action_play (plugin->totem);
+ return;
+ }
+ plugin->priv->last_time = time;
+
+ /* capture frame */
+ bvw = BACON_VIDEO_WIDGET (totem_get_video_widget (plugin->totem));
+ plugin->priv->last_frame = bacon_video_widget_get_current_frame (bvw);
+ g_object_add_weak_pointer (G_OBJECT (plugin->priv->last_frame), (gpointer *) &plugin->priv->last_frame);
+ g_object_unref (bvw);
+
+ /* create chapter-edit dialog */
+ plugin->edit_chapter = TOTEM_EDIT_CHAPTER (totem_edit_chapter_new ());
+ g_object_add_weak_pointer (G_OBJECT (plugin->edit_chapter), (gpointer *) &(plugin->edit_chapter));
+
+ g_signal_connect (G_OBJECT (plugin->edit_chapter), "delete-event",
+ G_CALLBACK (gtk_widget_destroy), NULL);
+ g_signal_connect (G_OBJECT (plugin->edit_chapter), "response",
+ G_CALLBACK (chapter_edit_dialog_response_cb), plugin);
+
+ gtk_window_set_transient_for (GTK_WINDOW (plugin->edit_chapter),
+ main_window);
+ gtk_widget_show (GTK_WIDGET (plugin->edit_chapter));
+
+ g_object_unref (main_window);
+}
+
+static void
+chapter_selection_changed_cb (GtkTreeSelection *tree_selection,
+ TotemChaptersPlugin *plugin)
+{
+ gint count;
+ gboolean allow_remove, allow_goto;
+
+ g_return_if_fail (GTK_IS_TREE_SELECTION (tree_selection));
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ count = gtk_tree_selection_count_selected_rows (tree_selection);
+ allow_remove = (count > 0);
+ allow_goto = (count == 1);
+
+ gtk_widget_set_sensitive (plugin->priv->remove_button, allow_remove);
+ gtk_widget_set_sensitive (plugin->priv->goto_button, allow_goto);
+}
+
+static void
+gconf_autoload_changed_cb (GConfClient *gconf,
+ guint cnx_id,
+ GConfEntry *entry,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_if_fail (GCONF_IS_CLIENT (gconf));
+ g_return_if_fail (entry != NULL);
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ if (G_LIKELY (entry->value != NULL))
+ plugin->priv->autoload = gconf_value_get_bool (entry->value);
+ else
+ plugin->priv->autoload = TRUE;
+}
+
+static void
+load_chapters_from_file (const gchar *uri,
+ gboolean from_dialog,
+ TotemChaptersPlugin *plugin)
+{
+ TotemCmmlAsyncData *data;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ if (plugin->priv->cancellable[0] != NULL) {
+ g_cancellable_cancel (plugin->priv->cancellable[0]);
+ g_object_unref (plugin->priv->cancellable[0]);
+ }
+
+ data = g_new0 (TotemCmmlAsyncData, 1);
+ /* do not forget to save this pointer in the result function */
+ data->file = g_strdup (uri);
+ data->final = totem_file_opened_result_cb;
+ data->user_data = (gpointer) plugin;
+ if (from_dialog)
+ data->from_dialog = TRUE;
+ /* cancellable object shouldn't be finalized during result func */
+ plugin->priv->cancellable[0] = g_cancellable_new ();
+ g_object_add_weak_pointer (G_OBJECT (plugin->priv->cancellable[0]),
+ (gpointer *) &(plugin->priv->cancellable[0]));
+ data->cancellable = plugin->priv->cancellable[0];
+
+ if (G_UNLIKELY (totem_cmml_read_file_async (data) < 0)) {
+ g_warning ("chapters: wrong parameters for reading CMML file, may be a bug");
+
+ set_no_data_visible (TRUE, TRUE, plugin);
+
+ g_object_unref (plugin->priv->cancellable[0]);
+ g_free (data);
+ }
+}
+
+static void
+set_no_data_visible (gboolean visible,
+ gboolean show_buttons,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ if (visible) {
+ gtk_widget_hide (plugin->priv->list_box);
+ gtk_widget_show (plugin->priv->load_box);
+ } else {
+ gtk_widget_hide (plugin->priv->load_box);
+ gtk_widget_show (plugin->priv->list_box);
+ }
+
+ gtk_widget_set_sensitive (plugin->priv->add_button, !visible);
+ gtk_widget_set_sensitive (plugin->priv->tree, !visible);
+
+ gtk_widget_set_visible (plugin->priv->load_button, show_buttons);
+ gtk_widget_set_visible (plugin->priv->continue_button, show_buttons);
+}
+
+void
+add_button_clicked_cb (GtkButton *button,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ show_chapter_edit_dialog (plugin);
+}
+
+void
+remove_button_clicked_cb (GtkButton *button,
+ TotemChaptersPlugin *plugin)
+{
+ GtkTreeSelection *selection;
+ GtkTreeIter iter;
+ GtkTreeModel *store;
+ GList *list;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ store = gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree));
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (plugin->priv->tree));
+ list = gtk_tree_selection_get_selected_rows (selection, NULL);
+
+ g_return_if_fail (g_list_length (list) != 0);
+
+ list = g_list_last (list);
+ while (list != NULL) {
+ gtk_tree_model_get_iter (GTK_TREE_MODEL (store), &iter, (GtkTreePath *) list->data);
+ gtk_tree_store_remove (GTK_TREE_STORE (store), &iter);
+
+ list = list->prev;
+ }
+
+ gtk_widget_set_sensitive (plugin->priv->save_button, TRUE);
+
+ g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL);
+ g_list_free (list);
+}
+
+static void
+save_chapters_result_cb (gpointer data,
+ gpointer user_data)
+{
+ TotemCmmlAsyncData *adata;
+ TotemChaptersPlugin *plugin;
+
+ g_return_if_fail (data != NULL);
+
+ adata = (TotemCmmlAsyncData *) data;
+ plugin = TOTEM_CHAPTERS_PLUGIN (adata->user_data);
+
+ if (G_UNLIKELY (!adata->successful && !g_cancellable_is_cancelled (adata->cancellable))) {
+ totem_action_error (plugin->totem, _("Error while writing file with chapters"),
+ adata->error);
+ gtk_widget_set_sensitive (plugin->priv->save_button, TRUE);
+ }
+
+ g_object_unref (adata->cancellable);
+ g_list_foreach (adata->list, (GFunc) totem_cmml_clip_free, NULL);
+ g_list_free (adata->list);
+ g_free (adata->error);
+ g_free (adata);
+}
+
+static GList *
+get_chapters_list (TotemChaptersPlugin *plugin)
+{
+ GList *list = NULL;
+ TotemCmmlClip *clip;
+ GtkTreeModel *store;
+ GtkTreeIter iter;
+ gchar *title;
+ gint64 time;
+ GdkPixbuf *pixbuf;
+ gboolean valid;
+
+ g_return_val_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin), NULL);
+
+ store = gtk_tree_view_get_model (GTK_TREE_VIEW (plugin->priv->tree));
+
+ valid = gtk_tree_model_get_iter_first (store, &iter);
+ while (valid) {
+ gtk_tree_model_get (store, &iter,
+ CHAPTERS_TITLE_PRIV_COLUMN, &title,
+ CHAPTERS_TIME_PRIV_COLUMN, &time,
+ CHAPTERS_PIXBUF_COLUMN, &pixbuf,
+ -1);
+ clip = totem_cmml_clip_new (title, NULL, time, pixbuf);
+ list = g_list_prepend (list, clip);
+
+ g_free (title);
+ g_object_unref (pixbuf);
+
+ valid = gtk_tree_model_iter_next (store, &iter);
+ }
+ list = g_list_reverse (list);
+
+ return list;
+}
+
+static gboolean
+show_popup_menu (TotemChaptersPlugin *plugin,
+ GdkEventButton *event)
+{
+ guint button = 0;
+ guint32 _time;
+ GtkTreePath *path;
+ gint count;
+ GtkWidget *menu;
+ GtkAction *remove_act, *goto_act;
+ GtkTreeSelection *selection;
+
+ g_return_val_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin), FALSE);
+
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (plugin->priv->tree));
+
+ if (event != NULL) {
+ button = event->button;
+ _time = event->time;
+
+ if (gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (plugin->priv->tree),
+ event->x, event->y, &path, NULL, NULL, NULL)) {
+ if (!gtk_tree_selection_path_is_selected (selection, path)) {
+ gtk_tree_selection_unselect_all (selection);
+ gtk_tree_selection_select_path (selection, path);
+ }
+ gtk_tree_path_free (path);
+ } else
+ gtk_tree_selection_unselect_all (selection);
+ } else
+ _time = gtk_get_current_event_time ();
+
+ count = gtk_tree_selection_count_selected_rows (selection);
+
+ if (count == 0)
+ return FALSE;
+
+ remove_act = gtk_action_group_get_action (plugin->priv->action_group, "remove");
+ goto_act = gtk_action_group_get_action (plugin->priv->action_group, "goto");
+ gtk_action_set_sensitive (remove_act, count > 0);
+ gtk_action_set_sensitive (goto_act, count == 1);
+
+ menu = gtk_ui_manager_get_widget (plugin->priv->ui_manager, "/totem-chapters-popup");
+
+ gtk_menu_shell_select_first (GTK_MENU_SHELL (menu), FALSE);
+
+ gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL,
+ button, _time);
+
+ return TRUE;
+}
+
+void
+save_button_clicked_cb (GtkButton *button,
+ TotemChaptersPlugin *plugin)
+{
+ TotemCmmlAsyncData *data;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ if (plugin->priv->cancellable[1] != NULL) {
+ g_cancellable_cancel (plugin->priv->cancellable[1]);
+ g_object_unref (plugin->priv->cancellable[1]);
+ }
+
+ data = g_new0 (TotemCmmlAsyncData, 1);
+ data->file = plugin->priv->cmml_mrl;
+ data->list = get_chapters_list (plugin);
+ data->final = save_chapters_result_cb;
+ data->user_data = (gpointer) plugin;
+ /* cancellable object shouldn't be finalized during result func */
+ data->cancellable = g_cancellable_new ();
+ plugin->priv->cancellable[1] = data->cancellable;
+ g_object_add_weak_pointer (G_OBJECT (plugin->priv->cancellable[1]),
+ (gpointer *) &(plugin->priv->cancellable[1]));
+
+ if (G_UNLIKELY (totem_cmml_write_file_async (data) < 0)) {
+ totem_action_error (plugin->totem, _("Error occurred while saving chapters"),
+ _("Please check you rights and free space"));
+ g_free (data);
+ g_object_unref (plugin->priv->cancellable);
+ } else
+ gtk_widget_set_sensitive (plugin->priv->save_button, FALSE);
+}
+
+void
+tree_view_row_activated_cb (GtkTreeView *tree_view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ TotemChaptersPlugin *plugin)
+{
+ GtkTreeModel *store;
+ GtkTreeIter iter;
+ gboolean seekable;
+ gint64 time;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+ g_return_if_fail (GTK_IS_TREE_VIEW (tree_view));
+ g_return_if_fail (path != NULL);
+
+ store = gtk_tree_view_get_model (tree_view);
+ seekable = totem_is_seekable (plugin->totem);
+ if (!seekable) {
+ g_warning ("chapters: unable to seek stream!");
+ return;
+ }
+
+ gtk_tree_model_get_iter (store, &iter, path);
+ gtk_tree_model_get (store, &iter, CHAPTERS_TIME_PRIV_COLUMN, &time, -1);
+
+ totem_action_seek_time (plugin->totem, time, TRUE);
+}
+
+gboolean
+tree_view_button_press_cb (GtkTreeView *tree_view,
+ GdkEventButton *event,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_val_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (event != NULL, FALSE);
+
+ if (event->type == GDK_BUTTON_PRESS && event->button == 3)
+ return show_popup_menu (plugin, event);
+
+ return FALSE;
+}
+
+gboolean
+tree_view_key_press_cb (GtkTreeView *tree_view,
+ GdkEventKey *event,
+ TotemChaptersPlugin *plugin)
+{
+ GtkTreeSelection *selection;
+
+ g_return_val_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (event != NULL, FALSE);
+
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (plugin->priv->tree));
+
+ /* Special case some shortcuts */
+ if (event->state != 0) {
+ if ((event->state & GDK_CONTROL_MASK) &&
+ event->keyval == GDK_a) {
+ gtk_tree_selection_select_all (selection);
+ return TRUE;
+ }
+ }
+
+ /* If we have modifiers, and either Ctrl, Mod1 (Alt), or any
+ * of Mod3 to Mod5 (Mod2 is num-lock...) are pressed, we
+ * let Gtk+ handle the key */
+ if (event->state != 0 &&
+ ((event->state & GDK_CONTROL_MASK)
+ || (event->state & GDK_MOD1_MASK)
+ || (event->state & GDK_MOD3_MASK)
+ || (event->state & GDK_MOD4_MASK)
+ || (event->state & GDK_MOD5_MASK)))
+ return FALSE;
+
+ if (event->keyval == GDK_Delete) {
+ if (gtk_tree_selection_count_selected_rows (selection) > 0)
+ remove_button_clicked_cb (GTK_BUTTON (plugin->priv->remove_button), plugin);
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+tree_view_popup_menu_cb (GtkTreeView *tree_view,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_val_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin), FALSE);
+
+ return show_popup_menu (plugin, NULL);
+}
+
+void
+popup_remove_action_cb (GtkAction *action,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ remove_button_clicked_cb (GTK_BUTTON (plugin->priv->remove_button), plugin);
+}
+
+void
+popup_goto_action_cb (GtkAction *action,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ goto_button_clicked_cb (GTK_BUTTON (plugin->priv->goto_button), plugin);
+}
+
+void
+load_button_clicked_cb (GtkButton *button,
+ TotemChaptersPlugin *plugin)
+{
+ GtkWindow *main_window;
+ GtkWidget *dialog;
+ GFile *cur, *parent;
+ GtkFileFilter *filter_supported, *filter_all;
+ gchar *filename, *mrl, *dir;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ plugin->priv->was_played = totem_is_playing (plugin->totem);
+ totem_action_pause (plugin->totem);
+
+ mrl = totem_get_current_mrl (plugin->totem);
+ main_window = totem_get_main_window (plugin->totem);
+ dialog = gtk_file_chooser_dialog_new (_("Open Chapters File"), main_window, GTK_FILE_CHOOSER_ACTION_OPEN,
+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (dialog), FALSE);
+
+ cur = g_file_new_for_uri (mrl);
+ parent = g_file_get_parent (cur);
+
+ if (parent != NULL)
+ dir = g_file_get_uri (parent);
+ else
+ dir = g_strdup (G_DIR_SEPARATOR_S);
+
+ filter_supported = gtk_file_filter_new ();
+ filter_all = gtk_file_filter_new ();
+
+ gtk_file_filter_add_pattern (filter_supported, "*.cmml");
+ gtk_file_filter_add_pattern (filter_supported, "*.CMML");
+ gtk_file_filter_set_name (filter_supported, _("Supported files"));
+
+ gtk_file_filter_add_pattern (filter_all, "*");
+ gtk_file_filter_set_name (filter_all, _("All files"));
+
+ gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter_supported);
+ gtk_file_chooser_add_filter(GTK_FILE_CHOOSER (dialog), filter_all);
+
+ gtk_file_chooser_set_current_folder_uri (GTK_FILE_CHOOSER (dialog), dir);
+
+ if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) {
+ filename = gtk_file_chooser_get_uri (GTK_FILE_CHOOSER (dialog));
+
+ load_chapters_from_file (filename, TRUE, plugin);
+
+ g_free (filename);
+ }
+
+ if (plugin->priv->was_played)
+ totem_action_play (plugin->totem);
+
+ gtk_widget_destroy (dialog);
+ g_object_unref (main_window);
+ g_object_unref (cur);
+ g_object_unref (parent);
+ g_free (mrl);
+ g_free (dir);
+}
+
+void
+continue_button_clicked_cb (GtkButton *button,
+ TotemChaptersPlugin *plugin)
+{
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ set_no_data_visible (FALSE, FALSE, plugin);
+}
+
+void
+goto_button_clicked_cb (GtkButton *button,
+ TotemChaptersPlugin *plugin)
+{
+ GtkTreeView *tree;
+ GtkTreeModel *store;
+ GtkTreeSelection *selection;
+ GList *list;
+
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ tree = GTK_TREE_VIEW (plugin->priv->tree);
+ store = gtk_tree_view_get_model (tree);
+ selection = gtk_tree_view_get_selection (tree);
+
+ list = gtk_tree_selection_get_selected_rows (selection, &store);
+
+ tree_view_row_activated_cb (tree, (GtkTreePath *) list->data, NULL, plugin);
+
+ g_list_foreach (list, (GFunc) gtk_tree_path_free, NULL);
+ g_list_free (list);
+}
+
+static void
+impl_activate (PeasActivatable *plugin,
+ GObject *object)
+{
+ TotemObject *totem;
+ GtkWindow *main_window;
+ GConfClient *gconf;
+ GtkBuilder *builder;
+ GtkWidget *main_box;
+ GtkTreeSelection *selection;
+ TotemChaptersPlugin *cplugin;
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+ gchar *mrl;
+
+ g_return_if_fail (TOTEM_IS_OBJECT (object));
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ cplugin = TOTEM_CHAPTERS_PLUGIN (plugin);
+ totem = TOTEM_OBJECT (object);
+ main_window = totem_get_main_window (totem);
+
+ builder = totem_interface_load ("chapters-list.ui", TRUE,
+ main_window, cplugin);
+ g_object_unref (main_window);
+
+ if (builder == NULL)
+ return;
+
+ gconf = gconf_client_get_default ();
+ if (G_LIKELY (gconf != NULL)) {
+ cplugin->priv->autoload = gconf_client_get_bool (gconf, GCONF_PREFIX"/autoload_chapters", NULL);
+ cplugin->priv->autoload_handle_id = gconf_client_notify_add (gconf, GCONF_PREFIX"/autoload_chapters",
+ (GConfClientNotifyFunc) gconf_autoload_changed_cb,
+ cplugin, NULL, NULL);
+ } else
+ cplugin->priv->autoload = TRUE;
+ cplugin->priv->gconf = gconf;
+
+ cplugin->priv->tree = GTK_WIDGET (gtk_builder_get_object (builder, "chapters_tree_view"));
+ cplugin->priv->action_group = GTK_ACTION_GROUP (gtk_builder_get_object (builder, "chapters-action-group"));
+ g_object_ref (cplugin->priv->action_group);
+ cplugin->priv->ui_manager = GTK_UI_MANAGER (gtk_builder_get_object (builder, "totem-chapters-ui-manager"));
+ g_object_ref (cplugin->priv->ui_manager);
+
+ renderer = gtk_cell_renderer_pixbuf_new ();
+ column = gtk_tree_view_column_new_with_attributes ("Pixbuf", renderer, "pixbuf", CHAPTERS_PIXBUF_COLUMN, NULL);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (cplugin->priv->tree), column);
+
+ renderer = gtk_cell_renderer_text_new ();
+
+ g_object_set (renderer, "editable", TRUE, NULL);
+ g_signal_connect (G_OBJECT (renderer), "editing-started",
+ G_CALLBACK (prepare_chapter_edit), cplugin);
+ g_signal_connect (G_OBJECT (renderer), "edited",
+ G_CALLBACK (finish_chapter_edit), cplugin);
+
+ column = gtk_tree_view_column_new_with_attributes ("Title", renderer,
+ "markup", CHAPTERS_TITLE_COLUMN, NULL);
+ gtk_tree_view_append_column (GTK_TREE_VIEW (cplugin->priv->tree), column);
+
+ cplugin->totem = g_object_ref (totem);
+ /* for read operation */
+ cplugin->priv->cancellable[0] = NULL;
+ /* for write operation */
+ cplugin->priv->cancellable[1] = NULL;
+ cplugin->edit_chapter = NULL;
+ cplugin->priv->last_frame = NULL;
+ cplugin->priv->cmml_mrl = NULL;
+ cplugin->priv->last_time = 0;
+
+ cplugin->priv->add_button = GTK_WIDGET (gtk_builder_get_object (builder, "add_button"));
+ cplugin->priv->remove_button = GTK_WIDGET (gtk_builder_get_object (builder, "remove_button"));
+ cplugin->priv->save_button = GTK_WIDGET (gtk_builder_get_object (builder, "save_button"));
+ cplugin->priv->goto_button = GTK_WIDGET (gtk_builder_get_object (builder, "goto_button"));
+ cplugin->priv->load_button = GTK_WIDGET (gtk_builder_get_object (builder, "load_button"));
+ cplugin->priv->continue_button = GTK_WIDGET (gtk_builder_get_object (builder, "continue_button"));
+
+ gtk_widget_hide (cplugin->priv->load_button);
+ gtk_widget_hide (cplugin->priv->continue_button);
+
+ cplugin->priv->list_box = GTK_WIDGET (gtk_builder_get_object (builder, "main_vbox"));
+ cplugin->priv->load_box = GTK_WIDGET (gtk_builder_get_object (builder, "load_vbox"));
+
+ main_box = gtk_vbox_new (FALSE, 6);
+ gtk_box_pack_start (GTK_BOX (main_box), cplugin->priv->list_box, TRUE, TRUE, 0);
+ gtk_box_pack_start (GTK_BOX (main_box), cplugin->priv->load_box, TRUE, TRUE, 0);
+
+ set_no_data_visible (TRUE, FALSE, cplugin);
+
+ totem_add_sidebar_page (totem, "chapters", _("Chapters"), main_box);
+
+ selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (cplugin->priv->tree));
+ gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
+
+ g_signal_connect (G_OBJECT (totem),
+ "file-opened",
+ G_CALLBACK (totem_file_opened_async_cb),
+ plugin);
+ g_signal_connect (G_OBJECT (totem),
+ "file-closed",
+ G_CALLBACK (totem_file_closed_cb),
+ plugin);
+ g_signal_connect (G_OBJECT (selection),
+ "changed",
+ G_CALLBACK (chapter_selection_changed_cb),
+ plugin);
+
+ mrl = totem_get_current_mrl (cplugin->totem);
+ if (mrl != NULL)
+ totem_file_opened_async_cb (cplugin->totem, mrl, cplugin);
+
+ g_object_unref (builder);
+ g_free (mrl);
+}
+
+static void
+impl_deactivate (PeasActivatable *plugin,
+ GObject *object)
+{
+ TotemObject *totem;
+ TotemChaptersPlugin *cplugin;
+
+ g_return_if_fail (TOTEM_IS_OBJECT (object));
+ g_return_if_fail (TOTEM_IS_CHAPTERS_PLUGIN (plugin));
+
+ totem = TOTEM_OBJECT (object);
+ cplugin = TOTEM_CHAPTERS_PLUGIN (plugin);
+
+ /* FIXME: do not cancel async operation if any */
+
+ g_signal_handlers_disconnect_by_func (G_OBJECT (totem),
+ totem_file_opened_async_cb,
+ plugin);
+ g_signal_handlers_disconnect_by_func (G_OBJECT (totem),
+ totem_file_closed_cb,
+ plugin);
+ if (cplugin->priv->gconf != NULL) {
+ gconf_client_notify_remove (cplugin->priv->gconf, cplugin->priv->autoload_handle_id);
+ g_object_unref (cplugin->priv->gconf);
+ }
+
+ if (G_UNLIKELY (cplugin->priv->last_frame != NULL))
+ g_object_unref (G_OBJECT (cplugin->priv->last_frame));
+
+ if (G_UNLIKELY (cplugin->edit_chapter != NULL))
+ gtk_widget_destroy (GTK_WIDGET (cplugin->edit_chapter));
+
+ if (G_LIKELY (cplugin->priv->action_group != NULL))
+ g_object_unref (cplugin->priv->action_group);
+
+ if (G_LIKELY (cplugin->priv->ui_manager != NULL))
+ g_object_unref (cplugin->priv->ui_manager);
+
+ if (G_LIKELY (cplugin->priv->cancellable[0] != NULL))
+ g_cancellable_cancel (cplugin->priv->cancellable[0]);
+
+ if (G_LIKELY (cplugin->priv->cancellable[1] != NULL))
+ g_cancellable_cancel (cplugin->priv->cancellable[1]);
+
+
+ g_object_unref (cplugin->totem);
+ g_free (cplugin->priv->cmml_mrl);
+
+ totem_remove_sidebar_page (totem, "chapters");
+}
diff --git a/src/plugins/chapters/totem-cmml-parser.c b/src/plugins/chapters/totem-cmml-parser.c
new file mode 100644
index 000000000..691f90c3e
--- /dev/null
+++ b/src/plugins/chapters/totem-cmml-parser.c
@@ -0,0 +1,826 @@
+/*
+ * Copyright (C) 2010 Alexander Saprykin <xelfium@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ */
+
+/**
+ * SECTION:totem-cmml-parser
+ * @short_description: parser for CMML files
+ * @stability: Unstable
+ *
+ * These functions are used to parse CMML files for chapters.
+ **/
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+#include <glib/gi18n-lib.h>
+#include <gdk-pixbuf/gdk-pixdata.h>
+#include <libxml/xmlreader.h>
+#include <libxml/xmlwriter.h>
+#include <string.h>
+#include <math.h>
+#include "totem-cmml-parser.h"
+
+#define MSECS_IN_HOUR (1000 * 60 * 60)
+#define MSECS_IN_MINUTE (1000 * 60)
+#define MSECS_IN_SECOND (1000)
+
+#define TOTEM_CMML_PREAMBLE "<!DOCTYPE cmml SYSTEM \"cmml.dtd\">\n"
+
+typedef void (*TotemCmmlCallback) (TotemCmmlClip *, gpointer user_data);
+
+typedef enum {
+ TOTEM_CMML_NONE = 0,
+ TOTEM_CMML_CMML,
+ TOTEM_CMML_HEAD,
+ TOTEM_CMML_CLIP
+} TotemCmmlStatus;
+
+typedef struct {
+ xmlTextReaderPtr reader;
+ TotemCmmlStatus status;
+ TotemCmmlClip *clip;
+ TotemCmmlCallback callback;
+ gpointer user_data;
+} TotemCmmlContext;
+
+static TotemCmmlContext * totem_cmml_context_new (void);
+static void totem_cmml_context_free (TotemCmmlContext *context);
+static void totem_cmml_context_set_callback (TotemCmmlContext *context, TotemCmmlCallback cb, gpointer user_data);
+static int totem_cmml_compare_clips (gconstpointer pointer_a, gconstpointer pointer_b);
+static TotemCmmlClip * totem_cmml_clip_new_from_attrs (const xmlChar **attrs);
+static void totem_cmml_clip_insert_img_attr (TotemCmmlClip *clip, const xmlChar **attrs);
+static gdouble totem_cmml_parse_smpte (const gchar *str, gdouble framerate);
+static gdouble totem_cmml_parse_npt (const gchar *str);
+static gdouble totem_cmml_parse_time_str (const gchar *str);
+static void totem_cmml_parse_start (TotemCmmlContext *context, const xmlChar *tag, const xmlChar **attrs);
+static void totem_cmml_parse_end (TotemCmmlContext *context, const xmlChar *tag);
+static void totem_cmml_parse_xml_node (TotemCmmlContext *context);
+static void totem_cmml_read_clip_cb (TotemCmmlClip *clip, gpointer user_data);
+
+static TotemCmmlContext *
+totem_cmml_context_new (void)
+{
+ TotemCmmlContext *ctx = g_new0 (TotemCmmlContext, 1);
+
+ ctx->status = TOTEM_CMML_NONE;
+ return ctx;
+}
+
+static void
+totem_cmml_context_free (TotemCmmlContext *context)
+{
+ g_free (context);
+}
+
+static void
+totem_cmml_context_set_callback (TotemCmmlContext *context,
+ TotemCmmlCallback cb,
+ gpointer user_data)
+{
+ g_return_if_fail (context != NULL);
+
+ context->callback = cb;
+ context->user_data = user_data;
+}
+
+static TotemCmmlClip *
+totem_cmml_clip_new_from_attrs (const xmlChar **attrs)
+{
+ gint i = 0;
+ gint64 start = -1;
+ gchar *title = NULL;
+
+ g_return_val_if_fail (attrs != NULL, NULL);
+
+ while (attrs[i] != NULL) {
+ if (g_strcmp0 ((const gchar *) attrs[i], "title") == 0)
+ title = g_strdup ((const gchar *) attrs[i + 1]);
+ else if (g_strcmp0 ((const gchar *) attrs[i], "start") == 0)
+ start = (gint64) (totem_cmml_parse_time_str ((const gchar *) attrs[i + 1]) * 1000);
+ i += 2;
+ }
+
+ return totem_cmml_clip_new (title, NULL, start, NULL);
+}
+
+static void
+totem_cmml_clip_insert_img_attr (TotemCmmlClip *clip,
+ const xmlChar **attrs)
+{
+ gint i = 0;
+ GdkPixdata *pixdata;
+ GdkPixbuf *pixbuf;
+ guchar *base64_dec;
+ guint st_len;
+ GError *error = NULL;
+
+ g_return_if_fail (clip != NULL);
+ g_return_if_fail (attrs != NULL);
+
+ while (attrs[i] != NULL) {
+ if (G_LIKELY (g_strcmp0 ((const gchar *) attrs[i], "src") == 0 &&
+ xmlStrlen (attrs[i + 1]) > 0)) {
+ pixdata = g_new0 (GdkPixdata, 1);
+ /* decode pixbuf data */
+ base64_dec = g_base64_decode ((const gchar *) attrs[i + 1], &st_len);
+ /* deserialize pixbuf data */
+ if (G_UNLIKELY (!gdk_pixdata_deserialize (pixdata, st_len, base64_dec, NULL))) {
+ g_warning ("chapters: failed to deserialize clip's pixbuf data");
+ pixbuf = NULL;
+ } else {
+ pixbuf = gdk_pixbuf_from_pixdata (pixdata, TRUE, &error);
+ if (error != NULL) {
+ g_warning ("chapters: failed to create pixbuf from pixdata: %s", error->message);
+ pixbuf = NULL;
+ g_free (error);
+ }
+ }
+ g_free (pixdata);
+ g_free (base64_dec);
+
+ if (G_LIKELY (pixbuf != NULL)) {
+ clip->pixbuf = g_object_ref (pixbuf);
+ g_object_unref (pixbuf);
+ } else
+ clip->pixbuf = pixbuf;
+ break;
+ }
+ i += 2;
+ }
+}
+
+/* the idea of parsing time was taken from libcmml (and some of code, too) */
+static gdouble
+totem_cmml_parse_smpte (const gchar *str,
+ gdouble framerate)
+{
+ gint h = 0, m = 0, s = 0;
+ gfloat frames;
+
+ if (G_UNLIKELY (str == NULL))
+ return -1.0;
+
+ /* all is according to specs */
+ if (sscanf (str, "%d:%d:%d:%f", &h, &m, &s, &frames) == 4);
+ /* this is slightly off the specs, but we can handle it */
+ else if (sscanf (str, "%d:%d:%f", &m, &s, &frames) == 3)
+ h = 0;
+ else
+ return -1.0;
+
+ /* check time and framerate bounds */
+ if (G_UNLIKELY (h < 0))
+ return -1.0;
+ if (G_UNLIKELY (m > 59 || m < 0))
+ return -1.0;
+ if (G_UNLIKELY (s > 59 || s < 0))
+ return -1.0;
+
+ if (G_UNLIKELY (frames > (gfloat) ceil (framerate) || frames < 0))
+ return -1.0;
+
+ return ((h * 3600.0) + (m * 60.0) + s) + (frames / framerate);
+}
+
+static gdouble
+totem_cmml_parse_npt (const gchar *str)
+{
+ gint h, m;
+ gfloat s;
+
+ if (G_UNLIKELY (str == NULL))
+ return -1.0;
+
+ /* all is ok */
+ if (sscanf (str, "%d:%d:%f", &h, &m, &s) == 3);
+ /* slightly off the spec */
+ else if (sscanf (str, "%d:%f", &m, &s) == 2)
+ h = 0;
+ /* time in seconds, all is ok and we can return it now */
+ else if (G_LIKELY (sscanf (str, "%f", &s)))
+ return s;
+ else
+ return -1.0;
+
+ if (G_UNLIKELY (h < 0))
+ return -1;
+ if (G_UNLIKELY (m > 59 || m < 0))
+ return -1;
+ if (G_UNLIKELY (s >= 60.0 || s < 0.0))
+ return -1;
+
+ return (h * 3600.0) + (m * 60.0) + s;
+}
+
+
+static gdouble
+totem_cmml_parse_time_str (const gchar *str)
+{
+ gchar timespec[16];
+
+ if (G_UNLIKELY (str == NULL))
+ return -1.0;
+
+ /* we need to choose parsing function to use */
+ if (sscanf (str, "npt:%16s", timespec) == 1)
+ return totem_cmml_parse_npt (str + 4);
+
+ if (sscanf (str, "smpte-24:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 9, 24.0);
+
+ if (sscanf (str, "smpte-24-drop:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 14, 23.976);
+
+ if (sscanf (str, "smpte-25:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 9, 25.0);
+
+ if (sscanf (str, "smpte-30:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 9, 30.0);
+
+ if (sscanf (str, "smpte-30-drop:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 14, 29.97);
+
+ if (sscanf (str, "smpte-50:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 9, 50.0);
+
+ if (sscanf (str, "smpte-60:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 9, 60);
+
+ if (sscanf (str, "smpte-60-drop:%16s", timespec) == 1)
+ return totem_cmml_parse_smpte (str + 14, 59.94);
+
+ /* default is npt */
+ return totem_cmml_parse_npt (str);
+}
+
+static void
+totem_cmml_parse_start (TotemCmmlContext *context,
+ const xmlChar *tag,
+ const xmlChar **attrs)
+{
+ g_return_if_fail (context != NULL);
+ g_return_if_fail (tag != NULL);
+
+ if (g_strcmp0 ((const gchar *) tag, "cmml") == 0) {
+ if (G_UNLIKELY (context->status != TOTEM_CMML_NONE))
+ return;
+
+ /* empty document */
+ if (G_UNLIKELY (xmlTextReaderIsEmptyElement (context->reader)))
+ return;
+
+ context->status = TOTEM_CMML_CMML;
+ } else if (g_strcmp0 ((const gchar *) tag, "head") == 0) {
+ if (G_UNLIKELY (context->status != TOTEM_CMML_CMML))
+ return;
+
+ /* empty head, let it ok */
+ if (G_UNLIKELY (xmlTextReaderIsEmptyElement (context->reader)))
+ context->status = TOTEM_CMML_CMML;
+
+ context->status = TOTEM_CMML_HEAD;
+ } else if (g_strcmp0 ((const gchar *) tag, "clip") == 0) {
+ if (G_UNLIKELY (context->status != TOTEM_CMML_CMML))
+ return;
+
+ context->clip = totem_cmml_clip_new_from_attrs (attrs);
+
+ /* empty clip element, we need to set status to CMML */
+ if (G_UNLIKELY (xmlTextReaderIsEmptyElement (context->reader))) {
+ if (G_LIKELY(context->callback != NULL))
+ (context->callback) (context->clip, context->user_data);
+
+ context->status = TOTEM_CMML_CMML;
+ totem_cmml_clip_free (context->clip);
+ context->clip = NULL;
+ } else
+ context->status = TOTEM_CMML_CLIP;
+ } else if (g_strcmp0 ((const gchar *) tag, "img") == 0) {
+ if (G_UNLIKELY (context->status != TOTEM_CMML_CLIP))
+ return;
+
+ totem_cmml_clip_insert_img_attr (context->clip, attrs);
+ }
+}
+
+static void
+totem_cmml_parse_end (TotemCmmlContext *context,
+ const xmlChar *tag)
+{
+ g_return_if_fail (context != NULL);
+ g_return_if_fail (tag != NULL);
+
+ if (g_strcmp0 ((const gchar *) tag, "cmml") == 0) {
+ if (G_UNLIKELY (context->status != TOTEM_CMML_CMML))
+ return;
+
+ context->status = TOTEM_CMML_NONE;
+ } else if (g_strcmp0 ((const gchar *) tag, "head") == 0) {
+ if (G_UNLIKELY (context->status != TOTEM_CMML_HEAD))
+ return;
+
+ context->status = TOTEM_CMML_CMML;
+ } else if (g_strcmp0 ((const gchar *) tag, "clip") == 0) {
+ if (G_UNLIKELY (context->status != TOTEM_CMML_CLIP))
+ return;
+
+ context->status = TOTEM_CMML_CMML;
+ if (G_LIKELY (context->callback != NULL))
+ (context->callback) (context->clip, context->user_data);
+
+ totem_cmml_clip_free (context->clip);
+ context->clip = NULL;
+ return;
+ } else if (g_strcmp0 ((const gchar *) tag, "img") == 0);
+}
+
+static void
+totem_cmml_parse_xml_node (TotemCmmlContext *context)
+{
+ xmlChar *tag;
+ xmlChar **attrs = NULL;
+ gint j, i = 0;
+
+ g_return_if_fail (context != NULL);
+ g_return_if_fail (context->reader != NULL);
+
+ tag = xmlStrdup (xmlTextReaderName (context->reader));
+
+ if (xmlTextReaderNodeType (context->reader) == XML_READER_TYPE_ELEMENT) {
+ /* read all attributes into the array [name, value, name...] */
+ if (xmlTextReaderHasAttributes (context->reader)) {
+ attrs = g_new0 (xmlChar *, xmlTextReaderAttributeCount (context->reader) * 2 + 1);
+
+ while (xmlTextReaderMoveToNextAttribute (context->reader)) {
+ attrs[i] = xmlStrdup (xmlTextReaderName (context->reader));
+ attrs[i + 1] = xmlStrdup (xmlTextReaderValue (context->reader));
+ i += 2;
+ }
+
+ attrs[i] = NULL;
+ xmlTextReaderMoveToElement (context->reader);
+ }
+
+ totem_cmml_parse_start (context, tag, (const xmlChar **) attrs);
+ /* free resources */
+ for (j = i - 1; j >= 0; j -= 1)
+ xmlFree (attrs[j]);
+ g_free (attrs);
+ } else if (xmlTextReaderNodeType (context->reader) == XML_READER_TYPE_END_ELEMENT)
+ totem_cmml_parse_end (context, tag);
+ xmlFree (tag);
+}
+
+static void
+totem_cmml_read_clip_cb (TotemCmmlClip *clip,
+ gpointer user_data)
+{
+ TotemCmmlClip *new_clip;
+
+ g_return_if_fail (clip != NULL);
+ g_return_if_fail (user_data != NULL);
+
+ new_clip = totem_cmml_clip_copy (clip);
+
+ if (G_LIKELY (new_clip != NULL && new_clip->time_start >= 0))
+ * ( (GList **) user_data) = g_list_append ( * ( (GList **) user_data), new_clip);
+ /* clip with -1 start time is bad one, remove it */
+ else
+ totem_cmml_clip_free (new_clip);
+}
+
+/**
+ * totem_cmml_convert_msecs_to_str:
+ * @time_msecs: time to convert in msecs
+ *
+ * Converts %time_msecs to string "hh:mm:ss".
+ *
+ * Returns: string in "hh:mm:ss" format.
+ **/
+gchar *
+totem_cmml_convert_msecs_to_str (gint64 time_msecs)
+{
+ gint32 hours, minutes, seconds;
+
+ if (G_UNLIKELY (time_msecs < 0))
+ hours = minutes = seconds = 0;
+ else {
+ hours = time_msecs / MSECS_IN_HOUR;
+ minutes = (time_msecs % MSECS_IN_HOUR) / MSECS_IN_MINUTE;
+ seconds = (time_msecs % MSECS_IN_MINUTE) / MSECS_IN_SECOND;
+ }
+ return g_strdup_printf ("%.2d:%.2d:%.2d", hours, minutes, seconds);
+}
+
+static int
+totem_cmml_compare_clips (gconstpointer pointer_a,
+ gconstpointer pointer_b)
+{
+ TotemCmmlClip *clip_a, *clip_b;
+
+ g_return_val_if_fail (pointer_a != NULL && pointer_b != NULL, -1);
+
+ clip_a = (TotemCmmlClip *) pointer_a;
+ clip_b = (TotemCmmlClip *) pointer_b;
+
+ return clip_a->time_start - clip_b->time_start;
+}
+
+/**
+ * totem_cmml_clip_new:
+ * @title: clip title, %NULL allowed
+ * @desc: clip description, %NULL allowed
+ * @start: clip start time in msecs
+ * @pixbuf: clip thumbnail
+ *
+ * Creates new clip structure with appropriate parameters.
+ *
+ * Returns: newly allocated #TotemCmmlClip structure.
+ **/
+TotemCmmlClip *
+totem_cmml_clip_new (const gchar *title,
+ const gchar *desc,
+ gint64 start,
+ GdkPixbuf *pixbuf)
+{
+ TotemCmmlClip *clip;
+
+ clip = g_new0 (TotemCmmlClip, 1);
+
+ clip->title = g_strdup (title);
+ clip->desc = g_strdup (desc);
+ clip->time_start = start;
+ if (G_LIKELY (pixbuf != NULL))
+ clip->pixbuf = g_object_ref (pixbuf);
+
+ return clip;
+}
+
+/**
+ * totem_cmml_clip_free:
+ * @clip: #TotemCmmlClip to free
+ *
+ * Frees unused clip structure.
+ **/
+void
+totem_cmml_clip_free (TotemCmmlClip *clip)
+{
+ if (clip == NULL)
+ return;
+
+ if (G_LIKELY (clip->pixbuf != NULL))
+ g_object_unref (clip->pixbuf);
+ g_free (clip->title);
+ g_free (clip->desc);
+ g_free (clip);
+}
+
+/**
+ * totem_cmml_clip_copy:
+ * @clip: #TotemCmmlClip structure to copy
+ *
+ * Copies #TotemCmmlClip structure.
+ *
+ * Returns: newly allocated #TotemCmmlClip if @clip != %NULL, %NULL otherwise.
+ **/
+TotemCmmlClip *
+totem_cmml_clip_copy (TotemCmmlClip *clip)
+{
+ g_return_val_if_fail (clip != NULL, NULL);
+
+ return totem_cmml_clip_new (clip->title, clip->desc, clip->time_start, clip->pixbuf);
+}
+
+static void
+totem_cmml_read_file_result (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ xmlTextReaderPtr reader;
+ TotemCmmlContext *context;
+ TotemCmmlAsyncData *data;
+ gint ret;
+ gchar *contents;
+ gsize length;
+
+
+ data = (TotemCmmlAsyncData *) user_data;
+ data->is_exists = TRUE;
+
+ g_file_load_contents_finish (G_FILE (source_object), result, &contents, &length, NULL, &error);
+ g_object_unref (source_object);
+
+ if (G_UNLIKELY (error != NULL)) {
+ g_warning ("chapters: failed to load CMML file %s: %s", data->file, error->message);
+ if (g_error_matches (error, g_io_error_quark(), G_IO_ERROR_NOT_FOUND)) {
+ /* it's ok if file doesn't exist */
+ data->successful = TRUE;
+ data->is_exists = FALSE;
+ } else {
+ data->successful = FALSE;
+ data->error = g_strdup (error->message);
+ }
+ g_error_free (error);
+ (data->final) (data, NULL);
+ return;
+ }
+
+ /* parse in-memory xml data */
+ reader = xmlReaderForMemory (contents, length, "", NULL, 0);
+ if (G_UNLIKELY (reader == NULL)) {
+ g_warning ("chapters: failed to parse CMML file %s", data->file);
+ g_free (contents);
+ data->successful = FALSE;
+ data->error = g_strdup (_("Failed to parse CMML file"));
+ (data->final) (data, NULL);
+ return;
+ }
+
+ context = totem_cmml_context_new ();
+ context->reader = reader;
+ totem_cmml_context_set_callback (context, totem_cmml_read_clip_cb, &(data->list));
+
+ ret = xmlTextReaderRead (reader);
+ while (ret == 1) {
+ totem_cmml_parse_xml_node (context);
+ ret = xmlTextReaderRead (reader);
+ }
+
+ g_free (contents);
+ xmlFreeTextReader (reader);
+ totem_cmml_clip_free (context->clip);
+ totem_cmml_context_free (context);
+
+ /* sort clips by time growth */
+ data->list = g_list_sort (data->list, (GCompareFunc) totem_cmml_compare_clips);
+ data->successful = TRUE;
+ (data->final) (data, NULL);
+}
+
+/**
+ * totem_cmml_read_file_async:
+ * @data: #TotemCmmlAsyncData structure with info needed
+ *
+ * Reads CMML file and parse it for clips in async way.
+ *
+ * Returns: 0 if no errors occurred while starting async reading, -1 otherwise.
+ **/
+gint
+totem_cmml_read_file_async (TotemCmmlAsyncData *data)
+{
+ GFile *gio_file;
+
+ g_return_val_if_fail (data != NULL, -1);
+ g_return_val_if_fail (data->file != NULL, -1);
+ g_return_val_if_fail (data->list == NULL, -1);
+ g_return_val_if_fail (data->final != NULL, -1);
+
+ gio_file = g_file_new_for_uri (data->file);
+ g_file_load_contents_async (gio_file, data->cancellable, totem_cmml_read_file_result, data);
+ return 0;
+}
+
+static void
+totem_cmml_write_file_result (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ TotemCmmlAsyncData *data;
+
+ data = (TotemCmmlAsyncData *) user_data;
+
+ g_file_replace_contents_finish (G_FILE (source_object), result, NULL, &error);
+ g_object_unref (source_object);
+
+ if (G_UNLIKELY (error != NULL)) {
+ g_warning ("chapters: failed to write CMML file %s: %s", data->file, error->message);
+ data->error = g_strdup (error->message);
+ data->successful = FALSE;
+ g_error_free (error);
+ (data->final) (data, NULL);
+ return;
+ }
+
+ g_free (data->buf);
+ data->successful = TRUE;
+ (data->final) (data, NULL);
+}
+
+/**
+ * totem_cmml_write_file_async:
+ * @data: #TotemCmmlAsyncData structure with info needed
+ *
+ * Writes CMML file with clips given.
+ *
+ * Returns: 0 if no errors occurred while starting async writing, -1 otherwise.
+ **/
+gint
+totem_cmml_write_file_async (TotemCmmlAsyncData *data)
+{
+ GFile *gio_file;
+ gint res, len;
+ GList *cur_clip;
+ xmlTextWriterPtr writer;
+ xmlBufferPtr buf;
+
+ g_return_val_if_fail (data != NULL, -1);
+ g_return_val_if_fail (data->file != NULL, -1);
+ g_return_val_if_fail (data->final != NULL, -1);
+
+ buf = xmlBufferCreate ();
+ if (G_UNLIKELY (buf == NULL)) {
+ g_warning ("chapters: failed to create xml buffer");
+ return -1;
+ }
+
+ writer = xmlNewTextWriterMemory (buf, 0);
+ if (G_UNLIKELY (writer == NULL)) {
+ g_warning ("chapters: failed to create xml buffer");
+ xmlBufferFree (buf);
+ return -1;
+ }
+
+ res = xmlTextWriterStartDocument (writer, "1.0", "UTF-8", "yes");
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ /* CMML preamble */
+ res = xmlTextWriterWriteRaw (writer, (const xmlChar *) TOTEM_CMML_PREAMBLE);
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ /* start <cmml> tag */
+ res = xmlTextWriterStartElement (writer, (const xmlChar *) "cmml");
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ res = xmlTextWriterWriteRaw (writer, (const xmlChar *) "\n");
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ /* write <head> tag */
+ res = xmlTextWriterWriteElement (writer, (const xmlChar *) "head", (const xmlChar *) "");
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ res = xmlTextWriterWriteRaw (writer, (const xmlChar *) "\n");
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ /* iterate through clip list */
+ cur_clip = data->list;
+ while (cur_clip != NULL) {
+
+ gdouble time_start;
+ gchar *base64_enc;
+ GdkPixdata *pixdata;
+ guint st_len;
+ guint8 *stream;
+ TotemCmmlClip *clip;
+
+ clip = (TotemCmmlClip *) cur_clip->data;
+ time_start = ((gdouble) clip->time_start) / 1000;
+
+ /* start <clip> tag */
+ res = xmlTextWriterStartElement (writer, (const xmlChar *) "clip");
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ res = xmlTextWriterWriteAttribute (writer, (const xmlChar *) "title", (const xmlChar *) clip->title);
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ res = xmlTextWriterWriteFormatAttribute (writer, (const xmlChar *) "start", "%.3f", time_start);
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ res = xmlTextWriterWriteRaw (writer, (const xmlChar *) "\n");
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ /* start <img> tag */
+ res = xmlTextWriterStartElement (writer, (const xmlChar *) "img");
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ if (G_LIKELY (((TotemCmmlClip *) cur_clip->data)->pixbuf != NULL)) {
+ pixdata = g_new0 (GdkPixdata, 1);
+
+ /* encode and serialize pixbuf data */
+ gdk_pixdata_from_pixbuf (pixdata, ((TotemCmmlClip *) cur_clip->data)->pixbuf, TRUE);
+ stream = gdk_pixdata_serialize (pixdata, &st_len);
+ base64_enc = g_base64_encode (stream, st_len);
+
+ g_free (pixdata->pixel_data);
+ g_free (pixdata);
+ g_free (stream);
+ }
+ else
+ base64_enc = g_strdup ("");
+
+ if (g_strcmp0 (base64_enc, "") != 0) {
+ res = xmlTextWriterWriteAttribute (writer, (const xmlChar *) "src", (const xmlChar *) base64_enc);
+ if (G_UNLIKELY (res < 0)) {
+ g_free (base64_enc);
+ break;
+ }
+ }
+ g_free (base64_enc);
+
+ /* end <img> tag */
+ res = xmlTextWriterEndElement (writer);
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ res = xmlTextWriterWriteRaw (writer, (const xmlChar *) "\n");
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ /* end <clip> tag */
+ res = xmlTextWriterEndElement (writer);
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ res = xmlTextWriterWriteRaw (writer, (const xmlChar *) "\n");
+ if (G_UNLIKELY (res < 0))
+ break;
+
+ cur_clip = cur_clip->next;
+ }
+
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ /* end <cmml> tag*/
+ res = xmlTextWriterEndElement (writer);
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ res = xmlTextWriterEndDocument (writer);
+ if (G_UNLIKELY (res < 0)) {
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+ return -1;
+ }
+
+ data->buf = g_strdup ((const char *) xmlBufferContent (buf));
+ len = xmlBufferLength (buf);
+ xmlBufferFree (buf);
+ xmlFreeTextWriter (writer);
+
+ gio_file = g_file_new_for_uri (data->file);
+ g_file_replace_contents_async (gio_file, data->buf, len, NULL, FALSE,
+ G_FILE_CREATE_NONE, data->cancellable,
+ (GAsyncReadyCallback) totem_cmml_write_file_result, data);
+
+ return 0;
+}
diff --git a/src/plugins/chapters/totem-cmml-parser.h b/src/plugins/chapters/totem-cmml-parser.h
new file mode 100644
index 000000000..649a29c69
--- /dev/null
+++ b/src/plugins/chapters/totem-cmml-parser.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 Alexander Saprykin <xelfium@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ */
+
+#ifndef TOTEM_CMML_PARSER_H_
+#define TOTEM_CMML_PARSER_H_
+
+#include <glib.h>
+#include <libxml/xmlreader.h>
+
+/**
+ * TotemCmmlClip:
+ * @title: clip title
+ * @desc: clip description
+ * @time_start: start time of clip in msecs
+ * @pixbuf: clip thumbnail
+ *
+ * Structure to handle clip data.
+ **/
+typedef struct {
+ gchar *title;
+ gchar *desc;
+ gint64 time_start;
+ GdkPixbuf *pixbuf;
+} TotemCmmlClip;
+
+/**
+ * TotemCmmlAsyncData:
+ * @file: file to read
+ * @list: list to store chapters to read in/write
+ * @final: function to call at final, %NULL is allowed
+ * @user_data: user data passed to @final callback
+ * @buf: buffer for writing
+ * @error: last error message string, or %NULL if not any
+ * @successful: whether operation was successful or not
+ * @is_exists: whether @file exists or not
+ * @from_dialog: whether read operation was started from open dialog
+ * @cancellable: object to cancel operation, %NULL is allowed
+ *
+ * Structure to handle data for async reading/writing clip data.
+ **/
+typedef struct {
+ gchar *file;
+ GList *list;
+ GFunc final;
+ gpointer user_data;
+ gchar *buf;
+ gchar *error;
+ gboolean successful;
+ gboolean is_exists;
+ gboolean from_dialog;
+ GCancellable *cancellable;
+} TotemCmmlAsyncData;
+
+gchar * totem_cmml_convert_msecs_to_str (gint64 time_msecs);
+TotemCmmlClip * totem_cmml_clip_new (const gchar *title, const gchar *desc, gint64 start, GdkPixbuf *pixbuf);
+void totem_cmml_clip_free (TotemCmmlClip *clip);
+TotemCmmlClip * totem_cmml_clip_copy (TotemCmmlClip *clip);
+gint totem_cmml_read_file_async (TotemCmmlAsyncData *data);
+gint totem_cmml_write_file_async (TotemCmmlAsyncData *data);
+
+#endif /* TOTEM_CMML_PARSER_H_ */
diff --git a/src/plugins/chapters/totem-edit-chapter.c b/src/plugins/chapters/totem-edit-chapter.c
new file mode 100644
index 000000000..aa0fd14b5
--- /dev/null
+++ b/src/plugins/chapters/totem-edit-chapter.c
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2010 Alexander Saprykin <xelfium@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ */
+
+/**
+ * SECTION:totem-edit-chapter
+ * @short_description: dialog to add new chapters in chapters plugin
+ * @stability: Unstable
+ * @include: totem-edit-chapter.h
+ *
+ * #TotemEditChapter dialog is used for entering names of new chapters in the chapters plugin
+ **/
+
+#include <gtk/gtk.h>
+
+#include "totem.h"
+#include "totem-interface.h"
+#include "totem-edit-chapter.h"
+#include <string.h>
+
+struct TotemEditChapterPrivate {
+ GtkEntry *title_entry;
+ GtkWidget *container;
+};
+
+
+#define TOTEM_EDIT_CHAPTER_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), TOTEM_TYPE_EDIT_CHAPTER, TotemEditChapterPrivate))
+
+G_DEFINE_TYPE (TotemEditChapter, totem_edit_chapter, GTK_TYPE_DIALOG)
+
+/* GtkBuilder callbacks */
+void title_entry_changed_cb (GtkEditable *entry, gpointer user_data);
+
+static void
+totem_edit_chapter_class_init (TotemEditChapterClass *klass)
+{
+ g_type_class_add_private (klass, sizeof (TotemEditChapterPrivate));
+}
+
+static void
+totem_edit_chapter_init (TotemEditChapter *self)
+{
+ GtkBuilder *builder;
+
+ self->priv = TOTEM_EDIT_CHAPTER_GET_PRIVATE (self);
+ builder = totem_interface_load ("chapters-edit.ui", FALSE, NULL, self);
+
+ if (builder == NULL) {
+ self->priv->container = NULL;
+ return;
+ }
+
+ self->priv->container = GTK_WIDGET (gtk_builder_get_object (builder, "main_vbox"));
+ g_object_ref (self->priv->container);
+ self->priv->title_entry = GTK_ENTRY (gtk_builder_get_object (builder, "title_entry"));
+
+ g_object_unref (builder);
+}
+
+GtkWidget*
+totem_edit_chapter_new (void)
+{
+ TotemEditChapter *edit_chapter;
+ GtkWidget *dialog_area;
+
+ edit_chapter = TOTEM_EDIT_CHAPTER (g_object_new (TOTEM_TYPE_EDIT_CHAPTER, NULL));
+
+ if (G_UNLIKELY (edit_chapter->priv->container == NULL)) {
+ g_object_unref (edit_chapter);
+ return NULL;
+ }
+
+ gtk_window_set_title (GTK_WINDOW (edit_chapter), "Add Chapter");
+
+ gtk_dialog_add_buttons (GTK_DIALOG (edit_chapter),
+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ GTK_STOCK_OK, GTK_RESPONSE_OK,
+ NULL);
+ gtk_dialog_set_has_separator (GTK_DIALOG (edit_chapter), FALSE);
+ gtk_container_set_border_width (GTK_CONTAINER (edit_chapter), 5);
+ gtk_dialog_set_default_response (GTK_DIALOG (edit_chapter), GTK_RESPONSE_OK);
+ gtk_dialog_set_response_sensitive (GTK_DIALOG (edit_chapter), GTK_RESPONSE_OK, FALSE);
+
+ dialog_area = gtk_dialog_get_content_area (GTK_DIALOG (edit_chapter));
+ gtk_box_pack_start (GTK_BOX (dialog_area),
+ edit_chapter->priv->container,
+ FALSE, TRUE, 0);
+
+ gtk_widget_show_all (dialog_area);
+
+ return GTK_WIDGET (edit_chapter);
+}
+
+void
+totem_edit_chapter_set_title (TotemEditChapter *edit_chapter,
+ const gchar *title)
+{
+ g_return_if_fail (TOTEM_IS_EDIT_CHAPTER (edit_chapter));
+
+ gtk_entry_set_text (edit_chapter->priv->title_entry, title);
+}
+
+gchar *
+totem_edit_chapter_get_title (TotemEditChapter *edit_chapter)
+{
+ g_return_val_if_fail (TOTEM_IS_EDIT_CHAPTER (edit_chapter), NULL);
+
+ return g_strdup (gtk_entry_get_text (edit_chapter->priv->title_entry));
+}
+
+void
+title_entry_changed_cb (GtkEditable *entry,
+ gpointer user_data)
+{
+ GtkDialog *dialog;
+ gboolean sens;
+
+ g_return_if_fail (GTK_IS_ENTRY (entry));
+ g_return_if_fail (GTK_IS_DIALOG (user_data));
+
+ dialog = GTK_DIALOG (user_data);
+ sens = (gtk_entry_get_text_length (GTK_ENTRY (entry)) > 0);
+
+ gtk_dialog_set_response_sensitive (dialog, GTK_RESPONSE_OK, sens);
+}
diff --git a/src/plugins/chapters/totem-edit-chapter.h b/src/plugins/chapters/totem-edit-chapter.h
new file mode 100644
index 000000000..8a61e9e6e
--- /dev/null
+++ b/src/plugins/chapters/totem-edit-chapter.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2010 Alexander Saprykin <xelfium@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ *
+ * The Totem project hereby grant permission for non-gpl compatible GStreamer
+ * plugins to be used and distributed together with GStreamer and Totem. This
+ * permission are above and beyond the permissions granted by the GPL license
+ * Totem is covered by.
+ */
+
+#ifndef TOTEM_EDIT_CHAPTER_H
+#define TOTEM_EDIT_CHAPTER_H
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define TOTEM_TYPE_EDIT_CHAPTER (totem_edit_chapter_get_type ())
+#define TOTEM_EDIT_CHAPTER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), TOTEM_TYPE_EDIT_CHAPTER, TotemEditChapter))
+#define TOTEM_EDIT_CHAPTER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), TOTEM_TYPE_EDIT_CHAPTER, TotemEditChapterClass))
+#define TOTEM_IS_EDIT_CHAPTER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TOTEM_TYPE_EDIT_CHAPTER))
+#define TOTEM_IS_EDIT_CHAPTER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), TOTEM_TYPE_EDIT_CHAPTER))
+
+typedef struct TotemEditChapter TotemEditChapter;
+typedef struct TotemEditChapterClass TotemEditChapterClass;
+typedef struct TotemEditChapterPrivate TotemEditChapterPrivate;
+
+struct TotemEditChapter {
+ GtkDialog parent;
+ TotemEditChapterPrivate *priv;
+};
+
+struct TotemEditChapterClass {
+ GtkDialogClass parent_class;
+};
+
+GType totem_edit_chapter_get_type (void);
+GtkWidget * totem_edit_chapter_new (void);
+void totem_edit_chapter_set_title (TotemEditChapter *edit_chapter, const gchar *title);
+gchar * totem_edit_chapter_get_title (TotemEditChapter *edit_chapter);
+
+G_END_DECLS
+
+#endif /* TOTEM_EDIT_CHAPTER_H */
diff --git a/src/totem-preferences.c b/src/totem-preferences.c
index 8dbae16ac..e0015ccf8 100644
--- a/src/totem-preferences.c
+++ b/src/totem-preferences.c
@@ -63,6 +63,7 @@ G_MODULE_EXPORT void font_set_cb (GtkFontButton * fb, Totem * totem);
G_MODULE_EXPORT void encoding_set_cb (GtkComboBox *cb, Totem *totem);
G_MODULE_EXPORT void font_changed_cb (GConfClient *client, guint cnxn_id, GConfEntry *entry, Totem *totem);
G_MODULE_EXPORT void encoding_changed_cb (GConfClient *client, guint cnxn_id, GConfEntry *entry, Totem *totem);
+G_MODULE_EXPORT void auto_chapters_toggled_cb (GtkToggleButton *togglebutton, Totem *totem);
static void
totem_action_info (char *reason, Totem *totem)
@@ -353,6 +354,23 @@ autoload_subtitles_changed_cb (GConfClient *client, guint cnxn_id,
G_CALLBACK (checkbutton3_toggled_cb), totem);
}
+static void
+autoload_chapters_changed_cb (GConfClient *client, guint cnxn_id,
+ GConfEntry *entry, Totem *totem)
+{
+ GObject *item;
+
+ item = gtk_builder_get_object (totem->xml, "tpw_auto_chapters_checkbutton");
+ g_signal_handlers_disconnect_by_func (item,
+ auto_chapters_toggled_cb, totem);
+
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (item),
+ gconf_client_get_bool (totem->gc, GCONF_PREFIX"/autoload_chapters", NULL));
+
+ g_signal_connect (item, "toggled",
+ G_CALLBACK (auto_chapters_toggled_cb), totem);
+}
+
void
connection_combobox_changed (GtkComboBox *combobox, Totem *totem)
{
@@ -523,10 +541,20 @@ encoding_changed_cb (GConfClient *client, guint cnxn_id,
}
void
+auto_chapters_toggled_cb (GtkToggleButton *togglebutton, Totem *totem)
+{
+ gboolean value;
+
+ value = gtk_toggle_button_get_active (togglebutton);
+
+ gconf_client_set_bool (totem->gc, GCONF_PREFIX"/autoload_chapters", value, NULL);
+}
+
+void
totem_setup_preferences (Totem *totem)
{
GtkWidget *menu, *content_area;
- gboolean show_visuals, auto_resize, is_local, no_deinterlace, lock_screensaver_on_audio;
+ gboolean show_visuals, auto_resize, is_local, no_deinterlace, lock_screensaver_on_audio, auto_chapters;
int connection_speed;
guint i, hidden;
char *visual, *font, *encoding;
@@ -657,6 +685,19 @@ totem_setup_preferences (Totem *totem)
(GConfClientNotifyFunc) autoload_subtitles_changed_cb,
totem, NULL, NULL);
+ /* Auto-load external chapters */
+ item = gtk_builder_get_object (totem->xml, "tpw_auto_chapters_checkbutton");
+ auto_chapters = gconf_client_get_bool (totem->gc,
+ GCONF_PREFIX"/autoload_chapters", NULL);
+
+ g_signal_handlers_disconnect_by_func (item, auto_chapters_toggled_cb, totem);
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (item), auto_chapters);
+ g_signal_connect (item, "toggled", G_CALLBACK (auto_chapters_toggled_cb), totem);
+
+ gconf_client_notify_add (totem->gc, GCONF_PREFIX"/autoload_chapters",
+ (GConfClientNotifyFunc) autoload_chapters_changed_cb,
+ totem, NULL, NULL);
+
/* Visuals list */
list = bacon_video_widget_get_visuals_list (totem->bvw);
menu = gtk_menu_new ();