diff options
author | Alexander Saprykin <xelfium@gmail.com> | 2010-07-02 00:17:08 +0400 |
---|---|---|
committer | Bastien Nocera <hadess@hadess.net> | 2010-08-09 16:16:03 +0100 |
commit | 66d1958a89affb91380777409bd863faf668e872 (patch) | |
tree | cfecf08fa483221e93b6c06776c046c1a3da080b | |
parent | 640088aa5e38e4da6f4484a96c6a0d417a1e2245 (diff) | |
download | totem-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.in | 11 | ||||
-rw-r--r-- | data/totem.schemas.in | 14 | ||||
-rw-r--r-- | data/totem.ui | 95 | ||||
-rw-r--r-- | po/POTFILES.in | 5 | ||||
-rw-r--r-- | src/plugins/chapters/Makefile.am | 47 | ||||
-rw-r--r-- | src/plugins/chapters/chapters-edit.ui | 35 | ||||
-rw-r--r-- | src/plugins/chapters/chapters-list.ui | 237 | ||||
-rw-r--r-- | src/plugins/chapters/chapters.totem-plugin.in | 9 | ||||
-rw-r--r-- | src/plugins/chapters/totem-chapters-utils.c | 95 | ||||
-rw-r--r-- | src/plugins/chapters/totem-chapters-utils.h | 33 | ||||
-rw-r--r-- | src/plugins/chapters/totem-chapters.c | 1259 | ||||
-rw-r--r-- | src/plugins/chapters/totem-cmml-parser.c | 826 | ||||
-rw-r--r-- | src/plugins/chapters/totem-cmml-parser.h | 82 | ||||
-rw-r--r-- | src/plugins/chapters/totem-edit-chapter.c | 145 | ||||
-rw-r--r-- | src/plugins/chapters/totem-edit-chapter.h | 58 | ||||
-rw-r--r-- | src/totem-preferences.c | 43 |
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">●</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 (); |