/** * @file * GTK widget, extending a \e GtkTreeView, for displaying an experiment's * structure for navigational purposes. */ /* * Copyright (C) 2012 Otto-von-Guericke-Universität Magdeburg * * 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 3 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, see . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include "cclosure-marshallers.h" #include "gtk-experiment-navigator.h" static void gtk_experiment_navigator_class_init(GtkExperimentNavigatorClass *klass); static void gtk_experiment_navigator_init(GtkExperimentNavigator *klass); static void gtk_experiment_navigator_dispose(GObject *gobject); static void gtk_experiment_navigator_finalize(GObject *gobject); static void gtk_experiment_navigator_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column); static void gtk_experiment_navigator_cursor_changed(GtkTreeView *tree_view); static void time_cell_data_cb(GtkTreeViewColumn *col, GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data); static inline void select_time(GtkExperimentNavigator *navi, gint64 selected_time); static inline void activate_section(GtkExperimentNavigator *navi, gint64 start, gint64 end); static void topic_row_callback(ExperimentReader *reader, const gchar *topic_id, gint64 start_time, gint64 end_time, gpointer data); /** * @private * Unreference object given by variable, but only once. * Use it in \ref gtk_experiment_navigator_dispose to unreference object * references in public or private instance attributes. * * @sa gtk_experiment_navigator_dispose * * @param VAR Variable to unreference */ #define GOBJECT_UNREF_SAFE(VAR) G_STMT_START { \ if ((VAR) != NULL) { \ g_object_unref(VAR); \ VAR = NULL; \ } \ } G_STMT_END /** @private */ #define GTK_EXPERIMENT_NAVIGATOR_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE((obj), GTK_TYPE_EXPERIMENT_NAVIGATOR, GtkExperimentNavigatorPrivate)) /** * @private * Private instance attribute structure. * You can access these attributes using \c klass->priv->attribute. */ struct _GtkExperimentNavigatorPrivate { gint dummy; /**< unused dummy attribute, may be deleted when other attributes are added */ /** * @todo * Add necessary \b private instance attributes. They must be * initialized in the instance initializer function. */ }; struct TopicCallbackData { GtkTreeIter iter; GtkTreeStore *store; gint64 start_time; gint64 end_time; }; /** @private */ enum { TIME_SELECTED_SIGNAL, SECTION_ACTIVATED_SIGNAL, LAST_SIGNAL }; static guint gtk_experiment_navigator_signals[LAST_SIGNAL] = {0}; /** * @private * Enumeration of tree store columns that serve as Ids when manipulating * the store. */ enum { COL_NAME, /**< Name of the section, subsection or topic (\c G_TYPE_STRING) */ COL_START_TIME, /**< Start time of the entity (\c G_TYPE_INT64 in milliseconds) */ COL_END_TIME, /**< End time of the entity (\c G_TYPE_INT64 in milliseconds) */ NUM_COLS /**< Number of columns */ /** @todo Add additional tree store columns as necessary */ }; /** * @private * Will create \e gtk_experiment_navigator_get_type and set * \e gtk_experiment_navigator_parent_class */ G_DEFINE_TYPE(GtkExperimentNavigator, gtk_experiment_navigator, GTK_TYPE_TREE_VIEW); static void gtk_experiment_navigator_class_init(GtkExperimentNavigatorClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); GtkTreeViewClass *treeview_class = GTK_TREE_VIEW_CLASS(klass); gobject_class->dispose = gtk_experiment_navigator_dispose; gobject_class->finalize = gtk_experiment_navigator_finalize; treeview_class->row_activated = gtk_experiment_navigator_row_activated; treeview_class->cursor_changed = gtk_experiment_navigator_cursor_changed; gtk_experiment_navigator_signals[TIME_SELECTED_SIGNAL] = g_signal_new("time-selected", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(GtkExperimentNavigatorClass, time_selected), NULL, NULL, gtk_experiment_widgets_marshal_VOID__INT64, G_TYPE_NONE, 1, G_TYPE_INT64); gtk_experiment_navigator_signals[SECTION_ACTIVATED_SIGNAL] = g_signal_new("section-activated", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION, G_STRUCT_OFFSET(GtkExperimentNavigatorClass, section_activated), NULL, NULL, gtk_experiment_widgets_marshal_VOID__INT64_INT64, G_TYPE_NONE, 2, G_TYPE_INT64, G_TYPE_INT64); g_type_class_add_private(klass, sizeof(GtkExperimentNavigatorPrivate)); } /** * @brief Instance initializer function for the \e GtkExperimentNavigator widget * * It has to create the \e GtkTreeStore (MVC model), add and configure * view columns and add cell renderers to the view columns. * It should connect the necessary signals to respond to row activations * (double click) in order to emit the "time-selected" signal. * It should also initialize all used \b public and \b private attributes. * * @param klass Newly constructed \e GtkExperimentNavigator instance */ static void gtk_experiment_navigator_init(GtkExperimentNavigator *klass) { GtkTreeView *view = GTK_TREE_VIEW(klass); GtkTreeViewColumn *col; GtkCellRenderer *renderer; GtkTreeStore *store; klass->priv = GTK_EXPERIMENT_NAVIGATOR_GET_PRIVATE(klass); /* * Create tree store (and model) * NOTE: GtkTreeStore is directly derived from GObject and has a * reference count of 1 after creation. */ store = gtk_tree_store_new(NUM_COLS, G_TYPE_STRING, G_TYPE_INT64, G_TYPE_INT64); /* * Create TreeView column corresponding to the * TreeStore column \e COL_NAME */ col = gtk_tree_view_column_new(); gtk_tree_view_column_set_title(col, "Name"); gtk_tree_view_column_set_expand(col, TRUE); gtk_tree_view_append_column(view, col); renderer = gtk_cell_renderer_text_new(); gtk_tree_view_column_pack_start(col, renderer, TRUE); gtk_tree_view_column_add_attribute(col, renderer, "text", COL_NAME); /** * @todo * Perhaps an icon should be rendered in front of the name to * indicate the entity's type (section, subsection, topic...) */ /* * Create TreeView column corresponding to the * TreeStore column \c COL_START_TIME */ col = gtk_tree_view_column_new(); gtk_tree_view_column_set_title(col, "Start"); gtk_tree_view_append_column(view, col); renderer = gtk_cell_renderer_text_new(); gtk_tree_view_column_pack_start(col, renderer, TRUE); /* Cell data function for custom formatting */ gtk_tree_view_column_set_cell_data_func(col, renderer, time_cell_data_cb, GINT_TO_POINTER(COL_START_TIME), NULL); /* * Create TreeView column corresponding to the * TreeStore column \c COL_END_TIME */ col = gtk_tree_view_column_new(); gtk_tree_view_column_set_title(col, "End"); gtk_tree_view_append_column(view, col); renderer = gtk_cell_renderer_text_new(); gtk_tree_view_column_pack_start(col, renderer, TRUE); /* Cell data function for custom formatting */ gtk_tree_view_column_set_cell_data_func(col, renderer, time_cell_data_cb, GINT_TO_POINTER(COL_END_TIME), NULL); /* * Set TreeView model to store's model */ gtk_tree_view_set_model(view, GTK_TREE_MODEL(store)); /* destroy store/model automatically with view */ g_object_unref(store); /** @todo better \e TreeViewColumn formatting */ /** * @todo * Initialize necessary \b public and \b private attributes. * When using object references, they must be unreferenced in * \ref gtk_experiment_navigator_dispose. * Keep in mind that when using objects derived from \e GtkObject, * they will not be reference-counted like ordinary \e GObjects (you * will not own an ordinary reference after object creation that can * be unreferenced). * In order to get an ordinary reference, use \e g_object_ref_sink * on the object after creation. */ } /** * @brief Instance disposal function * * Its purpose is to unreference \e GObjects * the instance keeps references to (object pointers saved in the * instance attributes). * Keep in mind that this function may be called more than once, so * you must guard against unreferencing an object more than once (since you * will only own a single reference). * Also keep in mind that instance methods may be invoked \b after the instance * disposal function was executed but \b before instance finalization. This * case has to be handled gracefully in the instance methods. * * @sa GOBJECT_UNREF_SAFE * @sa gtk_experiment_navigator_finalize * @sa gtk_experiment_navigator_init * * @param gobject \e GObject to dispose */ static void gtk_experiment_navigator_dispose(GObject *gobject) { GtkExperimentNavigator *navi = GTK_EXPERIMENT_NAVIGATOR(gobject); /* * destroy might be called more than once, but we have only one * reference for each object */ /** * @todo * Unreference all \e GObject references kept in public or * private attributes. Use \ref GOBJECT_UNREF_SAFE. * For example, to unreference private object \c widget: * @code * GOBJECT_UNREF_SAFE(navi->priv->widget); * @endcode */ /* Chain up to the parent class */ G_OBJECT_CLASS(gtk_experiment_navigator_parent_class)->dispose(gobject); } /** * @brief Instance finalization function * * Its purpose is to free all remaining allocated memory referenced * in public and private instance attributes (e.g. a string). * For freeing (unreferencing) objects, * use \ref gtk_experiment_navigator_dispose. * * @sa gtk_experiment_navigator_dispose * @sa gtk_experiment_navigator_init * * @param gobject \e GObject to finalize */ static void gtk_experiment_navigator_finalize(GObject *gobject) { GtkExperimentNavigator *navi = GTK_EXPERIMENT_NAVIGATOR(gobject); /** @todo Free all memory referenced in public and private attributes */ /* Chain up to the parent class */ G_OBJECT_CLASS(gtk_experiment_navigator_parent_class)->finalize(gobject); } /** * Send time-selected Signal when a row is double-clicked * * @param tree_view the object on which the signal is emitted * @param path the GtkTreePath for the activated row * @param column the GtkTreeViewColumn in which the activation occurred */ static void gtk_experiment_navigator_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column __attribute__((unused))) { GtkTreeModel *treemodel = gtk_tree_view_get_model(tree_view); gint64 start_time; GtkTreeIter treeiter; gtk_tree_model_get_iter(treemodel, &treeiter, path); gtk_tree_model_get(treemodel, &treeiter, COL_START_TIME, &start_time, -1); select_time(GTK_EXPERIMENT_NAVIGATOR(tree_view), start_time); } /** * Send cursor-changed when the cursor has changed * * @param tree_view the object on which the signal is emitted */ static void gtk_experiment_navigator_cursor_changed(GtkTreeView *tree_view) { GtkTreeModel *treemodel = gtk_tree_view_get_model(tree_view); GtkTreePath *treepath; GtkTreeIter treeiter; gint64 start_time; gint64 end_time; gtk_tree_view_get_cursor(tree_view, &treepath, NULL); gtk_tree_model_get_iter(treemodel, &treeiter, treepath); gtk_tree_path_free(treepath); gtk_tree_model_get(treemodel, &treeiter, COL_START_TIME, &start_time, COL_END_TIME, &end_time, -1); activate_section(GTK_EXPERIMENT_NAVIGATOR(tree_view), start_time, end_time); } /** * @brief Cell data function to invoke when rendering the "Start" and * "End" columns. * * @param col \e GtkTreeViewColumn to render for * @param renderer Cell renderer to use for rendering * @param model \e GtkTreeModel associated with the view * @param iter Row identifier * @param user_data Callback user data */ static void time_cell_data_cb(GtkTreeViewColumn *col __attribute__((unused)), GtkCellRenderer *renderer, GtkTreeModel *model, GtkTreeIter *iter, gpointer user_data) { gint column = GPOINTER_TO_INT(user_data); gint64 time_val; gchar buf[20]; gtk_tree_model_get(model, iter, column, &time_val, -1); g_snprintf(buf, sizeof(buf), "%" G_GINT64_FORMAT ":%02" G_GINT64_FORMAT , time_val /1000/60, time_val/1000 % 60); g_object_set(renderer, "text", buf, NULL); } /** * @brief Emit "time-selected" signal on a \e GtkExperimentNavigator instance. * * It should be emitted when a row entry was selected (double-clicked). * * @sa GtkExperimentNavigatorClass::time_selected * * @param navi Widget to emit the signal on * @param selected_time Selected time in milliseconds */ static inline void select_time(GtkExperimentNavigator *navi, gint64 selected_time) { g_signal_emit(navi, gtk_experiment_navigator_signals[TIME_SELECTED_SIGNAL], 0, selected_time); } /** * @brief Emit "section-activated" signal on a \e GtkExperimentNavigator instance. * * It should be emitted when a row entry was activated (e.g. single-clicked) * * @sa GtkExperimentNavigatorClass::section_activated * * @param navi Widget to emit the signal on * @param start Start time of section in milliseconds * @param end End time of section in milliseconds */ static inline void activate_section(GtkExperimentNavigator *navi, gint64 start, gint64 end) { g_signal_emit(navi, gtk_experiment_navigator_signals[SECTION_ACTIVATED_SIGNAL], 0, start, end); } /** * Callback function insert new row in GtkTreeStore * initialised colomb name and start time out of userdata * * @param reader \e ExperimentReader the information refers to * @param topic_id Symbolic identifier of experiment \b topic * @param start_time Beginning of first \b contribution in \e topic (milliseconds) * @param end_time End of last \b contribution in \e topic (milliseconds) * @param data Callback user data */ static void topic_row_callback(ExperimentReader *reader, const gchar *topic_id, gint64 start_time, gint64 end_time, gpointer data) { struct TopicCallbackData *tcb = (struct TopicCallbackData *) data; GtkTreeIter topic; if (tcb->start_time < 0) tcb->start_time = start_time; tcb->end_time = end_time; gtk_tree_store_append(tcb->store, &topic, &tcb->iter); gtk_tree_store_set(tcb->store, &topic, COL_NAME, topic_id, COL_START_TIME, start_time, COL_END_TIME, end_time, -1); } /* * API */ /** * @brief Construct new \e GtkExperimentNavigator widget instance. * * @return New \e GtkExperimentNavigator widget instance */ GtkWidget * gtk_experiment_navigator_new(void) { return GTK_WIDGET(g_object_new(GTK_TYPE_EXPERIMENT_NAVIGATOR, NULL)); } /** * Fills the \e GtkExperimentNavigator widget with the structure specified * in an experiment-XML file (see session.dtd). * Any existing contents should be cleared. * * @param navi Object instance to display the structure in * @param exp \e ExperimentReader instance of opened XML-file * @return \c TRUE on success, else \c FALSE */ gboolean gtk_experiment_navigator_load(GtkExperimentNavigator *navi, ExperimentReader *exp) { struct TopicCallbackData tcd; GtkTreeIter experiment_level; GtkTreeIter last_minute_level; tcd.store = GTK_TREE_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(navi))); gtk_tree_store_clear(tcd.store); /* greeting */ gtk_tree_store_append(tcd.store, &tcd.iter, NULL); tcd.start_time = -1; experiment_reader_foreach_greeting_topic(exp, topic_row_callback, &tcd); gtk_tree_store_set(tcd.store, &tcd.iter, COL_NAME, "greeting", COL_START_TIME, tcd.start_time, COL_END_TIME, tcd.end_time, -1); /* experiment */ gtk_tree_store_append(tcd.store, &experiment_level, NULL); gtk_tree_store_set(tcd.store, &experiment_level, COL_NAME, "experiment", -1); gtk_tree_store_append(tcd.store, &tcd.iter, &experiment_level); tcd.start_time = -1; experiment_reader_foreach_exp_initial_narrative_topic(exp, topic_row_callback, &tcd); gtk_tree_store_set(tcd.store, &tcd.iter, COL_NAME, "initial-narrative", COL_START_TIME, tcd.start_time < 0 ? tcd.end_time : tcd.start_time, COL_END_TIME, tcd.end_time, -1); gtk_tree_store_set(tcd.store, &experiment_level, COL_START_TIME, tcd.start_time < 0 ? tcd.end_time : tcd.start_time, -1); gtk_tree_store_append(tcd.store, &last_minute_level, &experiment_level); gtk_tree_store_set(tcd.store, &last_minute_level, COL_NAME, "last minute", -1); for (gint i = 1; i <= 6; i++) { gchar phasename[8]; g_snprintf(phasename, sizeof(phasename), "phase %d", i); gtk_tree_store_append(tcd.store, &tcd.iter, &last_minute_level); tcd.start_time = -1; experiment_reader_foreach_exp_last_minute_phase_topic(exp, i, topic_row_callback, &tcd); gtk_tree_store_set(tcd.store, &tcd.iter, COL_NAME, phasename, COL_START_TIME, tcd.start_time < 0 ? tcd.end_time : tcd.start_time, COL_END_TIME, tcd.end_time, -1); if (i == 1) { gtk_tree_store_set(tcd.store, &last_minute_level, COL_START_TIME, tcd.start_time < 0 ? tcd.end_time : tcd.start_time, -1); } } gtk_tree_store_set(tcd.store, &last_minute_level, COL_END_TIME, tcd.end_time, -1); gtk_tree_store_set(tcd.store, &experiment_level, COL_END_TIME, tcd.end_time, -1); /* farewell */ gtk_tree_store_append(tcd.store, &tcd.iter, NULL); tcd.start_time = -1; experiment_reader_foreach_farewell_topic(exp, topic_row_callback, &tcd); gtk_tree_store_set(tcd.store, &tcd.iter, COL_NAME, "farewell", COL_START_TIME, tcd.start_time, COL_END_TIME, tcd.end_time, -1); return TRUE; } /** * Fills the \e GtkExperimentNavigator widget with the structure specified * in an experiment-XML file (see session.dtd). * It accepts an XML filename and is otherwise identical to * \ref gtk_experiment_navigator_load. * * @sa gtk_experiment_navigator_load * * @param navi Object instance to display the structure in * @param exp Filename of XML-file to open and use for configuring \e navi * @return \c TRUE on success, else \c FALSE */ gboolean gtk_experiment_navigator_load_filename(GtkExperimentNavigator *navi, const gchar *exp) { gboolean returnvalue; ExperimentReader *expread = experiment_reader_new(exp); if (expread == NULL) return FALSE; returnvalue = gtk_experiment_navigator_load(navi, expread); g_object_unref(expread); return returnvalue; }