/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 2001-2002 CodeFactory AB * Copyright (C) 2001-2002 Richard Hult * Copyright (C) 2001-2002 Mikael Hallendal * * 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., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #include #include #include #include #include #include #include #include "planner-marshal.h" #include "planner-gantt-header.h" #include "planner-gantt-model.h" #include "planner-scale-utils.h" struct _PlannerGanttHeaderPriv { GdkWindow *bin_window; GtkAdjustment *hadjustment; PangoLayout *layout; MrpTimeUnit major_unit; PlannerScaleFormat major_format; MrpTimeUnit minor_unit; PlannerScaleFormat minor_format; gdouble hscale; gint width; gint height; gdouble x1; gdouble x2; gchar *date_hint; }; /* Properties */ enum { PROP_0, PROP_HEIGHT, PROP_X1, PROP_X2, PROP_SCALE, PROP_ZOOM }; enum { DATE_HINT_CHANGED, LAST_SIGNAL }; static void gantt_header_class_init (PlannerGanttHeaderClass *klass); static void gantt_header_init (PlannerGanttHeader *header); static void gantt_header_finalize (GObject *object); static void gantt_header_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec); static void gantt_header_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec); static void gantt_header_destroy (GtkObject *object); static void gantt_header_map (GtkWidget *widget); static void gantt_header_realize (GtkWidget *widget); static void gantt_header_unrealize (GtkWidget *widget); static void gantt_header_size_allocate (GtkWidget *widget, GtkAllocation *allocation); static gboolean gantt_header_expose_event (GtkWidget *widget, GdkEventExpose *event); static gboolean gantt_header_motion_notify_event (GtkWidget *widget, GdkEventMotion *event); static gboolean gantt_header_leave_notify_event (GtkWidget *widget, GdkEventCrossing *event); static void gantt_header_set_adjustments (PlannerGanttHeader *header, GtkAdjustment *hadj, GtkAdjustment *vadj); static void gantt_header_adjustment_changed (GtkAdjustment *adjustment, PlannerGanttHeader *header); static GtkWidgetClass *parent_class = NULL; static guint signals[LAST_SIGNAL]; GtkType planner_gantt_header_get_type (void) { static GtkType planner_gantt_header_type = 0; if (!planner_gantt_header_type) { static const GTypeInfo planner_gantt_header_info = { sizeof (PlannerGanttHeaderClass), NULL, /* base_init */ NULL, /* base_finalize */ (GClassInitFunc) gantt_header_class_init, NULL, /* class_finalize */ NULL, /* class_data */ sizeof (PlannerGanttHeader), 0, /* n_preallocs */ (GInstanceInitFunc) gantt_header_init }; planner_gantt_header_type = g_type_register_static ( GTK_TYPE_WIDGET, "PlannerGanttHeader", &planner_gantt_header_info, 0); } return planner_gantt_header_type; } static void gantt_header_class_init (PlannerGanttHeaderClass *class) { GObjectClass *o_class; GtkObjectClass *object_class; GtkWidgetClass *widget_class; GtkContainerClass *container_class; parent_class = g_type_class_peek_parent (class); o_class = (GObjectClass *) class; object_class = (GtkObjectClass *) class; widget_class = (GtkWidgetClass *) class; container_class = (GtkContainerClass *) class; /* GObject methods. */ o_class->set_property = gantt_header_set_property; o_class->get_property = gantt_header_get_property; o_class->finalize = gantt_header_finalize; /* GtkObject methods. */ object_class->destroy = gantt_header_destroy; /* GtkWidget methods. */ widget_class->map = gantt_header_map;; widget_class->realize = gantt_header_realize; widget_class->unrealize = gantt_header_unrealize; widget_class->size_allocate = gantt_header_size_allocate; widget_class->expose_event = gantt_header_expose_event; widget_class->leave_notify_event = gantt_header_leave_notify_event; widget_class->motion_notify_event = gantt_header_motion_notify_event; class->set_scroll_adjustments = gantt_header_set_adjustments; widget_class->set_scroll_adjustments_signal = g_signal_new ("set_scroll_adjustments", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (PlannerGanttHeaderClass, set_scroll_adjustments), NULL, NULL, planner_marshal_VOID__OBJECT_OBJECT, G_TYPE_NONE, 2, GTK_TYPE_ADJUSTMENT, GTK_TYPE_ADJUSTMENT); /* Properties. */ g_object_class_install_property ( o_class, PROP_HEIGHT, g_param_spec_int ("height", NULL, NULL, 0, G_MAXINT, 0, G_PARAM_READWRITE)); g_object_class_install_property ( o_class, PROP_X1, g_param_spec_double ("x1", NULL, NULL, -1, G_MAXDOUBLE, -1, G_PARAM_READWRITE)); g_object_class_install_property ( o_class, PROP_X2, g_param_spec_double ("x2", NULL, NULL, -1, G_MAXDOUBLE, -1, G_PARAM_READWRITE)); g_object_class_install_property ( o_class, PROP_SCALE, g_param_spec_double ("scale", NULL, NULL, 0.000001, G_MAXDOUBLE, 1.0, G_PARAM_WRITABLE)); g_object_class_install_property ( o_class, PROP_ZOOM, g_param_spec_double ("zoom", NULL, NULL, -G_MAXDOUBLE, G_MAXDOUBLE, 7, G_PARAM_WRITABLE)); signals[DATE_HINT_CHANGED] = g_signal_new ("date-hint-changed", G_TYPE_FROM_CLASS (class), G_SIGNAL_RUN_LAST, 0, NULL, NULL, planner_marshal_VOID__STRING, G_TYPE_NONE, 1, G_TYPE_STRING); } static void gantt_header_init (PlannerGanttHeader *header) { PlannerGanttHeaderPriv *priv; gtk_widget_set_redraw_on_allocate (GTK_WIDGET (header), FALSE); priv = g_new0 (PlannerGanttHeaderPriv, 1); header->priv = priv; gantt_header_set_adjustments (header, NULL, NULL); priv->hscale = 1.0; priv->x1 = 0; priv->x2 = 0; priv->height = -1; priv->width = -1; priv->major_unit = MRP_TIME_UNIT_MONTH; priv->minor_unit = MRP_TIME_UNIT_WEEK; priv->layout = gtk_widget_create_pango_layout (GTK_WIDGET (header), NULL); } static void gantt_header_set_zoom (PlannerGanttHeader *header, gdouble zoom) { PlannerGanttHeaderPriv *priv; gint level; priv = header->priv; level = planner_scale_clamp_zoom (zoom); priv->major_unit = planner_scale_conf[level].major_unit; priv->major_format = planner_scale_conf[level].major_format; priv->minor_unit = planner_scale_conf[level].minor_unit; priv->minor_format = planner_scale_conf[level].minor_format; } static void gantt_header_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { PlannerGanttHeader *header; PlannerGanttHeaderPriv *priv; gdouble tmp; gint width; gdouble tmp_scale; gboolean change_width = FALSE; gboolean change_height = FALSE; gboolean change_scale = FALSE; header = PLANNER_GANTT_HEADER (object); priv = header->priv; switch (prop_id) { case PROP_HEIGHT: priv->height = g_value_get_int (value); change_height = TRUE; break; case PROP_X1: tmp = g_value_get_double (value); if (tmp != priv->x1) { priv->x1 = tmp; change_width = TRUE; } break; case PROP_X2: tmp = g_value_get_double (value); if (tmp != priv->x2) { priv->x2 = tmp; change_width = TRUE; } break; case PROP_SCALE: tmp_scale = g_value_get_double (value); if (tmp_scale != priv->hscale) { priv->hscale = tmp_scale; change_scale = TRUE; } break; case PROP_ZOOM: gantt_header_set_zoom (header, g_value_get_double (value)); break; default: break; } if (change_width) { if (priv->x1 > 0 && priv->x2 > 0) { width = floor (priv->x2 - priv->x1 + 0.5); /* If both widths aren't set yet, this can happen: */ if (width < -1) { width = -1; } } else { width = -1; } priv->width = width; } if (change_width || change_height) { gtk_widget_set_size_request (GTK_WIDGET (header), priv->width, priv->height); } if ((change_width || change_height || change_scale) && GTK_WIDGET_REALIZED (header)) { gdk_window_invalidate_rect (priv->bin_window, NULL, FALSE); } } static void gantt_header_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { PlannerGanttHeader *header; header = PLANNER_GANTT_HEADER (object); switch (prop_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gantt_header_finalize (GObject *object) { PlannerGanttHeader *header = PLANNER_GANTT_HEADER (object); g_object_unref (header->priv->layout); g_free (header->priv->date_hint); g_free (header->priv); if (G_OBJECT_CLASS (parent_class)->finalize) { (* G_OBJECT_CLASS (parent_class)->finalize) (object); } } static void gantt_header_destroy (GtkObject *object) { /*PlannerGanttHeader *header = PLANNER_GANTT_HEADER (object);*/ if (GTK_OBJECT_CLASS (parent_class)->destroy) { (* GTK_OBJECT_CLASS (parent_class)->destroy) (object); } } static void gantt_header_map (GtkWidget *widget) { PlannerGanttHeader *header; header = PLANNER_GANTT_HEADER (widget); GTK_WIDGET_SET_FLAGS (widget, GTK_MAPPED); gdk_window_show (header->priv->bin_window); gdk_window_show (widget->window); } static void gantt_header_realize (GtkWidget *widget) { PlannerGanttHeader *header; GdkWindowAttr attributes; GdkGCValues values; gint attributes_mask; header = PLANNER_GANTT_HEADER (widget); GTK_WIDGET_SET_FLAGS (widget, GTK_REALIZED); /* Create the main, clipping window. */ attributes.window_type = GDK_WINDOW_CHILD; 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.visual = gtk_widget_get_visual (widget); attributes.colormap = gtk_widget_get_colormap (widget); attributes.event_mask = GDK_VISIBILITY_NOTIFY_MASK; 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); gdk_window_set_user_data (widget->window, widget); /* Bin window. */ attributes.x = 0; attributes.y = header->priv->height; attributes.width = header->priv->width; attributes.height = widget->allocation.height; attributes.event_mask = GDK_EXPOSURE_MASK | GDK_SCROLL_MASK | GDK_POINTER_MOTION_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | gtk_widget_get_events (widget); header->priv->bin_window = gdk_window_new (widget->window, &attributes, attributes_mask); gdk_window_set_user_data (header->priv->bin_window, widget); values.foreground = (widget->style->white.pixel == 0 ? widget->style->black : widget->style->white); values.function = GDK_XOR; values.subwindow_mode = GDK_INCLUDE_INFERIORS; widget->style = gtk_style_attach (widget->style, widget->window); gdk_window_set_background (widget->window, &widget->style->base[widget->state]); gdk_window_set_background (header->priv->bin_window, &widget->style->base[widget->state]); } static void gantt_header_unrealize (GtkWidget *widget) { PlannerGanttHeader *header; header = PLANNER_GANTT_HEADER (widget); gdk_window_set_user_data (header->priv->bin_window, NULL); gdk_window_destroy (header->priv->bin_window); header->priv->bin_window = NULL; if (GTK_WIDGET_CLASS (parent_class)->unrealize) { (* GTK_WIDGET_CLASS (parent_class)->unrealize) (widget); } } static void gantt_header_size_allocate (GtkWidget *widget, GtkAllocation *allocation) { PlannerGanttHeader *header; header = PLANNER_GANTT_HEADER (widget); if (GTK_WIDGET_REALIZED (widget)) { gdk_window_move_resize (widget->window, allocation->x, allocation->y, allocation->width, allocation->height); gdk_window_move_resize (header->priv->bin_window, - (gint) header->priv->hadjustment->value, 0, MAX (header->priv->width, allocation->width), allocation->height); } } static gboolean gantt_header_expose_event (GtkWidget *widget, GdkEventExpose *event) { PlannerGanttHeader *header; PlannerGanttHeaderPriv *priv; gint width, height; gdouble hscale; gint x; mrptime t0; mrptime t1; mrptime t; gchar *str; gint minor_width; gint major_width; GdkGC *gc; GdkRectangle rect; header = PLANNER_GANTT_HEADER (widget); priv = header->priv; hscale = priv->hscale; t0 = floor ((priv->x1 + event->area.x) / hscale + 0.5); t1 = floor ((priv->x1 + event->area.x + event->area.width) / hscale + 0.5); gdk_drawable_get_size (event->window, &width, &height); /* Draw background. We only draw over the exposed area, padding with +/- * 5 so we don't mess up the header with button edges all over. */ gtk_paint_box (widget->style, event->window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, &event->area, widget, "button", event->area.x - 5, 0, event->area.width + 10, height); gdk_draw_line (event->window, widget->style->fg_gc[GTK_STATE_INSENSITIVE], event->area.x, height / 2, event->area.x + event->area.width, height / 2); /* Get the widths of major/minor ticks so that we know how wide to make * the clip region. */ major_width = hscale * (mrp_time_align_next (t0, priv->major_unit) - mrp_time_align_prev (t0, priv->major_unit)); minor_width = hscale * (mrp_time_align_next (t0, priv->minor_unit) - mrp_time_align_prev (t0, priv->minor_unit)); gc = gdk_gc_new (widget->window); gdk_gc_copy (gc, widget->style->text_gc[GTK_STATE_NORMAL]); rect.y = 0; rect.height = height; /* Draw the major scale. */ if (major_width < 2 || priv->major_unit == MRP_TIME_UNIT_NONE) { /* Unless it's too thin to make sense. */ goto minor_ticks; } t = mrp_time_align_prev (t0, priv->major_unit); while (t <= t1) { x = floor (t * hscale - priv->x1 + 0.5); gdk_draw_line (event->window, widget->style->fg_gc[GTK_STATE_INSENSITIVE], x, 0, x, height / 2); str = planner_scale_format_time (t, priv->major_unit, priv->major_format); pango_layout_set_text (priv->layout, str, -1); g_free (str); rect.x = x; rect.width = major_width; gdk_gc_set_clip_rectangle (gc, &rect); gdk_draw_layout (event->window, gc, x + 3, 2, priv->layout); t = mrp_time_align_next (t, priv->major_unit); } minor_ticks: /* Draw the minor scale. */ if (minor_width < 2 || priv->major_unit == MRP_TIME_UNIT_NONE) { /* Unless it's too thin to make sense. */ goto done; } t = mrp_time_align_prev (t0, priv->minor_unit); while (t <= t1) { x = floor (t * hscale - priv->x1 + 0.5); gdk_draw_line (event->window, widget->style->fg_gc[GTK_STATE_INSENSITIVE], x, height / 2, x, height); str = planner_scale_format_time (t, priv->minor_unit, priv->minor_format); pango_layout_set_text (priv->layout, str, -1); g_free (str); rect.x = x; rect.width = minor_width; gdk_gc_set_clip_rectangle (gc, &rect); gdk_draw_layout (event->window, gc, x + 3, height / 2 + 2, priv->layout); t = mrp_time_align_next (t, priv->minor_unit); } done: gdk_gc_unref (gc); return TRUE; } static gboolean gantt_header_motion_notify_event (GtkWidget *widget, GdkEventMotion *event) { PlannerGanttHeader *header; PlannerGanttHeaderPriv *priv; mrptime t; char *str; header = PLANNER_GANTT_HEADER (widget); priv = header->priv; t = floor ((priv->x1 + event->x) / priv->hscale + 0.5); str = mrp_time_format (_("%e %b %Y"), t); if (!priv->date_hint || strcmp (str, priv->date_hint) != 0) { g_signal_emit (widget, signals[DATE_HINT_CHANGED], 0, str); g_free (priv->date_hint); priv->date_hint = str; } else { g_free (str); } return FALSE; } static gboolean gantt_header_leave_notify_event (GtkWidget *widget, GdkEventCrossing *event) { PlannerGanttHeader *header; PlannerGanttHeaderPriv *priv; header = PLANNER_GANTT_HEADER (widget); priv = header->priv; if (priv->date_hint) { g_signal_emit (widget, signals[DATE_HINT_CHANGED], 0, NULL); g_free (priv->date_hint); priv->date_hint = NULL; } return FALSE; } /* Callbacks */ static void gantt_header_set_adjustments (PlannerGanttHeader *header, GtkAdjustment *hadj, GtkAdjustment *vadj) { if (hadj == NULL) { hadj = GTK_ADJUSTMENT (gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)); } if (header->priv->hadjustment && (header->priv->hadjustment != hadj)) { gtk_object_unref (GTK_OBJECT (header->priv->hadjustment)); } if (header->priv->hadjustment != hadj) { header->priv->hadjustment = hadj; gtk_object_ref (GTK_OBJECT (header->priv->hadjustment)); gtk_object_sink (GTK_OBJECT (header->priv->hadjustment)); g_signal_connect (hadj, "value_changed", G_CALLBACK (gantt_header_adjustment_changed), header); gtk_widget_set_scroll_adjustments (GTK_WIDGET (header), hadj, NULL); } } static void gantt_header_adjustment_changed (GtkAdjustment *adjustment, PlannerGanttHeader *header) { if (GTK_WIDGET_REALIZED (header)) { gdk_window_move (header->priv->bin_window, -header->priv->hadjustment->value, 0); } } GtkWidget * planner_gantt_header_new (void) { return g_object_new (PLANNER_TYPE_GANTT_HEADER, NULL); }