diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/experiment-reader/experiment-reader.c | 3 | ||||
-rw-r--r-- | lib/gtk-experiment-widgets/Makefile.am | 4 | ||||
-rw-r--r-- | lib/gtk-experiment-widgets/gtk-experiment-transcript.c | 468 | ||||
-rw-r--r-- | lib/gtk-experiment-widgets/gtk-experiment-transcript.h | 76 | ||||
-rw-r--r-- | lib/gtk-experiment-widgets/gtk-experiment-widgets-catalog.xml | 4 |
5 files changed, 553 insertions, 2 deletions
diff --git a/lib/experiment-reader/experiment-reader.c b/lib/experiment-reader/experiment-reader.c index 8c01904..c4484bb 100644 --- a/lib/experiment-reader/experiment-reader.c +++ b/lib/experiment-reader/experiment-reader.c @@ -305,7 +305,8 @@ experiment_reader_get_contribution_by_time(GList *contribs, for (GList *cur = contribs; cur != NULL; cur = cur->next) { ExperimentReaderContrib *contrib = (ExperimentReaderContrib *)cur->data; - if (contrib->start_time >= timept) + if (contrib->start_time >= timept || + cur->next == NULL) return cur; } diff --git a/lib/gtk-experiment-widgets/Makefile.am b/lib/gtk-experiment-widgets/Makefile.am index 5074c56..ab05c9a 100644 --- a/lib/gtk-experiment-widgets/Makefile.am +++ b/lib/gtk-experiment-widgets/Makefile.am @@ -4,7 +4,9 @@ BUILT_SOURCES = cclosure-marshallers.c cclosure-marshallers.h lib_LTLIBRARIES = libgtk-experiment-widgets.la libgtk_experiment_widgets_la_SOURCES = gtk-experiment-navigator.c \ - gtk-experiment-navigator.h + gtk-experiment-navigator.h \ + gtk-experiment-transcript.c \ + gtk-experiment-transcript.h nodist_libgtk_experiment_widgets_la_SOURCES = $(BUILT_SOURCES) libgtk_experiment_widgets_la_CFLAGS = $(AM_CFLAGS) diff --git a/lib/gtk-experiment-widgets/gtk-experiment-transcript.c b/lib/gtk-experiment-widgets/gtk-experiment-transcript.c new file mode 100644 index 0000000..e4b1a3a --- /dev/null +++ b/lib/gtk-experiment-widgets/gtk-experiment-transcript.c @@ -0,0 +1,468 @@ +/** + * @file + * GTK widget, extending a \e GtkWidget, for displaying an experiment's + * transcript. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <assert.h> +#include <inttypes.h> + +#include <glib.h> +#include <glib/gprintf.h> + +#include <gtk/gtk.h> +#include <experiment-reader.h> + +#include "cclosure-marshallers.h" +#include "gtk-experiment-transcript.h" + +static void gtk_experiment_transcript_class_init(GtkExperimentTranscriptClass *klass); +static void gtk_experiment_transcript_init(GtkExperimentTranscript *klass); + +static void gtk_experiment_transcript_realize(GtkWidget *widget); +static void gtk_experiment_transcript_size_request(GtkWidget *widget, + GtkRequisition *requisition); +static void gtk_experiment_transcript_size_allocate(GtkWidget *widget, + GtkAllocation *allocation); +static gboolean gtk_experiment_transcript_configure(GtkWidget *widget, + GdkEventConfigure *event); +static gboolean gtk_experiment_transcript_expose(GtkWidget *widget, + GdkEventExpose *event); + +static void gtk_experiment_transcript_dispose(GObject *gobject); +static void gtk_experiment_transcript_finalize(GObject *gobject); + +static void time_adj_on_value_changed(GtkAdjustment *adj, gpointer user_data); + +static void text_layer_redraw(GtkExperimentTranscript *trans); + +#define DEFAULT_WIDTH 100 +#define DEFAULT_HEIGHT 200 + +#define LAYER_TEXT_INVISIBLE 100 + +/** @todo scale should be configurable */ +#define PX_PER_SECOND 15 +#define TIME_TO_PX(TIME) ((TIME)/(1000/PX_PER_SECOND)) +#define PX_TO_TIME(PX) (((PX)*1000)/PX_PER_SECOND) + +/** + * @private + * Unreference object given by variable, but only once. + * Use it in \ref gtk_experiment_transcript_dispose to unreference object + * references in public or private instance attributes. + * + * @sa gtk_experiment_transcript_dispose + * + * @param VAR Variable to unreference + */ +#define GOBJECT_UNREF_SAFE(VAR) do { \ + if ((VAR) != NULL) { \ + g_object_unref(VAR); \ + VAR = NULL; \ + } \ +} while (0) + +/** @private */ +#define GTK_EXPERIMENT_TRANSCRIPT_GET_PRIVATE(obj) \ + (G_TYPE_INSTANCE_GET_PRIVATE((obj), GTK_TYPE_EXPERIMENT_TRANSCRIPT, GtkExperimentTranscriptPrivate)) + +/** + * @private + * Private instance attribute structure. + * You can access these attributes using \c klass->priv->attribute. + */ +struct _GtkExperimentTranscriptPrivate { + GtkObject *time_adjustment; + gulong time_adj_on_value_changed_id; + + GdkPixmap *layer_text; + PangoLayout *layer_text_layout; + + GList *contribs; +}; + +/** + * @private + * Will create \e gtk_experiment_transcript_get_type and set + * \e gtk_experiment_transcript_parent_class + */ +G_DEFINE_TYPE(GtkExperimentTranscript, gtk_experiment_transcript, GTK_TYPE_WIDGET); + +static void +gtk_experiment_transcript_class_init(GtkExperimentTranscriptClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS(klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass); + + gobject_class->dispose = gtk_experiment_transcript_dispose; + gobject_class->finalize = gtk_experiment_transcript_finalize; + + widget_class->realize = gtk_experiment_transcript_realize; + /* FIXME: configure-event handler not invoked! */ + widget_class->configure_event = gtk_experiment_transcript_configure; + widget_class->expose_event = gtk_experiment_transcript_expose; + widget_class->size_request = gtk_experiment_transcript_size_request; + widget_class->size_allocate = gtk_experiment_transcript_size_allocate; + + g_type_class_add_private(klass, sizeof(GtkExperimentTranscriptPrivate)); +} + +/** + * @brief Instance initializer function for the \e GtkExperimentTranscript widget + * + * @param klass Newly constructed \e GtkExperimentTranscript instance + */ +static void +gtk_experiment_transcript_init(GtkExperimentTranscript *klass) +{ + klass->priv = GTK_EXPERIMENT_TRANSCRIPT_GET_PRIVATE(klass); + + klass->speaker = NULL; + + klass->priv->time_adjustment = gtk_adjustment_new(0., 0., 0., + 0., 0., 0.); + g_object_ref_sink(klass->priv->time_adjustment); + klass->priv->time_adj_on_value_changed_id = + g_signal_connect(G_OBJECT(klass->priv->time_adjustment), + "value-changed", + G_CALLBACK(time_adj_on_value_changed), klass); + + klass->priv->layer_text = NULL; + klass->priv->layer_text_layout = + gtk_widget_create_pango_layout(GTK_WIDGET(klass), NULL); + pango_layout_set_wrap(klass->priv->layer_text_layout, PANGO_WRAP_WORD_CHAR); + pango_layout_set_ellipsize(klass->priv->layer_text_layout, PANGO_ELLIPSIZE_END); + + klass->priv->contribs = NULL; +} + +/** + * @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_transcript_finalize + * @sa gtk_experiment_transcript_init + * + * @param gobject \e GObject to dispose + */ +static void +gtk_experiment_transcript_dispose(GObject *gobject) +{ + GtkExperimentTranscript *trans = GTK_EXPERIMENT_TRANSCRIPT(gobject); + + /* + * destroy might be called more than once, but we have only one + * reference for each object + */ + if (trans->priv->time_adjustment != NULL) { + g_signal_handler_disconnect(G_OBJECT(trans->priv->time_adjustment), + trans->priv->time_adj_on_value_changed_id); + g_object_unref(trans->priv->time_adjustment); + trans->priv->time_adjustment = NULL; + } + GOBJECT_UNREF_SAFE(trans->priv->layer_text); + GOBJECT_UNREF_SAFE(trans->priv->layer_text_layout); + + /* Chain up to the parent class */ + G_OBJECT_CLASS(gtk_experiment_transcript_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_transcript_dispose. + * + * @sa gtk_experiment_transcript_dispose + * @sa gtk_experiment_transcript_init + * + * @param gobject \e GObject to finalize + */ +static void +gtk_experiment_transcript_finalize(GObject *gobject) +{ + GtkExperimentTranscript *trans = GTK_EXPERIMENT_TRANSCRIPT(gobject); + + g_free(trans->speaker); + experiment_reader_free_contributions(trans->priv->contribs); + + /* Chain up to the parent class */ + G_OBJECT_CLASS(gtk_experiment_transcript_parent_class)->finalize(gobject); +} + +static void +gtk_experiment_transcript_realize(GtkWidget *widget) +{ + GdkWindowAttr attributes; + gint attributes_mask; + + gtk_widget_set_realized(widget, TRUE); + + attributes.x = widget->allocation.x; + attributes.y = widget->allocation.y; + attributes.width = widget->allocation.width; + attributes.height = widget->allocation.height; + attributes.wclass = GDK_INPUT_OUTPUT; + attributes.window_type = GDK_WINDOW_CHILD; + attributes.event_mask = gtk_widget_get_events(widget) + | GDK_EXPOSURE_MASK; + attributes.visual = gtk_widget_get_visual(widget); + attributes.colormap = gtk_widget_get_colormap(widget); + + attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP; + widget->window = gdk_window_new(gtk_widget_get_parent_window(widget), + &attributes, attributes_mask); + + widget->style = gtk_style_attach(widget->style, widget->window); + + gdk_window_set_user_data(widget->window, widget); + + gtk_style_set_background(widget->style, widget->window, GTK_STATE_ACTIVE); +} + +static void +gtk_experiment_transcript_size_request(GtkWidget *widget __attribute__((unused)), + GtkRequisition *requisition) +{ + requisition->width = DEFAULT_WIDTH; + requisition->height = DEFAULT_HEIGHT; +} + +static void +gtk_experiment_transcript_size_allocate(GtkWidget *widget, + GtkAllocation *allocation) +{ + widget->allocation = *allocation; + + if (gtk_widget_get_realized(widget)) { + gdk_window_move_resize(gtk_widget_get_window(widget), + allocation->x, allocation->y, + allocation->width, allocation->height); + + /* FIXME */ + gtk_experiment_transcript_configure(widget, NULL); + } +} + +static gboolean +gtk_experiment_transcript_configure(GtkWidget *widget, + GdkEventConfigure *event __attribute__((unused))) +{ + GtkExperimentTranscript *trans = GTK_EXPERIMENT_TRANSCRIPT(widget); + + gtk_adjustment_set_page_size(GTK_ADJUSTMENT(trans->priv->time_adjustment), + (gdouble)PX_TO_TIME(widget->allocation.height)); + + GOBJECT_UNREF_SAFE(trans->priv->layer_text); + trans->priv->layer_text = gdk_pixmap_new(gtk_widget_get_window(widget), + widget->allocation.width, + widget->allocation.height + LAYER_TEXT_INVISIBLE, + -1); + pango_layout_set_width(trans->priv->layer_text_layout, + widget->allocation.width*PANGO_SCALE); + + text_layer_redraw(trans); + + return TRUE; +} + +static gboolean +gtk_experiment_transcript_expose(GtkWidget *widget, GdkEventExpose *event) +{ + GtkExperimentTranscript *trans = GTK_EXPERIMENT_TRANSCRIPT(widget); + + gdk_draw_drawable(GDK_DRAWABLE(gtk_widget_get_window(widget)), + widget->style->fg_gc[gtk_widget_get_state(widget)], + GDK_DRAWABLE(trans->priv->layer_text), + event->area.x, event->area.y, + event->area.x, event->area.y, + event->area.width, event->area.height); + + return FALSE; +} + +static void +time_adj_on_value_changed(GtkAdjustment *adj, gpointer user_data) +{ + /** + * @todo + * heuristic to improve performance in the common case of advancing + * time in small steps + */ + text_layer_redraw(GTK_EXPERIMENT_TRANSCRIPT(user_data)); +} + +static void +text_layer_redraw(GtkExperimentTranscript *trans) +{ + GtkWidget *widget = GTK_WIDGET(trans); + + gint64 current_time = 0; + gint last_contrib_y; + + gdk_draw_rectangle(GDK_DRAWABLE(trans->priv->layer_text), + widget->style->white_gc, + TRUE, + 0, 0, + widget->allocation.width, + widget->allocation.height + LAYER_TEXT_INVISIBLE); + + gtk_widget_queue_draw_area(widget, 0, 0, + widget->allocation.width, + widget->allocation.height); + + if (trans->priv->contribs == NULL) + return; + + if (trans->priv->time_adjustment != NULL) + current_time = (gint64)gtk_adjustment_get_value(GTK_ADJUSTMENT(trans->priv->time_adjustment)); + last_contrib_y = widget->allocation.height + LAYER_TEXT_INVISIBLE; + + for (GList *cur = experiment_reader_get_contribution_by_time(trans->priv->contribs, + current_time); + cur != NULL; + cur = cur->prev) { + ExperimentReaderContrib *contrib = (ExperimentReaderContrib *)cur->data; + + gint y = widget->allocation.height - + TIME_TO_PX(current_time - contrib->start_time); + int logical_height; + + if (y > widget->allocation.height + LAYER_TEXT_INVISIBLE) + continue; + + /** @todo add attributes according to regexp masks and search mask */ + /* does that reset default attributes for the widget? */ + pango_layout_set_attributes(trans->priv->layer_text_layout, NULL); + + pango_layout_set_text(trans->priv->layer_text_layout, + contrib->text, -1); + pango_layout_set_height(trans->priv->layer_text_layout, + (last_contrib_y - y)*PANGO_SCALE); + + pango_layout_get_pixel_size(trans->priv->layer_text_layout, + NULL, &logical_height); + if (y + logical_height < 0) + break; + + gdk_draw_layout(GDK_DRAWABLE(trans->priv->layer_text), + widget->style->black_gc, + 0, y, trans->priv->layer_text_layout); + last_contrib_y = y; + } +} + +/* + * API + */ + +/** + * @brief Construct new \e GtkExperimentTranscript widget instance. + * + * @param speaker Name of speaker whose contributions are displayed + * @return New \e GtkExperimentTranscript widget instance + */ +GtkWidget * +gtk_experiment_transcript_new(const gchar *speaker) +{ + GtkExperimentTranscript *trans; + + trans = GTK_EXPERIMENT_TRANSCRIPT(g_object_new(GTK_TYPE_EXPERIMENT_TRANSCRIPT, NULL)); + trans->speaker = g_strdup(speaker); + if (trans->speaker == NULL) { + gtk_widget_destroy(GTK_WIDGET(trans)); + return NULL; + } + + return GTK_WIDGET(trans); +} + +gboolean +gtk_experiment_transcript_load(GtkExperimentTranscript *trans, + ExperimentReader *exp) +{ + experiment_reader_free_contributions(trans->priv->contribs); + trans->priv->contribs = + experiment_reader_get_contributions_by_speaker(exp, trans->speaker); + + text_layer_redraw(trans); + + return trans->priv->contribs == NULL; +} + +gboolean +gtk_experiment_transcript_load_filename(GtkExperimentTranscript *trans, + const gchar *filename) +{ + gboolean res = TRUE; + ExperimentReader *exp = experiment_reader_new(filename); + + if (exp != NULL) { + res = gtk_experiment_transcript_load(trans, exp); + g_object_unref(exp); + } + + return res; +} + +GtkAdjustment * +gtk_experiment_transcript_get_time_adjustment(GtkExperimentTranscript *trans) +{ + return trans->priv->time_adjustment != NULL + ? GTK_ADJUSTMENT(trans->priv->time_adjustment) + : NULL; +} + +/** + * @brief Change time-adjustment used by \e GtkExperimentTranscript + * + * The old adjustment will be + * unreferenced (and possibly destroyed) and a reference to the new + * adjustment will be fetched. + * + * @sa gtk_experiment_transcript_get_time_adjustment + * + * @param trans \e GtkExperimentTranscript instance + * @param adj New \e GtkAdjustment to use as time-adjustment. + */ +void +gtk_experiment_transcript_set_time_adjustment(GtkExperimentTranscript *trans, + GtkAdjustment *adj) +{ + GtkWidget *widget = GTK_WIDGET(trans); + + if (trans->priv->time_adjustment == NULL) + return; + + g_signal_handler_disconnect(G_OBJECT(trans->priv->time_adjustment), + trans->priv->time_adj_on_value_changed_id); + + g_object_unref(trans->priv->time_adjustment); + trans->priv->time_adjustment = GTK_OBJECT(adj); + g_object_ref_sink(trans->priv->time_adjustment); + + gtk_adjustment_set_page_size(GTK_ADJUSTMENT(trans->priv->time_adjustment), + (gdouble)PX_TO_TIME(widget->allocation.height)); + + trans->priv->time_adj_on_value_changed_id = + g_signal_connect(G_OBJECT(trans->priv->time_adjustment), + "value-changed", + G_CALLBACK(time_adj_on_value_changed), trans); +} diff --git a/lib/gtk-experiment-widgets/gtk-experiment-transcript.h b/lib/gtk-experiment-widgets/gtk-experiment-transcript.h new file mode 100644 index 0000000..471a5ef --- /dev/null +++ b/lib/gtk-experiment-widgets/gtk-experiment-transcript.h @@ -0,0 +1,76 @@ +/** + * @file + * Header file necessary to include when using the \e GtkExperimentTranscript + * widget. + */ + +#ifndef __GTK_EXPERIMENT_TRANSCRIPT_H +#define __GTK_EXPERIMENT_TRANSCRIPT_H + +#include <glib-object.h> +#include <gtk/gtk.h> + +#include <experiment-reader.h> + +G_BEGIN_DECLS + +#define GTK_TYPE_EXPERIMENT_TRANSCRIPT \ + (gtk_experiment_transcript_get_type()) +/** + * Cast instance pointer to \e GtkExperimentTranscript + * + * @param obj Object to cast to \e GtkExperimentTranscript + * @return \e obj casted to \e GtkExperimentTranscript + */ +#define GTK_EXPERIMENT_TRANSCRIPT(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_TYPE_EXPERIMENT_TRANSCRIPT, GtkExperimentTranscript)) +#define GTK_EXPERIMENT_TRANSCRIPT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GTK_TYPE_EXPERIMENT_TRANSCRIPT, GtkExperimentTranscriptClass)) +#define GTK_IS_EXPERIMENT_TRANSCRIPT(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_TYPE_EXPERIMENT_TRANSCRIPT)) +#define GTK_IS_EXPERIMENT_TRANSCRIPT_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), GTK_TYPE_EXPERIMENT_TRANSCRIPT)) +#define GTK_EXPERIMENT_TRANSCRIPT_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS((obj), GTK_TYPE_EXPERIMENT_TRANSCRIPT, GtkExperimentTranscriptClass)) + +/** @private */ +typedef struct _GtkExperimentTranscriptPrivate GtkExperimentTranscriptPrivate; + +/** + * \e GtkExperimentTranscript instance structure + */ +typedef struct _GtkExperimentTranscript { + GtkWidget parent_instance; /**< Parent instance structure */ + + gchar *speaker; /**< Name of speaker whose contributions are displayed (\b read-only) */ + + GtkExperimentTranscriptPrivate *priv; /**< @private Pointer to \b private instance attributes */ +} GtkExperimentTranscript; + +/** + * \e GtkExperimentTranscript class structure + */ +typedef struct _GtkExperimentTranscriptClass { + GtkWidgetClass parent_class; /**< Parent class structure */ +} GtkExperimentTranscriptClass; + +/** @private */ +GType gtk_experiment_transcript_get_type(void); + +/* + * API + */ +GtkWidget *gtk_experiment_transcript_new(const gchar *speaker); + +gboolean gtk_experiment_transcript_load(GtkExperimentTranscript *trans, + ExperimentReader *exp); +gboolean gtk_experiment_transcript_load_filename(GtkExperimentTranscript *trans, + const gchar *filename); + +GtkAdjustment *gtk_experiment_transcript_get_time_adjustment(GtkExperimentTranscript *trans); +void gtk_experiment_transcript_set_time_adjustment(GtkExperimentTranscript *trans, + GtkAdjustment *adj); + +G_END_DECLS + +#endif diff --git a/lib/gtk-experiment-widgets/gtk-experiment-widgets-catalog.xml b/lib/gtk-experiment-widgets/gtk-experiment-widgets-catalog.xml index 6728972..73a1ec9 100644 --- a/lib/gtk-experiment-widgets/gtk-experiment-widgets-catalog.xml +++ b/lib/gtk-experiment-widgets/gtk-experiment-widgets-catalog.xml @@ -10,10 +10,14 @@ <glade-widget-class name="GtkExperimentNavigator" generic-name="gtk-experiment-navigator" title="Experiment Navigator"/> + <glade-widget-class name="GtkExperimentTranscript" + generic-name="gtk-experiment-transcript" + title="Experiment Transcript"/> </glade-widget-classes> <glade-widget-group name="gtk-experiment-widgets" title="Experiment Widgets"> <glade-widget-class-ref name="GtkExperimentNavigator"/> + <glade-widget-class-ref name="GtkExperimentTranscript"/> </glade-widget-group> </glade-catalog> |