diff options
-rw-r--r-- | ChangeLog | 42 | ||||
-rw-r--r-- | components/news/.cvsignore | 7 | ||||
-rw-r--r-- | components/news/Makefile.am | 50 | ||||
-rw-r--r-- | components/news/Nautilus_View_news.oaf.in | 25 | ||||
-rw-r--r-- | components/news/Nautilus_View_news.server.in | 25 | ||||
-rw-r--r-- | components/news/nautilus-news.c | 1697 | ||||
-rw-r--r-- | components/news/news_bullet.png | bin | 0 -> 350 bytes | |||
-rw-r--r-- | components/news/news_channels.xml | 25 | ||||
-rw-r--r-- | components/news/pixmaps.h | 55 | ||||
-rw-r--r-- | configure.in | 1 |
10 files changed, 1927 insertions, 0 deletions
@@ -1,3 +1,45 @@ +2001-04-20 Andy Hertzfeld <andy@eazel.com> + + first check-in of "news" sidebar view to display news from selected + sites that support an rss feed. It's around 80% completed now, and + should be quite usable; I just need to finish the remaining 80%. + + * components/news/.cvsignore: + * components/news/Makefile.am: + * components/news/Nautilus_View_news.oaf.in: + + * components/news/nautilus-news.c: (get_bonobo_properties), + (set_bonobo_properties), (do_destroy), (pixbuf_composite), + (draw_triangle), (draw_rss_logo_image), (draw_rss_title), + (draw_rss_items), (nautilus_news_draw_channel), + (nautilus_news_update_display), (nautilus_news_configure_event), + (nautilus_news_expose_event), (nautilus_news_set_prelight_index), + (go_to_uri), (toggle_open_state), (item_hit_test), + (nautilus_news_button_release_event), + (nautilus_news_motion_notify_event), + (nautilus_news_leave_notify_event), (nautilus_news_set_title), + (free_rss_data_item), (free_rss_channel_items), (free_channel), + (nautilus_news_free_channel_list), (bool_to_text), + (nautilus_news_make_channel_document), + (nautilus_news_save_channel_state), (rss_logo_callback), + (extract_items), (update_size_and_redraw), + (rss_read_done_callback), (nautilus_news_load_channel), + (nautilus_news_make_new_channel), (nautilus_news_add_channels), + (get_xml_path), (read_channel_list), (check_for_updates), + (news_get_indicator_image), (load_xpm_image), + (nautilus_news_load_images), (configure_button_clicked), + (add_site_button_clicked), (add_site_from_fields), + (add_command_buttons), (get_channel_from_name), + (check_button_toggled_callback), (nautilus_news_load_location), + (add_channel_entry), (add_channels_to_configure_list), + (set_up_add_widgets), (set_up_configure_widgets), + (set_up_main_widgets), (make_news_view), (main): + + * components/news/news_bullet.png: + * components/news/news_channels.xml: + * components/news/pixmaps.h: + * configure.in: + 2001-04-20 Ramiro Estrugo <ramiro@eazel.com> * ChangeLog: rolled over to ChangeLog-20010420. diff --git a/components/news/.cvsignore b/components/news/.cvsignore new file mode 100644 index 000000000..a5f6024de --- /dev/null +++ b/components/news/.cvsignore @@ -0,0 +1,7 @@ +.deps +.libs +Makefile +Makefile.in +nautilus-news +nautilus-news.o +Nautilus_View_news.oaf diff --git a/components/news/Makefile.am b/components/news/Makefile.am new file mode 100644 index 000000000..6b98db781 --- /dev/null +++ b/components/news/Makefile.am @@ -0,0 +1,50 @@ +NULL = + +bin_PROGRAMS=nautilus-news + +INCLUDES=\ + -I$(top_srcdir) \ + -I$(top_builddir) \ + -I$(top_builddir)/libnautilus \ + -DNAUTILUS_DATADIR=\""$(datadir)/nautilus"\" \ + -DNAUTILUS_PIXMAPDIR=\""$(datadir)/pixmaps/nautilus"\" \ + -DDATADIR=\""$(datadir)"\" \ + -DGNOMELOCALEDIR=\""$(datadir)/locale"\" \ + $(EEL_INCLUDEDIR) \ + $(LIBRSVG_INCLUDEDIR) \ + $(GNOMEUI_CFLAGS) \ + $(GCONF_CFLAGS) \ + $(BONOBO_CFLAGS) \ + $(VFS_CFLAGS) + +LDADD=\ + $(top_builddir)/libnautilus/libnautilus.la \ + $(top_builddir)/libnautilus-extensions/libnautilus-extensions.la \ + $(EEL_LIBS) \ + $(LIBRSVG_LIBS) \ + $(BONOBO_LIBS) \ + $(GCONF_LIBS) \ + $(GNOMEUI_LIBS) + +nautilus_news_SOURCES=nautilus-news.c + +nautilusdir = $(datadir)/nautilus +nautilus_DATA = news_channels.xml + +nautiluspixmapdir = $(datadir)/pixmaps/nautilus +nautiluspixmap_DATA = news_bullet.png + +oafdir = $(datadir)/oaf +oaf_in_files = \ + Nautilus_View_news.oaf.in \ + $(NULL) +oaf_DATA = $(oaf_in_files:.oaf.in=.oaf) + +@XML_I18N_MERGE_OAF_RULE@ + +EXTRA_DIST= \ + $(nautilus_DATA) \ + $(nautiluspixmap_DATA) \ + $(oaf_DATA)\ + $(oaf_in_files) \ + $(NULL) diff --git a/components/news/Nautilus_View_news.oaf.in b/components/news/Nautilus_View_news.oaf.in new file mode 100644 index 000000000..330904326 --- /dev/null +++ b/components/news/Nautilus_View_news.oaf.in @@ -0,0 +1,25 @@ +<oaf_info> + +<oaf_server iid="OAFIID:nautilus_news_view_factory:041601" type="exe" location="nautilus-news"> + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:GNOME/ObjectFactory:1.0"/> + </oaf_attribute> + <oaf_attribute name="description" type="string" _value="Factory for news view"/> +</oaf_server> + +<oaf_server iid="OAFIID:nautilus_news_view:041601" type="factory" location="OAFIID:nautilus_news_view_factory:041601"> + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:Bonobo/Unknown:1.0"/> + <item value="IDL:Bonobo/Control:1.0"/> + <item value="IDL:Nautilus/View:1.0"/> + </oaf_attribute> + + <oaf_attribute name="description" type="string" _value="News sidebar panel fetches and displays RSS feeds"/> + <oaf_attribute name="name" type="string" _value="News sidebar panel"/> + <oaf_attribute name="nautilus:sidebar_panel_name" type="string" _value="News"/> + <oaf_attribute name="nautilus:recommended_uri_schemes" type="stringv"> + <item value="*"/> + </oaf_attribute> +</oaf_server> + +</oaf_info> diff --git a/components/news/Nautilus_View_news.server.in b/components/news/Nautilus_View_news.server.in new file mode 100644 index 000000000..330904326 --- /dev/null +++ b/components/news/Nautilus_View_news.server.in @@ -0,0 +1,25 @@ +<oaf_info> + +<oaf_server iid="OAFIID:nautilus_news_view_factory:041601" type="exe" location="nautilus-news"> + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:GNOME/ObjectFactory:1.0"/> + </oaf_attribute> + <oaf_attribute name="description" type="string" _value="Factory for news view"/> +</oaf_server> + +<oaf_server iid="OAFIID:nautilus_news_view:041601" type="factory" location="OAFIID:nautilus_news_view_factory:041601"> + <oaf_attribute name="repo_ids" type="stringv"> + <item value="IDL:Bonobo/Unknown:1.0"/> + <item value="IDL:Bonobo/Control:1.0"/> + <item value="IDL:Nautilus/View:1.0"/> + </oaf_attribute> + + <oaf_attribute name="description" type="string" _value="News sidebar panel fetches and displays RSS feeds"/> + <oaf_attribute name="name" type="string" _value="News sidebar panel"/> + <oaf_attribute name="nautilus:sidebar_panel_name" type="string" _value="News"/> + <oaf_attribute name="nautilus:recommended_uri_schemes" type="stringv"> + <item value="*"/> + </oaf_attribute> +</oaf_server> + +</oaf_info> diff --git a/components/news/nautilus-news.c b/components/news/nautilus-news.c new file mode 100644 index 000000000..7d614856b --- /dev/null +++ b/components/news/nautilus-news.c @@ -0,0 +1,1697 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */ + +/* + * Nautilus + * + * Copyright (C) 2001 Eazel, Inc. + * + * This library 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 library 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 library; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + * + * Author: Andy Hertzfeld <andy@eazel.com> + * + */ + +/* This is the News sidebar panel, which displays current news headlines from + * a variety of web sites, by fetching and displaying RSS files + */ + +#include <config.h> +#include <gnome.h> + +#include <gnome-xml/parser.h> +#include <gnome-xml/xmlmemory.h> + +#include <eel/eel-background.h> +#include <eel/eel-debug.h> +#include <eel/eel-gdk-pixbuf-extensions.h> +#include <eel/eel-glib-extensions.h> +#include <eel/eel-gtk-extensions.h> +#include <eel/eel-label.h> +#include <eel/eel-scalable-font.h> +#include <eel/eel-smooth-text-layout.h> +#include <eel/eel-string.h> +#include <eel/eel-vfs-extensions.h> +#include <eel/eel-xml-extensions.h> + +#include <libnautilus-extensions/nautilus-entry.h> +#include <libnautilus-extensions/nautilus-file-attributes.h> +#include <libnautilus-extensions/nautilus-file.h> +#include <libnautilus-extensions/nautilus-file-utilities.h> +#include <libnautilus-extensions/nautilus-font-factory.h> +#include <libnautilus-extensions/nautilus-global-preferences.h> +#include <libnautilus-extensions/nautilus-metadata.h> +#include <libnautilus-extensions/nautilus-theme.h> +#include <libnautilus-extensions/nautilus-undo-signal-handlers.h> + +#include <libnautilus/nautilus-clipboard.h> +#include <libnautilus/nautilus-view-standard-main.h> + +#include "pixmaps.h" + +/* property bag getting and setting routines */ +enum { + TAB_IMAGE, +}; + +/* data structure for the news view */ +typedef struct { + NautilusView *view; + BonoboPropertyBag *property_bag; + + char *uri; /* keep track of where content view is at */ + + GList *channel_list; + + GdkPixbuf *pixbuf; /* offscreen buffer for news display */ + + GdkPixbuf *closed_triangle; + GdkPixbuf *open_triangle; + GdkPixbuf *bullet; + + GtkWidget *main_box; + GtkWidget *news_display; + + GtkWidget *configure_box; + GtkWidget *checkbox_list; + + GtkWidget *add_site_box; + GtkWidget *item_name_field; + GtkWidget *item_location_field; + + EelScalableFont *font; + + gboolean news_changed; + gboolean always_display_title; + gboolean configure_mode; + + guint timer_task; +} News; + +/* per item structure for rss items */ +typedef struct { + char *item_title; + char *item_url; + int item_start_y; + int item_end_y; +} RSSItemData; + +/* per channel structure for rss channel */ +typedef struct { + char *name; + char *uri; + char *link_uri; + News *owner; + + char *title; + GdkPixbuf *logo_image; + + GList *items; + + EelReadFileHandle *load_file_handle; + EelPixbufLoadHandle *load_image_handle; + + int prelight_index; + + int logo_start_y; + int logo_end_y; + int items_start_y; + int items_end_y; + + time_t last_update; + uint update_interval; + + gboolean is_open; + gboolean is_showing; + + gboolean update_in_progress; + gboolean error_flag; +} RSSChannelData; + +/* pixel and drawing constant defines */ + +#define RSS_ITEM_HEIGHT 12 +#define CHANNEL_GAP_SIZE 12 +#define LOGO_GAP_SIZE 2 +#define DISCLOSURE_RIGHT_POSITION 16 +#define LOGO_LEFT_OFFSET 20 +#define ITEM_POSITION 30 +#define ITEM_FONT_SIZE 11 +#define TIME_FONT_SIZE 9 +#define TIME_MARGIN_OFFSET 2 +#define TITLE_FONT_SIZE 18 +#define MINIMUM_DRAW_SIZE 16 +#define NEWS_BACKGROUND_RGBA 0xFFFFFFFF + +static char *news_get_indicator_image (News *news_data); +static void nautilus_news_free_channel_list (News *news_data); +static gboolean nautilus_news_save_channel_state (News* news_data); +static void update_size_and_redraw (News* news_data); +static char* get_xml_path (const char *file_name, gboolean force_local); +static int check_for_updates (gpointer callback_data); +static void add_channel_entry (News *news_data, const char *channel_name, + int index, gboolean is_showing); +static RSSChannelData* +nautilus_news_make_new_channel (News *news_data, + const char *name, + const char* channel_uri, + gboolean is_open, + gboolean is_showing); + +static void +get_bonobo_properties (BonoboPropertyBag *bag, + BonoboArg *arg, + guint arg_id, + CORBA_Environment *ev, + gpointer callback_data) +{ + char *indicator_image; + News *news; + + news = (News *) callback_data; + + switch (arg_id) { + case TAB_IMAGE: { + /* if there is a note, return the name of the indicator image, + otherwise, return NULL */ + indicator_image = news_get_indicator_image (news); + BONOBO_ARG_SET_STRING (arg, indicator_image); + g_free (indicator_image); + break; + } + + default: + g_warning ("Unhandled arg %d", arg_id); + break; + } +} + +static void +set_bonobo_properties (BonoboPropertyBag *bag, + const BonoboArg *arg, + guint arg_id, + CORBA_Environment *ev, + gpointer callback_data) +{ + g_warning ("Can't set note property %u", arg_id); +} + + +static void +do_destroy (GtkObject *obj, News *news) +{ + /* If the widget is being destroyed first, make sure the bonobo object + * that owns it is not destroyed half-way through the widget destroy + * process by reffing the bonobo object and only unreffing it at idle + * time. If the bonobo object is being destroyed first, then don't do + * this because it exposes a bonobo bug. + */ + if (!GTK_OBJECT_DESTROYED (GTK_OBJECT (news->view))) { + bonobo_object_ref (BONOBO_OBJECT (news->view)); + bonobo_object_idle_unref (BONOBO_OBJECT (news->view)); + } + + g_free (news->uri); + + if (news->font) { + gtk_object_unref (GTK_OBJECT (news->font)); + } + + if (news->timer_task != 0) { + gtk_timeout_remove (news->timer_task); + news->timer_task = 0; + } + + if (news->closed_triangle != NULL) { + gdk_pixbuf_unref (news->closed_triangle); + } + + if (news->open_triangle != NULL) { + gdk_pixbuf_unref (news->open_triangle); + } + + if (news->bullet != NULL) { + gdk_pixbuf_unref (news->bullet); + } + + /* free all the channel data */ + nautilus_news_free_channel_list (news); + + g_free (news); +} + + +/* drawing routines start here... */ + +/* convenience routine to composite an image with the proper clipping */ +static void +pixbuf_composite (GdkPixbuf *source, GdkPixbuf *destination, int x_offset, int y_offset, int alpha) +{ + int source_width, source_height, dest_width, dest_height; + double float_x_offset, float_y_offset; + + source_width = gdk_pixbuf_get_width (source); + source_height = gdk_pixbuf_get_height (source); + dest_width = gdk_pixbuf_get_width (destination); + dest_height = gdk_pixbuf_get_height (destination); + + float_x_offset = x_offset; + float_y_offset = y_offset; + + /* clip to the destination size */ + if ((x_offset + source_width) > dest_width) { + source_width = dest_width - x_offset; + } + if ((y_offset + source_height) > dest_height) { + source_height = dest_height - y_offset; + } + + gdk_pixbuf_composite (source, destination, x_offset, y_offset, source_width, source_height, + float_x_offset, float_y_offset, 1.0, 1.0, GDK_PIXBUF_ALPHA_BILEVEL, alpha); +} + +/* utility to draw the disclosure triangle */ +static void +draw_triangle (GdkPixbuf *pixbuf, RSSChannelData *channel_data, int v_offset) +{ + GdkPixbuf *triangle; + int v_delta, triangle_position; + int logo_height; + if (channel_data->is_open) { + triangle = channel_data->owner->open_triangle; + } else { + triangle = channel_data->owner->closed_triangle; + } + + if (channel_data->logo_image == NULL) { + logo_height = TITLE_FONT_SIZE; + } else { + logo_height = gdk_pixbuf_get_height (channel_data->logo_image); + } + + v_delta = logo_height - gdk_pixbuf_get_height (triangle); + triangle_position = v_offset + (v_delta / 2); + pixbuf_composite (triangle, pixbuf, 2, triangle_position, 255); +} + +/* draw the logo image */ +static int +draw_rss_logo_image (RSSChannelData *channel_data, + GdkPixbuf *pixbuf, + int offset, + gboolean measure_only) +{ + char time_str[16]; + int logo_width, logo_height; + int v_offset, pixbuf_width; + int time_x_pos, time_y_pos; + EelDimensions time_dimensions; + + v_offset = offset; + + if (channel_data->logo_image != NULL) { + logo_width = gdk_pixbuf_get_width (channel_data->logo_image); + logo_height = gdk_pixbuf_get_height (channel_data->logo_image); + + if (!measure_only) { + draw_triangle (pixbuf, channel_data, v_offset); + pixbuf_composite (channel_data->logo_image, pixbuf, LOGO_LEFT_OFFSET, v_offset, 255); + } + v_offset += logo_height + 2; + + /* also, draw the update time in the upper right corner */ + if (channel_data->last_update != 0 && !measure_only) { + strftime (&time_str[0], 16, "%I:%M %p", localtime (&channel_data->last_update)); + + time_dimensions = eel_scalable_font_measure_text (channel_data->owner->font, 9, time_str, strlen (time_str)); + + pixbuf_width = gdk_pixbuf_get_width (pixbuf); + time_x_pos = pixbuf_width - time_dimensions.width - TIME_MARGIN_OFFSET; + time_y_pos = offset + ((gdk_pixbuf_get_height (channel_data->logo_image) - time_dimensions.height) / 2); + eel_scalable_font_draw_text (channel_data->owner->font, pixbuf, + time_x_pos, time_y_pos, + NULL, TIME_FONT_SIZE, time_str, strlen (time_str), + EEL_RGB_COLOR_BLACK, EEL_OPACITY_FULLY_OPAQUE); + } + } + return v_offset; +} + +/* draw the title */ +static int +draw_rss_title (RSSChannelData *channel_data, + GdkPixbuf *pixbuf, + int v_offset, + gboolean measure_only) +{ + EelDimensions title_dimensions; + int label_offset; + + if (channel_data->title == NULL || channel_data->owner->font == NULL) { + return v_offset; + } + + /* first, measure the text */ + title_dimensions = eel_scalable_font_measure_text (channel_data->owner->font, + TITLE_FONT_SIZE, + channel_data->title, strlen (channel_data->title)); + + /* if there is no image, draw the disclosure triangle */ + if (channel_data->logo_image == NULL && !measure_only) { + draw_triangle (pixbuf, channel_data, v_offset); + label_offset = LOGO_LEFT_OFFSET; + } else { + label_offset = 4; + } + + /* draw the name into the pixbuf using anti-aliased text */ + if (!measure_only) { + eel_scalable_font_draw_text (channel_data->owner->font, pixbuf, + label_offset, v_offset, + NULL, + 18, + channel_data->title, strlen (channel_data->title), + EEL_RGB_COLOR_BLACK, + EEL_OPACITY_FULLY_OPAQUE); + } + + return v_offset + title_dimensions.height; +} + +/* draw the items */ +static int +draw_rss_items (RSSChannelData *channel_data, + GdkPixbuf *pixbuf, + int v_offset, + gboolean measure_only) +{ + GList *current_item; + RSSItemData *item_data; + int bullet_width, bullet_height, font_size; + int item_index, bullet_alpha; + int bullet_x_pos, bullet_y_pos; + int line_width; + guint32 text_color; + ArtIRect dest_bounds; + EelSmoothTextLayout *smooth_text_layout; + EelDimensions text_dimensions; + + if (channel_data->owner->bullet) { + bullet_width = gdk_pixbuf_get_width (channel_data->owner->bullet); + bullet_height = gdk_pixbuf_get_height (channel_data->owner->bullet); + } else { + bullet_width = 0; + bullet_height = 0; + } + + current_item = channel_data->items; + item_index = 0; + + while (current_item != NULL) { + /* draw the text */ + item_data = (RSSItemData*) current_item->data; + bullet_alpha = 255; + + if (eel_strcasecmp (item_data->item_url, channel_data->owner->uri) == 0) { + text_color = EEL_RGBA_COLOR_PACK (160, 0, 160, 255); + } + else if (item_index == channel_data->prelight_index) { + text_color = EEL_RGBA_COLOR_PACK (0, 0, 128, 255); + } else { + text_color = EEL_RGB_COLOR_BLACK; + bullet_alpha = 192; + } + + font_size = ITEM_FONT_SIZE; + item_data->item_start_y = v_offset; + smooth_text_layout = eel_smooth_text_layout_new ( + item_data->item_title, strlen(item_data->item_title), + channel_data->owner->font, font_size, TRUE); + + line_width = channel_data->owner->news_display->allocation.width - ITEM_POSITION; + if (line_width > 0) { + eel_smooth_text_layout_set_line_wrap_width (smooth_text_layout, line_width - 4); + } + text_dimensions = eel_smooth_text_layout_get_dimensions (smooth_text_layout); + + if (!measure_only) { + dest_bounds.x0 = ITEM_POSITION; + dest_bounds.y0 = v_offset; + dest_bounds.x1 = gdk_pixbuf_get_width (pixbuf); + dest_bounds.y1 = gdk_pixbuf_get_height (pixbuf); + + if (!art_irect_empty (&dest_bounds)) { + eel_smooth_text_layout_draw_to_pixbuf + (smooth_text_layout, pixbuf, + 0, 0, &dest_bounds, GTK_JUSTIFY_LEFT, + TRUE, text_color, + EEL_OPACITY_FULLY_OPAQUE); + + /* draw the bullet */ + if (channel_data->owner->bullet) { + bullet_x_pos = ITEM_POSITION - bullet_width - 2; + bullet_y_pos = v_offset + 2; + pixbuf_composite (channel_data->owner->bullet, pixbuf, + bullet_x_pos, bullet_y_pos, bullet_alpha); + } + } + } + gtk_object_destroy (GTK_OBJECT (smooth_text_layout)); + + item_data->item_end_y = item_data->item_start_y + text_dimensions.height; + v_offset += text_dimensions.height + 4; + + item_index += 1; + current_item = current_item->next; + + /* limit to five items max, for now */ + if (item_index > 5) { + break; + } + } + return v_offset; +} + +/* draw a single channel */ +static int +nautilus_news_draw_channel (News *news_data, + RSSChannelData *channel, + int v_offset, + gboolean measure_only) +{ + channel->logo_start_y = v_offset; + v_offset = draw_rss_logo_image (channel, news_data->pixbuf, v_offset, measure_only); + + if (news_data->always_display_title || channel->logo_image == NULL) { + v_offset = draw_rss_title (channel, news_data->pixbuf, v_offset, measure_only); + } + + channel->logo_end_y = v_offset; + v_offset += LOGO_GAP_SIZE; + + channel->items_start_y = v_offset; + if (channel->is_open) { + v_offset = draw_rss_items (channel, news_data->pixbuf, v_offset, measure_only); + } + + channel->items_end_y = v_offset; + return v_offset; +} + +/* main routine to render the channel list into the display pixbuf */ +static int +nautilus_news_update_display (News *news_data, gboolean measure_only) +{ + int width, height, v_offset; + GList *channel_item; + RSSChannelData *channel_data; + + v_offset = 2; + + if (news_data->pixbuf == NULL && !measure_only) { + return v_offset; + } + + /* don't draw if too small */ + if (!measure_only) { + width = gdk_pixbuf_get_width (news_data->pixbuf); + height = gdk_pixbuf_get_height (news_data->pixbuf); + + /* don't draw when too small, like during size negotiation */ + if ((width < MINIMUM_DRAW_SIZE || height < MINIMUM_DRAW_SIZE) && !measure_only) { + return v_offset; + } + + eel_gdk_pixbuf_fill_rectangle_with_color (news_data->pixbuf, NULL, NEWS_BACKGROUND_RGBA); + } + + /* loop through the channel list, drawing one channel at a time */ + channel_item = news_data->channel_list; + while (channel_item != NULL) { + channel_data = (RSSChannelData*) channel_item->data; + if (!channel_data->update_in_progress && channel_data->is_showing) { + + v_offset = nautilus_news_draw_channel (news_data, + channel_data, + v_offset, measure_only); + if (channel_data->is_open) { + v_offset += CHANNEL_GAP_SIZE; + } + } + channel_item = channel_item->next; + } + return v_offset; +} + +/* allocate the pixbuf to draw into */ +static gint +nautilus_news_configure_event (GtkWidget *widget, GdkEventConfigure *event, News *news_data ) +{ + if (news_data->pixbuf != NULL) { + gdk_pixbuf_unref (news_data->pixbuf); + } + + news_data->pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, TRUE, 8, + widget->allocation.width, widget->allocation.height); + + eel_gdk_pixbuf_fill_rectangle_with_color (news_data->pixbuf, NULL, NEWS_BACKGROUND_RGBA); + return TRUE; +} + +/* handle the news display drawing */ +static gint +nautilus_news_expose_event( GtkWidget *widget, GdkEventExpose *event, News *news_data ) +{ + int pixbuf_width, pixbuf_height; + + /* this shouldn't be necessary - remove it soon */ + nautilus_news_update_display (news_data, FALSE); + + pixbuf_width = gdk_pixbuf_get_width (news_data->pixbuf); + pixbuf_height = gdk_pixbuf_get_height (news_data->pixbuf); + + gdk_pixbuf_render_to_drawable_alpha (news_data->pixbuf, + widget->window, + 0, 0, + widget->allocation.x, widget->allocation.y, + pixbuf_width, pixbuf_height, + GDK_PIXBUF_ALPHA_BILEVEL, 128, + GDK_RGB_DITHER_MAX, + 0, 0); + return FALSE; +} + +/* utility to set the prelight index of a channel and redraw if necessary */ +static void +nautilus_news_set_prelight_index (RSSChannelData *channel_data, int new_prelight_index) +{ + if (channel_data->prelight_index != new_prelight_index) { + channel_data->prelight_index = new_prelight_index; + nautilus_news_update_display (channel_data->owner, FALSE); + gtk_widget_queue_draw (GTK_WIDGET (channel_data->owner->news_display)); + } +} + + +/* utility routine to tell Nautilus to navigate to the passed-in uri */ +static void +go_to_uri (News* news_data, const char* uri) +{ + nautilus_view_open_location_in_this_window (news_data->view, uri); +} + +/* utility routine to toggle the open state of the passed in channel */ +static void +toggle_open_state (RSSChannelData *channel_data) +{ + channel_data->is_open = !channel_data->is_open; + update_size_and_redraw (channel_data->owner); + nautilus_news_save_channel_state (channel_data->owner); +} + +/* handle item hit testing */ +static int +item_hit_test (RSSChannelData *channel_data, int y_pos) +{ + RSSItemData *item_data; + GList *next_item; + int item_index; + + item_index = 0; + next_item = channel_data->items; + while (next_item != NULL) { + item_data = (RSSItemData*) next_item->data; + if (y_pos >= item_data->item_start_y && y_pos <= item_data->item_end_y) { + return item_index; + } + item_index += 1; + next_item = next_item->next; + } + return -1; +} + +/* handle the news display hit-testing */ +static gint +nautilus_news_button_release_event (GtkWidget *widget, GdkEventButton *event, News *news_data ) +{ + GList *current_channel; + GList *selected_item; + RSSChannelData *channel_data; + RSSItemData *item_data; + int which_item; + + /* loop through all of the channels */ + current_channel = news_data->channel_list; + while (current_channel != NULL) { + channel_data = (RSSChannelData*) current_channel->data; + + /* if the channel isn't showing, skip all this */ + if (!channel_data->is_showing) { + current_channel = current_channel->next; + continue; + } + + /* see if the mouse went down in this channel */ + if (event->y >= channel_data->logo_start_y && event->y <= channel_data->items_end_y) { + + /* see if the user clicked on the logo or title area */ + if (event->y <= channel_data->logo_end_y) { + /* distinguish between the disclosure triangle area and the logo proper */ + if (event->x < DISCLOSURE_RIGHT_POSITION) { + toggle_open_state (channel_data); + } else { + go_to_uri (news_data, channel_data->link_uri); + } + return TRUE; + } + + + /* if it's open, determine which item was clicked */ + if (channel_data->is_open && event->y >= channel_data->items_start_y) { + which_item = item_hit_test (channel_data, event->y); + if (which_item < (int) g_list_length (channel_data->items)) { + selected_item = g_list_nth (channel_data->items, which_item); + item_data = (RSSItemData*) selected_item->data; + go_to_uri (news_data, item_data->item_url); + return TRUE; + } + } + } + current_channel = current_channel->next; + } + return TRUE; +} + +/* handle motion notify events by prelighting as appropriate */ +static gint +nautilus_news_motion_notify_event (GtkWidget *widget, GdkEventMotion *event, News *news_data ) +{ + GList *current_channel; + RSSChannelData *channel_data; + int which_item; + + /* loop through all of the channels to find the one the mouse is over */ + current_channel = news_data->channel_list; + while (current_channel != NULL) { + channel_data = (RSSChannelData*) current_channel->data; + + /* see if it's in the items for this channel */ + if (event->y >= channel_data->items_start_y && event->y <= channel_data->items_end_y) { + which_item = item_hit_test (channel_data, event->y); + if (which_item < (int) g_list_length (channel_data->items)) { + nautilus_news_set_prelight_index (channel_data, which_item); + return TRUE; + } + } else { + nautilus_news_set_prelight_index (channel_data, -1); + } + + current_channel = current_channel->next; + } + return TRUE; +} + +/* handle leave notify events by turning off any prelighting */ +static gint +nautilus_news_leave_notify_event (GtkWidget *widget, GdkEventMotion *event, News *news_data ) +{ + GList *current_channel; + RSSChannelData *channel_data; + + /* loop through all of the channels to turn off prelighting */ + current_channel = news_data->channel_list; + while (current_channel != NULL) { + channel_data = (RSSChannelData*) current_channel->data; + nautilus_news_set_prelight_index (channel_data, -1); + current_channel = current_channel->next; + } + return TRUE; +} + +static void +nautilus_news_set_title (RSSChannelData *channel_data, const char *title) +{ + if (eel_strcmp (channel_data->title, title) == 0) { + return; + } + + if (channel_data->title) { + g_free (channel_data->title); + } + if (title != NULL) { + channel_data->title = g_strdup (title); + } else { + channel_data->title = NULL; + } +} + +static void +free_rss_data_item (RSSItemData *item) +{ + g_free (item->item_title); + g_free (item->item_url); + g_free (item); +} + +static void +free_rss_channel_items (RSSChannelData *channel_data) +{ + eel_g_list_free_deep_custom (channel_data->items, (GFunc) free_rss_data_item, NULL); + channel_data->items = NULL; +} + +/* this frees a single channel object */ +static void +free_channel (RSSChannelData *channel_data) +{ + g_free (channel_data->name); + g_free (channel_data->uri); + g_free (channel_data->link_uri); + g_free (channel_data->title); + + if (channel_data->logo_image != NULL) { + gdk_pixbuf_unref (channel_data->logo_image); + } + + if (channel_data->load_file_handle != NULL) { + eel_read_file_cancel (channel_data->load_file_handle); + } + + if (channel_data->load_image_handle != NULL) { + eel_cancel_gdk_pixbuf_load (channel_data->load_image_handle); + } + + free_rss_channel_items (channel_data); + + g_free (channel_data); +} + +/* free the entire channel list */ +static void +nautilus_news_free_channel_list (News *news_data) +{ + GList *current_item; + + current_item = news_data->channel_list; + while (current_item != NULL) { + free_channel ((RSSChannelData*) current_item->data); + current_item = current_item->next; + } + + g_list_free (news_data->channel_list); + news_data->channel_list = NULL; +} + +/* utility to express boolean as a string */ +static char * +bool_to_text (gboolean value) +{ + return value ? "true" : "false"; +} + +/* build a channels xml file from the current channels state */ +static xmlDocPtr +nautilus_news_make_channel_document (News* news_data) +{ + xmlDoc *channel_doc; + xmlNode *root_node; + xmlNode *channel_node; + RSSChannelData *channel_data; + GList *next_channel; + + channel_doc = xmlNewDoc ("1.0"); + + /* add the root node to the channel document */ + root_node = xmlNewDocNode (channel_doc, NULL, "rss_news_channels", NULL); + xmlDocSetRootElement (channel_doc, root_node); + + /* loop through the channels, adding a node for each channel */ + next_channel = news_data->channel_list; + while (next_channel != NULL) { + channel_node = xmlNewChild (root_node, NULL, "rss_channel", NULL); + channel_data = (RSSChannelData*) next_channel->data; + + xmlSetProp (channel_node, "name", channel_data->name); + xmlSetProp (channel_node, "uri", channel_data->uri); + xmlSetProp (channel_node, "show", bool_to_text (channel_data->is_showing)); + xmlSetProp (channel_node, "open", bool_to_text (channel_data->is_open)); + + next_channel = next_channel->next; + } + return channel_doc; +} + +/* save the current channel state to disk */ +static gboolean +nautilus_news_save_channel_state (News* news_data) +{ + int result; + char *path; + xmlDoc *channel_doc; + + path = get_xml_path ("news_channels.xml", TRUE); + channel_doc = nautilus_news_make_channel_document (news_data); + + result = xmlSaveFile (path, channel_doc); + + g_free (path); + xmlFreeDoc (channel_doc); + + return result > 0; +} + +static void +rss_logo_callback (GnomeVFSResult error, GdkPixbuf *pixbuf, gpointer callback_data) +{ + RSSChannelData *channel_data; + + channel_data = (RSSChannelData*) callback_data; + channel_data->load_image_handle = NULL; + + if (channel_data->logo_image) { + gdk_pixbuf_unref (channel_data->logo_image); + } + + if (pixbuf != NULL) { + gdk_pixbuf_ref (pixbuf); + pixbuf = eel_gdk_pixbuf_scale_down_to_fit (pixbuf, 192, 40); + channel_data->logo_image = pixbuf; + update_size_and_redraw (channel_data->owner); + } +} + +/* utility routine to extract items from a node, returning the count of items found */ +static int +extract_items (RSSChannelData *channel_data, xmlNodePtr container_node) +{ + RSSItemData *item_parameters; + xmlNodePtr current_node, title_node, temp_node; + int item_count; + char *title, *temp_str; + + current_node = container_node->childs; + item_count = 0; + while (current_node != NULL) { + if (eel_strcmp (current_node->name, "item") == 0) { + title_node = eel_xml_get_child_by_name (current_node, "title"); + if (title_node) { + item_parameters = (RSSItemData*) g_new0 (RSSItemData, 1); + + title = xmlNodeGetContent (title_node); + item_parameters->item_title = g_strdup (title); + xmlFree (title); + temp_node = eel_xml_get_child_by_name (current_node, "link"); + + if (temp_node) { + temp_str = xmlNodeGetContent (temp_node); + item_parameters->item_url = g_strdup (temp_str); + xmlFree (temp_str); + } + + channel_data->items = g_list_append (channel_data->items, item_parameters); + item_count += 1; + } + } + current_node = current_node->next; + } + return item_count; +} + +/* utility routine to resize the news display and redraw it */ +static void +update_size_and_redraw (News* news_data) +{ + int display_size; + + display_size = nautilus_news_update_display (news_data, TRUE); + gtk_widget_set_usize (news_data->news_display, -1, display_size); + + nautilus_news_update_display (news_data, FALSE); + gtk_widget_queue_resize (GTK_WIDGET (news_data->news_display)); + gtk_widget_queue_draw (GTK_WIDGET (news_data->news_display)); +} + +/* completion routine invoked when we've loaded the rss file uri. Parse the xml document, and + * then extract the various elements that we require */ +static void +rss_read_done_callback (GnomeVFSResult result, + GnomeVFSFileSize file_size, + char *file_contents, + gpointer callback_data) +{ + xmlDocPtr rss_document; + xmlNodePtr image_node, channel_node; + xmlNodePtr current_node, temp_node, uri_node; + char *image_uri, *title, *temp_str; + char *error_message; + int item_count; + RSSChannelData *channel_data; + + char *buffer; + + channel_data = (RSSChannelData*) callback_data; + channel_data->load_file_handle = NULL; + + /* make sure the read was successful */ + if (result != GNOME_VFS_OK) { + g_assert (file_contents == NULL); + channel_data->update_in_progress = FALSE; + error_message = g_strdup_printf ("Couldn't load %s", channel_data->name); + nautilus_news_set_title (channel_data, error_message); + channel_data->error_flag = TRUE; + g_free (error_message); + return; + } + + /* flag the update time */ + time (&channel_data->last_update); + + /* Parse the rss file with gnome-xml. The gnome-xml parser requires a zero-terminated array. */ + buffer = g_realloc (file_contents, file_size + 1); + buffer[file_size] = '\0'; + rss_document = xmlParseMemory (buffer, file_size); + g_free (buffer); + + /* make sure there wasn't in error parsing the document */ + if (rss_document == NULL) { + channel_data->update_in_progress = FALSE; + return; + } + + /* extract the title and set it */ + channel_node = eel_xml_get_child_by_name (xmlDocGetRootElement (rss_document), "channel"); + if (channel_node != NULL) { + temp_node = eel_xml_get_child_by_name (channel_node, "title"); + if (temp_node != NULL) { + title = xmlNodeGetContent (temp_node); + if (title != NULL) { + nautilus_news_set_title (channel_data, title); + xmlFree (title); + } + } + + temp_node = eel_xml_get_child_by_name (channel_node, "link"); + if (temp_node != NULL) { + temp_str = xmlNodeGetContent (temp_node); + if (temp_str != NULL) { + g_free (channel_data->link_uri); + channel_data->link_uri = g_strdup (temp_str); + xmlFree (temp_str); + } + } + + } + + /* extract the image uri and, if found, load it asynchronously */ + /* don't reload it if we already have one */ + if (channel_data->logo_image == NULL) { + image_node = eel_xml_get_child_by_name (xmlDocGetRootElement (rss_document), "image"); + + /* if we can't find it at the top level, look inside the channel */ + if (image_node == NULL && channel_node != NULL) { + image_node = eel_xml_get_child_by_name (channel_node, "image"); + } + + if (image_node != NULL) { + uri_node = eel_xml_get_child_by_name (image_node, "url"); + if (uri_node != NULL) { + image_uri = xmlNodeGetContent (uri_node); + if (image_uri != NULL) { + channel_data->load_image_handle = eel_gdk_pixbuf_load_async (image_uri, rss_logo_callback, channel_data); + xmlFree (image_uri); + } + } + } + } + + /* extract the items */ + free_rss_channel_items (channel_data); + + current_node = rss_document->root; + item_count = extract_items (channel_data, current_node); + + /* if we couldn't find any items at the main level, look inside the channel node */ + if (item_count == 0 && channel_node != NULL) { + item_count = extract_items (channel_data, channel_node); + } + + /* we're done, so free everything up */ + xmlFreeDoc (rss_document); + channel_data->update_in_progress = FALSE; + + /* update the size of the display aread to reflect the new content and + * schedule a redraw. + */ + update_size_and_redraw (channel_data->owner); +} + +/* initiate the loading of a channel, by fetching the rss file through gnome-vfs */ +static void +nautilus_news_load_channel (News *news_data, RSSChannelData *channel_data) +{ + char *title; + /* load the uri asynchrounously, calling a completion routine when completed */ + + channel_data->update_in_progress = TRUE; + channel_data->load_file_handle = eel_read_entire_file_async (channel_data->uri, rss_read_done_callback, channel_data); + + /* put up a title that's displayed while we wait */ + title = g_strdup_printf ("Loading %s", channel_data->uri); + nautilus_news_set_title (channel_data, title); + g_free (title); +} + +/* create a new channel object and initialize it, and start loading the content */ +static RSSChannelData* +nautilus_news_make_new_channel (News *news_data, + const char *name, + const char* channel_uri, + gboolean is_open, + gboolean is_showing) +{ + RSSChannelData *channel_data; + + channel_data = g_new0 (RSSChannelData, 1); + channel_data->name = g_strdup (name); + channel_data->uri = g_strdup (channel_uri); + channel_data->owner = news_data; + channel_data->update_interval = 300; + channel_data->prelight_index = -1; + channel_data->is_open = is_open; + channel_data->is_showing = is_showing; + + if (channel_data->is_showing) { + nautilus_news_load_channel (news_data, channel_data); + } + return channel_data; +} + +/* add the channels defined in the passed in xml document to the channel list, + * and start fetching the actual channel data + */ +static void +nautilus_news_add_channels (News *news_data, xmlDocPtr channels) +{ + xmlNodePtr current_channel; + RSSChannelData *channel_data; + char *uri, *name; + char *open_str, *show_str; + gboolean is_open, is_showing; + + /* walk through the children of the root object, generating new channel + * objects and adding them to the channel list + */ + current_channel = xmlDocGetRootElement (channels)->childs; + while (current_channel != NULL) { + if (eel_strcmp (current_channel->name, "rss_channel") == 0) { + name = xmlGetProp (current_channel, "name"); + uri = xmlGetProp (current_channel, "uri"); + open_str = xmlGetProp (current_channel, "open"); + show_str = xmlGetProp (current_channel, "show"); + + if (uri != NULL) { + is_open = eel_strcasecmp (open_str, "true") == 0; + is_showing = eel_strcasecmp (show_str, "true") == 0; + + channel_data = nautilus_news_make_new_channel (news_data, name, uri, + is_open, is_showing); + xmlFree (uri); + if (channel_data != NULL) { + news_data->channel_list = g_list_append (news_data->channel_list, channel_data); + } + } + xmlFree (open_str); + xmlFree (show_str); + xmlFree (name); + } + current_channel = current_channel->next; + } +} + +static char* +get_xml_path (const char *file_name, gboolean force_local) +{ + char *xml_path; + char *user_directory; + + user_directory = nautilus_get_user_directory (); + + /* first try the user's home directory */ + xml_path = nautilus_make_path (user_directory, + file_name); + g_free (user_directory); + if (force_local || g_file_exists (xml_path)) { + return xml_path; + } + g_free (xml_path); + + /* next try the shared directory */ + xml_path = nautilus_make_path (NAUTILUS_DATADIR, + file_name); + if (g_file_exists (xml_path)) { + return xml_path; + } + g_free (xml_path); + + return NULL; +} + +/* read the channel definition xml file and load the channels */ +static void +read_channel_list (News *news_data) +{ + char *path; + xmlDocPtr channel_doc; + /* free the old channel data, if any */ + nautilus_news_free_channel_list (news_data); + + /* get the path to the local copy of the channels file */ + path = get_xml_path ("news_channels.xml", FALSE); + if (path != NULL) { + channel_doc = xmlParseFile (path); + + if (channel_doc) { + nautilus_news_add_channels (news_data, channel_doc); + xmlFreeDoc (channel_doc); + } + g_free (path); + } +} + +/* handle periodically updating the channels if necessary */ +static int +check_for_updates (gpointer callback_data) +{ + News *news_data; + guint current_time, next_update_time; + GList *current_item; + RSSChannelData *channel_data; + + news_data = (News*) callback_data; + current_time = time (NULL); + + /* loop through the channel list, checking to see if any need updating */ + current_item = news_data->channel_list; + while (current_item != NULL) { + channel_data = (RSSChannelData*) current_item->data; + next_update_time = channel_data->last_update + channel_data->update_interval; + + if (current_time > next_update_time && !channel_data->update_in_progress && channel_data->is_showing) { + nautilus_news_load_channel (news_data, channel_data); + } + + current_item = current_item->next; + } + + return TRUE; +} + +/* return an image if there is a new article since last viewing, otherwise return NULL */ +static char * +news_get_indicator_image (News *news_data) +{ + if (news_data->news_changed) { + return g_strdup ("note-indicator.png"); + } + return NULL; +} + +/* utility to load an xpm image */ +static void +load_xpm_image (GdkPixbuf** image_result, const char** image_name) +{ + if (*image_result != NULL) { + gdk_pixbuf_unref (*image_result); + } + *image_result = gdk_pixbuf_new_from_xpm_data (image_name); +} + +/* utility routine to load images needed by the news view */ +static void +nautilus_news_load_images (News *news_data) +{ + char *news_bullet_path; + + load_xpm_image (&news_data->closed_triangle, (const char**) triangle_xpm); + load_xpm_image (&news_data->open_triangle, (const char**) open_triangle_xpm); + + if (news_data->bullet != NULL) { + gdk_pixbuf_unref (news_data->bullet); + } + news_bullet_path = nautilus_theme_get_image_path ("news_bullet.png"); + if (news_bullet_path != NULL) { + news_data->bullet = gdk_pixbuf_new_from_file (news_bullet_path); + g_free (news_bullet_path); + } +} + +/* here's the button callback routine that toggles between display modes */ +static void +configure_button_clicked (GtkWidget *widget, News *news) +{ + news->configure_mode = !news->configure_mode; + if (news->configure_mode) { + gtk_widget_show_all (news->configure_box); + gtk_widget_hide_all (news->main_box); + gtk_widget_hide_all (news->add_site_box); + } else { + /* when exiting configure mode, update everything */ + gtk_widget_show_all (news->main_box); + gtk_widget_hide_all (news->configure_box); + gtk_widget_hide_all (news->add_site_box); + + nautilus_news_save_channel_state (news); + check_for_updates (news); + update_size_and_redraw (news); + } +} + +/* here's the button callback routine that handles the add new site button + * by showing the relevant widgets. + */ +static void +add_site_button_clicked (GtkWidget *widget, News *news) +{ + news->configure_mode = FALSE; + gtk_widget_hide_all (news->configure_box); + gtk_widget_show_all (news->add_site_box); +} + +/* handle adding a new site from the data in the "add site" fields */ +static void +add_site_from_fields (GtkWidget *widget, News *news) +{ + char *site_name, *site_location; + RSSChannelData *channel_data; + int channel_count; + + site_name = gtk_entry_get_text (GTK_ENTRY (news->item_name_field)); + site_location = gtk_entry_get_text (GTK_ENTRY (news->item_location_field)); + + channel_data = nautilus_news_make_new_channel (news, site_name, site_location, TRUE, TRUE); + if (channel_data != NULL) { + news->channel_list = g_list_append (news->channel_list, channel_data); + channel_count = g_list_length (news->channel_list); + add_channel_entry (news, site_name, channel_count, TRUE); + } + /* back to configure mode */ + configure_button_clicked (widget, news); +} + +/* utility routine to create the button box and constituent buttons */ +static GtkWidget * +add_command_buttons (News *news_data, const char* label, gboolean from_configure) +{ + GtkWidget *frame; + GtkWidget *button_box; + GtkWidget *button; + + frame = gtk_frame_new (NULL); + gtk_frame_set_shadow_type( GTK_FRAME (frame), GTK_SHADOW_OUT); + + button_box = gtk_hbutton_box_new (); + + gtk_container_set_border_width (GTK_CONTAINER (button_box), 2); + gtk_container_add (GTK_CONTAINER (frame), button_box); + + /* Set the appearance of the Button Box */ + gtk_button_box_set_layout (GTK_BUTTON_BOX (button_box), GTK_BUTTONBOX_END); + + gtk_button_box_set_spacing (GTK_BUTTON_BOX (button_box), 4); + gtk_button_box_set_child_size (GTK_BUTTON_BOX (button_box), 24, 14); + + if (from_configure) { + button = gtk_button_new_with_label (_("Add Site")); + gtk_container_add (GTK_CONTAINER (button_box), button); + + gtk_signal_connect (GTK_OBJECT (button), "clicked", + (GtkSignalFunc) add_site_button_clicked, news_data); + } + + button = gtk_button_new_with_label (label); + gtk_container_add (GTK_CONTAINER (button_box), button); + + gtk_signal_connect (GTK_OBJECT (button), "clicked", + (GtkSignalFunc) configure_button_clicked, news_data); + + return frame; +} + +/* utility routine to look up a channel from it's name */ +static RSSChannelData* +get_channel_from_name (News *news_data, const char *channel_name) +{ + GList *channel_item; + RSSChannelData *channel_data; + + channel_item = news_data->channel_list; + while (channel_item != NULL) { + channel_data = (RSSChannelData*) channel_item->data; + if (eel_strcasecmp (channel_data->name, channel_name) == 0) { + return channel_data; + } + channel_item = channel_item->next; + } + return NULL; +} + +/* here's the handler for handling clicks in channel check boxes */ +static void +check_button_toggled_callback (GtkToggleButton *toggle_button, gpointer user_data) +{ + News *news_data; + char *channel_name; + RSSChannelData *channel_data; + + news_data = (News*) user_data; + channel_name = gtk_object_get_data (GTK_OBJECT (toggle_button), "channel_name"); + + channel_data = get_channel_from_name (news_data, channel_name); + if (channel_data != NULL) { + channel_data->is_showing = !channel_data->is_showing; + if (channel_data->is_showing) { + channel_data->is_open = TRUE; + } + } +} + +/* callback to maintain the current location */ +static void +nautilus_news_load_location (NautilusView *view, const char *location, News *news) +{ + g_free (news->uri); + news->uri = g_strdup (location); + update_size_and_redraw (news); +} + +/* utility routine to add a check-box entry to the channel list */ +static void +add_channel_entry (News *news_data, const char *channel_name, int index, gboolean is_showing) +{ + GtkWidget *check_button; + + check_button = gtk_check_button_new_with_label (channel_name); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check_button), is_showing); + gtk_box_pack_start (GTK_BOX (news_data->checkbox_list), check_button, FALSE, FALSE, 0); + + gtk_signal_connect (GTK_OBJECT (check_button), "toggled", + GTK_SIGNAL_FUNC (check_button_toggled_callback), + news_data); + + /* set up user data to use in toggle handler */ + gtk_object_set_user_data (GTK_OBJECT (check_button), news_data); + gtk_object_set_data_full (GTK_OBJECT (check_button), + "channel_name", + g_strdup (channel_name), + (GtkDestroyNotify) g_free); +} + +/* here's the routine that loads and parses the xml file, then iterates through it + * to add channels to the enable/disable list + */ +static void +add_channels_to_configure_list (News* news_data) +{ + char *path; + char *channel_name, *show_str; + xmlDocPtr channel_doc; + xmlNodePtr current_channel; + int channel_index; + gboolean is_shown; + + /* read the xml file and parse it */ + path = get_xml_path ("news_channels.xml", FALSE); + if (path == NULL) { + return; + } + + channel_doc = xmlParseFile (path); + g_free (path); + if (channel_doc == NULL) { + return; + } + + /* loop through the channel entries, adding an entry to the configure + * list for each entry in the file + */ + current_channel = xmlDocGetRootElement (channel_doc)->childs; + channel_index = 0; + while (current_channel != NULL) { + if (eel_strcmp (current_channel->name, "rss_channel") == 0) { + channel_name = xmlGetProp (current_channel, "name"); + show_str = xmlGetProp (current_channel, "show"); + is_shown = eel_strcasecmp (show_str, "true") == 0; + + /* add an entry to the channel list */ + if (channel_name != NULL) { + add_channel_entry (news_data, channel_name, channel_index, is_shown); + channel_index += 1; + } + + xmlFree (show_str); + xmlFree (channel_name); + } + current_channel = current_channel->next; + } + + xmlFreeDoc (channel_doc); +} + +/* allocate the add/remove location widgets */ +static void +set_up_add_widgets (News *news, GtkWidget *container) +{ + GtkWidget *label; + GtkWidget *temp_vbox; + GtkWidget *expand_box; + GtkWidget *button_box, *button; + + news->add_site_box = gtk_vbox_new (FALSE, 0); + gtk_box_pack_start (GTK_BOX (container), news->add_site_box, TRUE, TRUE, 0); + + /* add a descriptive label */ + label = eel_label_new (_("Add A New Site:")); + eel_label_set_smooth_font_size (EEL_LABEL (label), 18); + eel_label_set_justify (EEL_LABEL (label), GTK_JUSTIFY_LEFT); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + + gtk_box_pack_start (GTK_BOX (news->add_site_box), label, FALSE, FALSE, 0); + + expand_box = gtk_vbox_new (FALSE, 0); + gtk_box_pack_start (GTK_BOX (news->add_site_box), expand_box, TRUE, TRUE, 0); + + temp_vbox = gtk_vbox_new (FALSE, 0); + gtk_container_set_border_width (GTK_CONTAINER (temp_vbox), 4); + gtk_box_pack_start (GTK_BOX (expand_box), temp_vbox, FALSE, FALSE, 0); + + label = eel_label_new (_("Site Name:")); + eel_label_set_justify (EEL_LABEL (label), GTK_JUSTIFY_LEFT); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (temp_vbox), label, FALSE, FALSE, 0); + + news->item_name_field = nautilus_entry_new (); + gtk_box_pack_start (GTK_BOX (temp_vbox), news->item_name_field, FALSE, FALSE, 0); + nautilus_undo_editable_set_undo_key (GTK_EDITABLE (news->item_name_field), TRUE); + + /* allocate the location field */ + temp_vbox = gtk_vbox_new (FALSE, 0); + gtk_container_set_border_width (GTK_CONTAINER (temp_vbox), 4); + gtk_box_pack_start (GTK_BOX (expand_box), temp_vbox, FALSE, FALSE, 0); + + label = eel_label_new (_("Site Location:")); + eel_label_set_justify (EEL_LABEL (label), GTK_JUSTIFY_LEFT); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (temp_vbox), label, FALSE, FALSE, 0); + + news->item_location_field = nautilus_entry_new (); + gtk_box_pack_start (GTK_BOX (temp_vbox), news->item_location_field, FALSE, FALSE, 0); + nautilus_undo_editable_set_undo_key (GTK_EDITABLE (news->item_location_field), TRUE); + + /* install the add buttons */ + button_box = gtk_hbutton_box_new (); + gtk_container_set_border_width (GTK_CONTAINER (button_box), 4); + gtk_box_pack_start (GTK_BOX (expand_box), button_box, FALSE, FALSE, 0); + + gtk_button_box_set_layout (GTK_BUTTON_BOX (button_box), GTK_BUTTONBOX_END); + gtk_button_box_set_spacing (GTK_BUTTON_BOX (button_box), 4); + gtk_button_box_set_child_size (GTK_BUTTON_BOX (button_box), 24, 14); + + button = gtk_button_new_with_label (_("Add New Site")); + gtk_container_add (GTK_CONTAINER (button_box), button); + gtk_signal_connect (GTK_OBJECT (button), "clicked", + (GtkSignalFunc) add_site_from_fields, news); + + /* add the button box at the bottom with a cancel button */ + button_box = add_command_buttons (news, _("Cancel"), FALSE); + gtk_box_pack_start (GTK_BOX (news->add_site_box), button_box, FALSE, FALSE, 0); +} + +/* allocate the widgets for the configure mode */ +static void +set_up_configure_widgets (News *news, GtkWidget *container) +{ + GtkWidget *button_box; + GtkWidget *viewport; + GtkScrolledWindow *scrolled_window; + GtkWidget *label; + + news->configure_box = gtk_vbox_new (FALSE, 0); + gtk_box_pack_start (GTK_BOX (container), news->configure_box, TRUE, TRUE, 0); + + /* add a descriptive label */ + label = eel_label_new (_("Select Sites:")); + eel_label_set_smooth_font_size (EEL_LABEL (label), 18); + eel_label_set_justify (EEL_LABEL (label), GTK_JUSTIFY_LEFT); + gtk_misc_set_alignment (GTK_MISC (label), 0.0, 0.5); + gtk_box_pack_start (GTK_BOX (news->configure_box), label, FALSE, FALSE, 0); + + /* allocate a table to hold the check boxes */ + news->checkbox_list = gtk_vbox_new (FALSE, 0); + + scrolled_window = GTK_SCROLLED_WINDOW (gtk_scrolled_window_new (NULL, NULL)); + gtk_scrolled_window_set_policy (scrolled_window, GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + viewport = gtk_viewport_new (gtk_scrolled_window_get_hadjustment (scrolled_window), + gtk_scrolled_window_get_vadjustment (scrolled_window)); + gtk_container_add (GTK_CONTAINER (scrolled_window), viewport); + gtk_viewport_set_shadow_type (GTK_VIEWPORT (viewport), GTK_SHADOW_NONE); + gtk_container_add (GTK_CONTAINER (viewport), news->checkbox_list); + gtk_box_pack_start (GTK_BOX (news->configure_box), GTK_WIDGET (scrolled_window), TRUE, TRUE, 0); + + /* allocate the button box for the done button */ + button_box = add_command_buttons (news, _("Done"), TRUE); + gtk_box_pack_start (GTK_BOX (news->configure_box), button_box, FALSE, FALSE, 0); +} + +/* allocate the widgets for the main display mode */ +static void +set_up_main_widgets (News *news, GtkWidget *container) +{ + GtkWidget *button_box; + GtkWidget *scrolled_window; + + /* allocate a vbox to hold all of the main UI elements elements */ + news->main_box = gtk_vbox_new (FALSE, 0); + gtk_box_pack_start (GTK_BOX (container), news->main_box, TRUE, TRUE, 0); + + /* create and install the display area */ + news->news_display = gtk_drawing_area_new (); + + /* put the display in a scrolled window so it can scroll */ + scrolled_window = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled_window), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled_window), news->news_display); + gtk_box_pack_start (GTK_BOX (news->main_box), scrolled_window, TRUE, TRUE, 0); + + /* connect the appropriate signals for drawing and event handling */ + gtk_signal_connect (GTK_OBJECT (news->news_display), "expose_event", + (GtkSignalFunc) nautilus_news_expose_event, news); + gtk_signal_connect (GTK_OBJECT(news->news_display),"configure_event", + (GtkSignalFunc) nautilus_news_configure_event, news); + + gtk_signal_connect (GTK_OBJECT (news->news_display), "motion_notify_event", + (GtkSignalFunc) nautilus_news_motion_notify_event, news); + gtk_signal_connect (GTK_OBJECT (news->news_display), "leave_notify_event", + (GtkSignalFunc) nautilus_news_leave_notify_event, news); + gtk_signal_connect (GTK_OBJECT (news->news_display), "button_release_event", + (GtkSignalFunc) nautilus_news_button_release_event, news); + + gtk_widget_set_events (news->news_display, GDK_EXPOSURE_MASK + | GDK_LEAVE_NOTIFY_MASK + | GDK_BUTTON_PRESS_MASK + | GDK_BUTTON_RELEASE_MASK); + gtk_widget_add_events (news->news_display, GDK_POINTER_MOTION_MASK); + + /* create a button box to hold the command buttons */ + button_box = add_command_buttons (news, _("Select Sites"), FALSE); + gtk_box_pack_start (GTK_BOX (news->main_box), button_box, FALSE, FALSE, 0); +} + +static NautilusView * +make_news_view (const char *iid, gpointer callback_data) +{ + News *news; + GtkWidget *main_container; + + /* create the private data for the news view */ + news = g_new0 (News, 1); + + /* allocate the main container */ + main_container = gtk_vbox_new (FALSE, 0); + + /* set up the widgets for the main,configure and add modes */ + set_up_main_widgets (news, main_container); + set_up_configure_widgets (news, main_container); + set_up_add_widgets (news, main_container); + + /* set up the font */ + news->font = eel_scalable_font_get_default_font (); + + /* default to the main mode */ + gtk_widget_show (main_container); + gtk_widget_show_all (news->main_box); + + /* load some images */ + nautilus_news_load_images (news); + + /* populate the configuration list */ + add_channels_to_configure_list (news); + + /* set up the update timeout */ + news->timer_task = gtk_timeout_add (10000, check_for_updates, news); + + /* Create the nautilus view CORBA object. */ + news->view = nautilus_view_new (main_container); + gtk_signal_connect (GTK_OBJECT (news->view), "destroy", do_destroy, news); + + gtk_signal_connect (GTK_OBJECT (news->view), "load_location", + nautilus_news_load_location, news); + + /* allocate a property bag to reflect the TAB_IMAGE property */ + news->property_bag = bonobo_property_bag_new (get_bonobo_properties, set_bonobo_properties, news); + bonobo_control_set_properties (nautilus_view_get_bonobo_control (news->view), news->property_bag); + bonobo_property_bag_add (news->property_bag, "tab_image", TAB_IMAGE, BONOBO_ARG_STRING, NULL, + "image indicating that the news has changed", 0); + + /* read the channel definition file and start loading the channels */ + read_channel_list (news); + + /* return the nautilus view */ + return news->view; +} + +int +main(int argc, char *argv[]) +{ + /* Make criticals and warnings stop in the debugger if NAUTILUS_DEBUG is set. + * Unfortunately, this has to be done explicitly for each domain. + */ + if (g_getenv("NAUTILUS_DEBUG") != NULL) { + eel_make_warnings_and_criticals_stop_in_debugger + (G_LOG_DOMAIN, g_log_domain_glib, "Gdk", "Gtk", "GnomeVFS", "GnomeUI", "Bonobo", NULL); + } + + return nautilus_view_standard_main ("nautilus-news", + VERSION, + PACKAGE, + GNOMELOCALEDIR, + argc, + argv, + "OAFIID:nautilus_news_view_factory:041601", + "OAFIID:nautilus_news_view:041601", + make_news_view, + nautilus_global_preferences_initialize, + NULL); +} diff --git a/components/news/news_bullet.png b/components/news/news_bullet.png Binary files differnew file mode 100644 index 000000000..22d2ffdbc --- /dev/null +++ b/components/news/news_bullet.png diff --git a/components/news/news_channels.xml b/components/news/news_channels.xml new file mode 100644 index 000000000..e56f3942f --- /dev/null +++ b/components/news/news_channels.xml @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<rss_news_channels> + <rss_channel name="Beyond 2000" uri="http://beyond2000.com/b2k.rdf" show="false" open="false"/> + <rss_channel name="CNN" uri="http://www.cnn.com/cnn.rss" show="false" open="false"/> + <rss_channel name="DVD Review" uri="http://www.dvdreview.com/rss/newschannel.rss" show="false" open="false"/> + <rss_channel name="Freshmeat" uri="http://freshmeat.net/backend/fm.rdf" show="false" open="false"/> + <rss_channel name="Kuro5hin" uri="http://www.kuro5hin.org/backend.rdf" show="false" open="false"/> + <rss_channel name="Linux Planet" uri="http://www.linuxplanet.com/rss" show="false" open="false"/> + <rss_channel name="Linux Today" uri="http://linuxtoday.com/backend/my-netscape.rdf" show="true" open="true"/> + <rss_channel name="Macintosh News" uri="http://www.macnn.com/macnn10.rdf" show="false" open="false"/> + <rss_channel name="Marijuana News" uri="http://my.marijuana.com/backend.php3" show="false" open="false"/> + <rss_channel name="Morons" uri="http://morons.org/morons.rss" show="false" open="false"/> + <rss_channel name="The Motley Fool" uri="http://www.fool.com/about/headlines/rss_headlines.asp" show="false" open="false"/> + <rss_channel name="Newsforge" uri="http://www.newsforge.com/newsforge.rss" show="false" open="false"/> + <rss_channel name="Nanotechnology News" uri="http://www.nanotechnews.com/nano/rdf" show="false" open="false"/> + <rss_channel name="Pigdog" uri="http://www.pigdog.org/pigdog.rdf" show="false" open="false"/> + <rss_channel name="Quotes of the Day" uri="http://www.quotationspage.com/data/mqotd.rss" show="false" open="false"/> + <rss_channel name="The Register" uri="http://www.theregister.co.uk/tonys/slashdot.rdf" show="false" open="false"/> + <rss_channel name="Root Prompt" uri="http://www.rootprompt.org/rss" show="false" open="false"/> + <rss_channel name="Salon" uri="http://www.salon.com/feed/RDF/salon_use.rdf" show="true" open="false"/> + <rss_channel name="Scripting News" uri="http://www.scriptingnews.userland.com/xml/scriptingnews2.xml" show="false" open="false"/> + <rss_channel name="Slashdot" uri="http://www.slashdot.org/slashdot.rdf" show="true" open="true"/> + <rss_channel name="Wired" uri="http://www.wired.com/news_drop/netcenter/netcenter.rdf" show="true" open="false"/> + <rss_channel name="Zope" uri="http://www.zope.org/SiteIndex/news.rss" show="false" open="false"/> +</rss_news_channels> diff --git a/components/news/pixmaps.h b/components/news/pixmaps.h new file mode 100644 index 000000000..8cc7b4c26 --- /dev/null +++ b/components/news/pixmaps.h @@ -0,0 +1,55 @@ +/* XPM images used by the news sidebar panel */ + +static char *triangle_xpm[] = { +/* columns rows colors chars-per-pixel */ +"12 12 11 1", +" c None", +". c #FFFFFF", +"+ c #000000", +"@ c #E5E5E5", +"# c #929292", +"$ c #D0D0D0", +"% c #6D6D6D", +"& c #BBBCBC", +"* c #888888", +"= c #BBBBBB", +"- c #A7A7A7", +"....+.......", +"....++......", +"....+@+.....", +"....+@#+....", +"....+@##+...", +"....+$##%+..", +"....+&#%+*=.", +"....+-%+*=..", +"....+#+*=...", +"....++*=....", +"....+*=.....", +".....=......"}; + +/* XPM */ +static char *open_triangle_xpm[] = { +"12 12 11 1", +" c None", +". c #FFFFFF", +"+ c #000000", +"@ c #E5E5E5", +"# c #D0D0D0", +"$ c #BBBCBC", +"% c #A7A7A7", +"& c #929292", +"* c #888888", +"= c #BBBBBB", +"- c #6D6D6D", +"............", +"............", +"............", +"+++++++++++.", +".+@@@#$%&+*=", +"..+&&&&-+*=.", +"...+&&-+*=..", +"....+-+*=...", +".....+*=....", +"......=.....", +"............", +"............"}; diff --git a/configure.in b/configure.in index 6e6267228..8ad9f41c2 100644 --- a/configure.in +++ b/configure.in @@ -938,6 +938,7 @@ components/help/converters/gnome-info2html2/Makefile components/help/converters/gnome-man2html2/Makefile components/image-viewer/Makefile components/music/Makefile +components/news/Makefile components/notes/Makefile components/sample/Makefile components/mozilla/Makefile |