/* * gpe-appmgr - a program launcher * * Copyright (c) 2004 Phil Blundell * * 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, 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. */ #include #include #include #include #include /* Gtk etc. */ #include #include #include #include #include /* I/O */ #include /* malloc / free */ #include #include /* time() */ #include /* i18n */ #include #include /* GPE */ #include #include #include #include #include #include #include /* everything else */ #include "main.h" #include #include #include #include //#define DEBUG #ifdef DEBUG #define DBG(x) {fprintf x ;} #define TRACE(x) {fprintf(stderr,"TRACE: " x "\n");} #else #define TRACE(x) ; #define DBG(x) ; #endif GtkWidget *programs_fixed; GdkPixbuf *background; GtkWidget *toplevel; GdkPixbuf *shutdown_pixbuf, *shutdown_down_pixbuf; gboolean shutdown_is_down; GtkWidget *tab_notebook; static GSList *views; static gchar *background_filename; static GSList *rows, *labels; static GtkWidget *tabstrip_widget; #define DEFAULT_BACKGROUND PREFIX "/share/pixmaps/gpe/default/gpe-appmgr/desktop-background.png" /* Rows start clear of the Psion blockletter watermark that runs down the left edge of the desktop background. Measured off Pulster's photograph of the real machine: the row boxes begin at x~99 on an 800px screen, with the rotated category label sitting in the LABEL_WIDTH to their left. Upstream GPE uses 52, which puts the rows over the watermark. */ #define ICONLIST_XOFFSET 104 #define LABEL_WIDTH 24 /* The vertical tab strip down the right-hand edge: TODAY, PROGRAMS, TASKS. * * Psion's own gpe-appmgr (CVS tag GPE_APPMGR_56) drew these and rendered the * TODAY and TASKS pages as HTML through gpe-helpviewer; that module did not * survive. TODAY selects gpe-today, which is the summary screen the * photographs show. TASKS is NOT the running-program list: Psion's * add-backup.patch shows tasks.html was a page of things to do -- back up, * connect, get help -- so it runs psion-tasks.sh, which offers Psion's own * task scripts. PROGRAMS is the row view already on screen. * * Width and placement are measured from Psion's photographs of the machine: * a gold strip about 35px wide running the full height of the desktop, with * the three tabs stacked at the top. The labels read top to bottom, i.e. * rotated the opposite way to the category labels down the left. */ #define TABSTRIP_WIDTH 26 /* 3 border columns + 23 of tab face */ #define EDGE_COLS 3 #define TAB_SURFACE (TABSTRIP_WIDTH - EDGE_COLS) #define TAB_HEIGHT 101 #define TAB_SLANT 6 /* the tabs are cut on a diagonal */ #define POWER_SIZE 20 /* the red power button at the strip's foot */ #define POWER_GAP 6 /* its clearance above the dock */ /* All three measured off psionlx-today-screen.jpeg after rectifying it to 800x600 through a homography on the display's corners. The rectification is calibrated on both axes by things whose size is already known: the dock buttons come back 63-64px against a true 64 (1.6%), and the dock itself 46px against a true 48 (4.3%). In that frame the strip runs x=777..799, so 23 wide. Scanning down the columns just inside the page edge gives the joins at y=96, 192 and 291 -- a pitch of 97, or 101 once the vertical scale is corrected. The same joins seen from the columns just inside the outer edge sit at 184 and 282, 8-9px higher, which is the diagonal cut: 2*TAB_SLANT across the width. The previous values (20, 130, 7) came from treating the strip as a 20px ruler, which was itself the thing being assumed. Measuring against the dock instead removes that circularity. */ enum { VIEW_TODAY, VIEW_PROGRAMS, VIEW_TASKS }; static const struct { const char *label; const char *exec; /* NULL == drawn in place, not launched */ } tabstrip[] = { { "TODAY", NULL }, { "PROGRAMS", NULL }, { "TASKS", "psion-tasks.sh" }, }; #define N_TABS (sizeof (tabstrip) / sizeof (tabstrip[0])) /* Which tab is selected. The photographs show the Today screen drawn on the desktop itself -- the Psion watermark is still visible down the left while Today is showing -- so these are views inside gpe-appmgr, not separate windows, and the tabs switch between them in place. */ static int current_view = -1; /* set from the preference on first use */ /* "Show this screen at startup" is a real preference, not decoration: the photograph shows the Today screen as what the machine sits on, and the checkbox is how you turn that off. Kept as a one-byte file beside the other ~/.gpe state rather than in the xsettings database, because gpe-appmgr already reads that directory directly for the Today lines. */ static const char * today_pref_path (void) { static char path[512]; const char *home = getenv ("HOME"); if (!home) home = "/home/lx"; snprintf (path, sizeof (path), "%s/.gpe/show-today", home); return path; } static gboolean today_at_startup (void) { FILE *fp = fopen (today_pref_path (), "r"); int c; if (!fp) return TRUE; /* on by default, as Psion shipped it */ c = fgetc (fp); fclose (fp); return c != '0'; } static void set_today_at_startup (gboolean on) { char dir[512]; const char *home = getenv ("HOME"); FILE *fp; if (!home) home = "/home/lx"; snprintf (dir, sizeof (dir), "%s/.gpe", home); mkdir (dir, 0700); fp = fopen (today_pref_path (), "w"); if (!fp) return; fputc (on ? '1' : '0', fp); fclose (fp); } static void today_startup_toggled (GtkToggleButton *b, gpointer data) { set_today_at_startup (gtk_toggle_button_get_active (b)); } static void refresh_callback (void); #define N_(x) (x) GdkGC *bg_gc; void row_view_set_bg_colour (guint c) { GSList *v; for (v = views; v; v = v->next) { gpe_icon_list_view_set_bg_color (GPE_ICON_LIST_VIEW (v->data), c); gtk_widget_queue_draw (GTK_WIDGET (v->data)); } } void row_view_refresh_background (void) { GSList *ll; for (ll = rows; ll; ll = ll->next) gpe_icon_list_view_set_bg_pixmap (GPE_ICON_LIST_VIEW (ll->data), background); if (toplevel) gtk_widget_queue_draw (toplevel); } void row_view_set_background (gchar *path) { GdkPixbuf *pix; int width, height; if (background) g_object_unref (background); if (background_filename) g_free (background_filename); if (path == NULL || path[0] == 0) path = DEFAULT_BACKGROUND; background_filename = g_strdup (path); pix = gdk_pixbuf_new_from_file (background_filename, NULL); if (!pix) pix = gdk_pixbuf_new_from_file (DEFAULT_BACKGROUND, NULL); gdk_drawable_get_size (toplevel->window, &width, &height); background = gdk_pixbuf_scale_simple (pix, width, height, GDK_INTERP_BILINEAR); row_view_refresh_background (); } static void run_callback (GObject *obj, GdkEventButton *ev, GnomeDesktopFile *p) { run_package (p, obj); } /* Vertical category label for the left-hand strip (OFFICE, INTERNET, PIM...). * * The upstream version drew this with cairo 1.0; the netBook Pro rootfs has * cairo 0.x, whose API predates cairo_set_source_rgb and friends. An earlier * attempt here used a rotated Pango matrix, which depends on gdk_draw_layout * honouring the context matrix. This version avoids that entirely: the text is * drawn horizontally onto a scratch pixmap, lifted into a pixbuf, and rotated * 90 degrees with gdk_pixbuf_rotate_simple, which GTK 2.6 provides. The * scratch pixmap is given an explicit colormap, without which drawing onto a * depth -1 pixmap can silently fail. */ static GtkWidget * make_label (char *text, int height) { GdkPixmap *pixmap, *scratch; GdkPixbuf *buf, *rot; GdkGC *gc, *sgc; GdkColormap *cmap; GdkColor white = { 0, 0xffff, 0xffff, 0xffff }; GdkColor rule = { 0, 0xa5a5, 0xaeae, 0xbfbf }; /* Measured off psion-netbook-pro-linux-psionlx_1.jpg, calibrated against the page white and a known-black icon caption in the same frame (that control comes back #101115, so the calibration is sound). The category labels are a dark navy, not the pure blue this used to draw. */ GdkColor blue = { 0, 0x0d0d, 0x2a2a, 0x4949 }; GdkColor band = { 0, 0xc2c2, 0xdede, 0xf1f1 }; PangoLayout *layout; PangoFontDescription *fd; gchar *up; int tw, th; if (programs_fixed == NULL || programs_fixed->window == NULL) return gtk_label_new (text); cmap = gdk_drawable_get_colormap (programs_fixed->window); gdk_colormap_alloc_color (cmap, &white, FALSE, TRUE); gdk_colormap_alloc_color (cmap, &rule, FALSE, TRUE); gdk_colormap_alloc_color (cmap, &blue, FALSE, TRUE); gdk_colormap_alloc_color (cmap, &band, FALSE, TRUE); /* scratch is laid out the long way round, then rotated */ scratch = gdk_pixmap_new (programs_fixed->window, height, LABEL_WIDTH, -1); gdk_drawable_set_colormap (scratch, cmap); sgc = gdk_gc_new (scratch); /* The band behind the category label is a light blue, not white. In the Pulster frame it is both darker and bluer than the box beside it -- measured against that box, per channel, 0.80 / 0.92 / 0.99, which off a #f2f2f2 box gives this. Drawn white it was the brightest thing on the page; in the photograph it is the darkest of band, box and desktop. */ gdk_gc_set_foreground (sgc, &band); gdk_draw_rectangle (scratch, sgc, TRUE, 0, 0, height, LABEL_WIDTH); /* Both photographs set these in caps -- OFFICE, INTERNET, PIM, OTHER -- where the category names arrive here in mixed case. The caps string is used for the whole of the rest of this function, not just to measure with: the glyphs go down one at a time below and walking the original would have drawn mixed case at the caps width. */ up = g_utf8_strup (text, -1); layout = gtk_widget_create_pango_layout (programs_fixed, up); fd = pango_font_description_from_string ("Sans Bold 7"); pango_layout_set_font_description (layout, fd); pango_layout_get_pixel_size (layout, &tw, &th); gdk_gc_set_foreground (sgc, &blue); { /* Measured off the Pulster frame, which is close to 1:1: "OFFICE" runs 39 pixels for six characters, letter pitch 6-7 and gaps of 1-2, with a cap height of 8. The earlier reading of 74 pixels came through the 20px-strip ruler that turned out to be wrong, and drove both the font and the tracking far too large -- this used to set it in Sans 10 at a cap height of 11, half as long again as the photograph. It is BOLD. Measuring ink coverage inside the band -- the fraction of pixels below 80% of the band's own background -- the photograph comes to 17.3% against 8.4% for the same string set in the regular weight at the same cap height. Twice the ink for the same size is the stroke weight, not the size. Bold also sets a little wider, which is why the tracking goes to nothing here: regular at track 1 ran 42 pixels where the photograph runs 39. Bold 8 came out at 43 against the photograph's 39 (about 41 once its 0.96 scale is allowed for), so a size under: Bold 7. Pango's letter-spacing attribute is 1.6 and newer and the device has 1.4, so the glyphs are drawn one at a time with the gap added. */ const int track = 0; const char *p = up; int total = tw + track * (g_utf8_strlen (up, -1) - 1); int x = (height - total) / 2; int y = (LABEL_WIDTH - th) / 2; while (p && *p) { const char *next = g_utf8_next_char (p); char ch[8]; int n = next - p, cw, chh; memcpy (ch, p, n); ch[n] = 0; pango_layout_set_text (layout, ch, -1); pango_layout_get_pixel_size (layout, &cw, &chh); gdk_draw_layout (scratch, sgc, x, y, layout); x += cw + track; p = next; } pango_layout_set_text (layout, up, -1); } g_free (up); buf = gdk_pixbuf_get_from_drawable (NULL, scratch, cmap, 0, 0, 0, 0, height, LABEL_WIDTH); rot = gdk_pixbuf_rotate_simple (buf, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE); pixmap = gdk_pixmap_new (programs_fixed->window, LABEL_WIDTH, height, -1); gdk_drawable_set_colormap (pixmap, cmap); gc = gdk_gc_new (pixmap); gdk_draw_pixbuf (pixmap, gc, rot, 0, 0, 0, 0, LABEL_WIDTH, height, GDK_RGB_DITHER_NONE, 0, 0); /* the rule down three sides, as the original drew it */ gdk_gc_set_foreground (gc, &rule); gdk_draw_line (pixmap, gc, LABEL_WIDTH - 1, 0, 0, 0); gdk_draw_line (pixmap, gc, 0, 0, 0, height - 1); gdk_draw_line (pixmap, gc, 0, height - 1, LABEL_WIDTH - 1, height - 1); pango_font_description_free (fd); g_object_unref (layout); g_object_unref (buf); g_object_unref (rot); g_object_unref (sgc); g_object_unref (gc); g_object_unref (scratch); return gtk_image_new_from_pixmap (pixmap, NULL); } /* ---------------------------------------------------------------- TODAY -- * * Psion's Today screen, rebuilt from the fss_88 photograph. Their own code * was in gpe-appmgr CVS tag 56 and does not survive -- none of the screen's * strings appear in any surviving source or binary, which was checked * exhaustively -- so this is a reimplementation of what the photograph * shows, not a recovery of the original. * * What the photograph establishes: a right-hand panel over the ordinary * desktop (the Psion watermark is still visible down the left while Today * is showing, which is why this is a view in gpe-appmgr rather than a * separate window); a date heading with a rule beneath it; right-aligned * status lines, three of them carrying a small icon; and a "Show this * screen at startup" checkbox at the foot. * * The statuses are read from the real system, not hard-coded, so the screen * says what is actually true of the machine. */ #define TODAY_RIGHT (TABSTRIP_WIDTH + 4) /* the block runs up to the strip */ /* Measured off the photograph using the tab strip as a local scale -- the strip is a known 20 screen pixels wide, so its width in the image gives the pixels-per-pixel at that height and sidesteps the perspective. The Today block is NARROW, tucked against the strip, and its type is small: the date sets in about 104 screen pixels for 25 characters, roughly a 7 pixel font, where this used to draw it at 10. */ #define PANEL_HEIGHT 48 /* matchbox-panel --size 48 */ /* 625 was taken from the photograph's proportions but clipped the longest line -- "Not connected to the Internet" sets in about 155px here against a 151px block. The text has to fit, so the rule moves back out far enough to hold it with a margin. */ #define TODAY_LEFT_RULE 588 #define TODAY_TOP 0 /* the date band sits at the very top */ #define TODAY_LINE 22 #define TODAY_FONT "Sans 7" static GSList *today_widgets; static int today_bottom; static gboolean file_has_line_beyond (const char *path, int header_lines) { FILE *fp = fopen (path, "r"); char buf[512]; int n = 0; if (!fp) return FALSE; while (fgets (buf, sizeof buf, fp)) if (++n > header_lines) { fclose (fp); return TRUE; } fclose (fp); return FALSE; } static gboolean have_default_route (void) { FILE *fp = fopen ("/proc/net/route", "r"); char iface[64], dest[64]; char line[512]; if (!fp) return FALSE; if (!fgets (line, sizeof line, fp)) { fclose (fp); return FALSE; } while (fgets (line, sizeof line, fp)) if (sscanf (line, "%63s %63s", iface, dest) == 2 && !strcmp (dest, "00000000")) { fclose (fp); return TRUE; } fclose (fp); return FALSE; } static gboolean db_has_content (const char *rel) { char path[512]; struct stat st; snprintf (path, sizeof path, "%s/%s", g_get_home_dir (), rel); /* an empty sqlite db is a bare page or two; anything larger holds rows */ return stat (path, &st) == 0 && st.st_size > 20480; } static void today_font (GtkWidget *w) { PangoFontDescription *fd = pango_font_description_from_string (TODAY_FONT); gtk_widget_modify_font (w, fd); pango_font_description_free (fd); } static GtkWidget * today_line (const char *text, const char *icon_file) { GtkWidget *box = gtk_hbox_new (FALSE, 4); GtkWidget *lab = gtk_label_new (text); gtk_misc_set_padding (GTK_MISC (lab), 4, 0); GdkPixbuf *pix = NULL; today_font (lab); /* Load straight from a path. gpe_find_icon() must NOT be used for a name that is not in this program's my_icons table -- libgpewidget puts up an error box and calls exit(1), which takes the whole desktop down. */ if (icon_file) { char path[512]; snprintf (path, sizeof path, PREFIX "/share/pixmaps/%s", icon_file); pix = gdk_pixbuf_new_from_file (path, NULL); if (pix) { GdkPixbuf *small = gdk_pixbuf_scale_simple (pix, 16, 16, GDK_INTERP_BILINEAR); if (small) { g_object_unref (pix); pix = small; } } } gtk_misc_set_alignment (GTK_MISC (lab), 1.0, 0.5); /* pack from the right so the icon ends up beside its text rather than stranded at the far left of a full-width row, as it must to match */ gtk_box_pack_end (GTK_BOX (box), lab, FALSE, FALSE, 0); if (pix) { GtkWidget *img = gtk_image_new_from_pixbuf (pix); gtk_box_pack_end (GTK_BOX (box), img, FALSE, FALSE, 0); g_object_unref (pix); } return box; } static void destroy_today (void) { GSList *l; for (l = today_widgets; l; l = l->next) gtk_widget_destroy (l->data); g_slist_free (today_widgets); today_widgets = NULL; } static void build_today (void) { int w = programs_fixed->allocation.width; int h = programs_fixed->allocation.height; int right = w - TODAY_RIGHT; int width = right - TODAY_LEFT_RULE; int y = TODAY_TOP; if (width < 110 || h < 120) /* not allocated yet */ return; char datebuf[128]; time_t now = time (NULL); struct tm *tm = localtime (&now); GtkWidget *wgt; int i; struct { const char *text; const char *icon; } lines[6]; strftime (datebuf, sizeof datebuf, "%A %d %B %Y", tm); /* How much of the container is actually on screen. programs_fixed is sized for the PROGRAMS rows, which are taller than the display, so its own allocation is no guide to where the bottom of the screen is. */ { /* gpe-appmgr runs with -d, so its window covers the whole root and the panel is drawn on top of it: the usable bottom is the screen height less the panel, not the window height. Putting the checkbox at the window's foot hid it behind the dock. */ int screen = gdk_screen_height (); int usable = screen - PANEL_HEIGHT; int vis = (toplevel && toplevel->allocation.height > 120) ? MIN (toplevel->allocation.height, usable) : usable; GtkWidget *rule; /* The rule down the left of the whole block, which the photograph shows running the full height rather than only under the date. */ rule = gtk_vseparator_new (); gtk_widget_set_usize (rule, 2, vis - 8); gtk_fixed_put (GTK_FIXED (programs_fixed), rule, TODAY_LEFT_RULE - 12, 4); today_widgets = g_slist_prepend (today_widgets, rule); today_bottom = vis; } /* The date sits in a band across the top, very slightly tinted against the page: measured at about 5% down in red and a shade cool, which is the "slight tint" the photograph shows. */ { GtkWidget *band = gtk_event_box_new (); GdkColor tint = { 0, 0xeded, 0xf1f1, 0xf5f5 }; gtk_widget_modify_bg (band, GTK_STATE_NORMAL, &tint); wgt = gtk_label_new (datebuf); today_font (wgt); gtk_misc_set_alignment (GTK_MISC (wgt), 1.0, 0.5); gtk_misc_set_padding (GTK_MISC (wgt), 6, 2); gtk_container_add (GTK_CONTAINER (band), wgt); gtk_widget_set_usize (band, width, 20); gtk_fixed_put (GTK_FIXED (programs_fixed), band, TODAY_LEFT_RULE, y); today_widgets = g_slist_prepend (today_widgets, band); } y += 20; /* a double rule under the band, as the photograph shows */ wgt = gtk_hseparator_new (); gtk_widget_set_usize (wgt, width, 2); gtk_fixed_put (GTK_FIXED (programs_fixed), wgt, TODAY_LEFT_RULE, y); today_widgets = g_slist_prepend (today_widgets, wgt); y += 5; wgt = gtk_hseparator_new (); gtk_widget_set_usize (wgt, width, 2); gtk_fixed_put (GTK_FIXED (programs_fixed), wgt, TODAY_LEFT_RULE, y); today_widgets = g_slist_prepend (today_widgets, wgt); y += 19; lines[0].text = file_has_line_beyond ("/proc/net/wireless", 2) ? "WiFi card present" : "WiFi card not present"; lines[0].icon = NULL; lines[1].text = have_default_route () ? "Connected to the Internet" : "Not connected to the Internet"; lines[1].icon = NULL; lines[2].text = "Inbox not available"; lines[2].icon = "email.png"; lines[3].text = "Outbox not available"; lines[3].icon = NULL; lines[4].text = db_has_content (".gpe/calendar") ? "You have appointments" : "No appointments"; lines[4].icon = "calendar.png"; lines[5].text = db_has_content (".gpe/todo") ? "To-do list has entries" : "To-do list is empty"; lines[5].icon = "gpe-todo.png"; for (i = 0; i < 6; i++) { wgt = today_line (lines[i].text, lines[i].icon); gtk_widget_set_usize (wgt, width, -1); gtk_fixed_put (GTK_FIXED (programs_fixed), wgt, TODAY_LEFT_RULE, y); today_widgets = g_slist_prepend (today_widgets, wgt); y += TODAY_LINE; if (i == 1 || i == 3) y += 8; /* the photograph groups them in pairs */ } /* "Show this screen at startup", beneath the status lines */ wgt = gtk_check_button_new_with_label ("Show this screen at startup"); today_font (GTK_BIN (wgt)->child); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (wgt), today_at_startup ()); g_signal_connect (G_OBJECT (wgt), "toggled", G_CALLBACK (today_startup_toggled), NULL); /* At the foot of the visible area, which is where the photograph puts it, measured from the toplevel rather than from programs_fixed -- that container is sized for the PROGRAMS rows and is taller than the screen, which is what put this off the bottom before. */ gtk_fixed_put (GTK_FIXED (programs_fixed), wgt, TODAY_LEFT_RULE + 4, (today_bottom > y + 60) ? today_bottom - 42 : y + 26); today_widgets = g_slist_prepend (today_widgets, wgt); { GSList *l; for (l = today_widgets; l; l = l->next) gtk_widget_show_all (l->data); } } /* Draw the whole right-hand strip as a single image: gold ground, the three * tabs stacked at the top, the rest plain. Laid out horizontally on a * scratch pixmap and rotated clockwise, which is the same trick make_label * uses -- see the comment there for why Pango matrix rotation is avoided. */ static GtkWidget * make_tabstrip (int height) { GdkPixmap *pixmap, *scratch; GdkPixbuf *buf, *rot; GdkGC *gc, *sgc; GdkColormap *cmap; /* Measured off psionlx-today-screen.jpeg with the lighting flattened -- every row of the photograph normalised by the page white at the same height, which removes the camera's falloff and leaves the interface's own shading. What survives that: - the plain strip below the tabs is the brightest gold - an unselected tab is a little darker than the strip - the SELECTED tab is darker again and browner. This code used to draw it LIGHTER, which was backwards - the divisions are pale highlight lines, not dark ones - the tabs are shaded across their width, brighter at the page edge The selected tab sits in front of its neighbours the way a file-folder tab does and drops a small shadow onto each of them, and the group as a whole stands proud of the plain bar below, which carries a shadow at that edge too. Absolute values off a hand-held photograph of a glossy panel are only good to a few percent; these relationships are what carry the look. */ GdkColor ink = { 0, 0x3a3a, 0x3232, 0x1c1c }; struct { int r, g, b; } ground = { 0xf0, 0xc7, 0x4a }, unsel = { 0xe6, 0xbf, 0x58 }, sel = { 0xe2, 0xbc, 0x58 }, /* The strip's page-side border, measured in the rectified photograph and corrected for its 85% exposure (page white reads 216 where it is 255): a white lip, then a shadow. The shadow is much deeper along the selected tab, because that tab is the one sunk below the surface. */ edge_lip = { 0xff, 0xff, 0xff }, edge_mid = { 0xb6, 0xc2, 0xcc }, edge_shade = { 0xb9, 0xb1, 0xa9 }, edge_deep = { 0x86, 0x82, 0x7b }; PangoLayout *layout; PangoFontDescription *fd; int i, y, tw, th, usable; const int W = TABSTRIP_WIDTH; if (programs_fixed == NULL || programs_fixed->window == NULL) return NULL; cmap = gdk_drawable_get_colormap (programs_fixed->window); gdk_colormap_alloc_color (cmap, &ink, FALSE, TRUE); scratch = gdk_pixmap_new (programs_fixed->window, height, W, -1); gdk_drawable_set_colormap (scratch, cmap); sgc = gdk_gc_new (scratch); /* The scratch is laid out the long way round and rotated clockwise at the end, so y here runs across the width of the finished strip. */ #define SHADE(c, pc) \ do { \ GdkColor _c; \ int _n = (pc); \ _c.red = MIN (255, (c).r * _n / 100) * 257; \ _c.green = MIN (255, (c).g * _n / 100) * 257; \ _c.blue = MIN (255, (c).b * _n / 100) * 257; \ gdk_gc_set_rgb_fg_color (sgc, &_c); \ } while (0) /* A lit edge does not just scale the gold. In the photograph the seam highlight measures 25-34% saturation where the tab body is 45-55%, and its absolute luminance is about the page white -- it is a cream line. Scaling the base colour by a percentage keeps the saturation and merely clips red and green, which gives a vivid yellow instead. Blend toward white. */ #define SHADE_LIT(c, num, w) \ do { \ GdkColor _c; \ int _r = MIN (255, (c).r * (num) / 100); \ int _g = MIN (255, (c).g * (num) / 100); \ int _b = MIN (255, (c).b * (num) / 100); \ _r += (255 - _r) * (w) / 100; \ _g += (255 - _g) * (w) / 100; \ _b += (255 - _b) * (w) / 100; \ _c.red = _r * 257; _c.green = _g * 257; _c.blue = _b * 257; \ gdk_gc_set_rgb_fg_color (sgc, &_c); \ } while (0) #define BOUND(k, yy) ((k) * TAB_HEIGHT - TAB_SLANT \ + (2 * TAB_SLANT * (yy)) / (W - 1)) /* Coordinates. The scratch is (height x W) and is rotated CLOCKWISE at the end, which maps scratch (x, y) to final (W-1-y, x). So: scratch y = W-1 -> final x = 0 the PAGE edge (left) scratch y = 0 -> final x = W-1 the OUTER edge (right) scratch x -> final y down the strip With the light coming from the top left, a tab that is pressed INTO the strip carries its shadow on the top and left inner edges and its highlight on the bottom and right; a tab standing proud of the strip is the other way round. Drawing the selected tab merely darker, which is what this did before, reads as a different colour rather than as a depression. */ for (y = 0; y < W; y++) { int page = (W - 1 - y); /* 0 at the page edge, W-1 at the outer */ int surf = page - EDGE_COLS; /* 0..TAB_SURFACE-1 across the tab face */ double t = (double) page / (W - 1); int k, d, ground_num; /* What marks the selected tab is not its colour but the DIRECTION of its shading, and the two edges of the strip say the same thing. Measured off a high-resolution crop, sampled perpendicular to the strip's axis (it sits about 7.7 degrees off vertical) with the 85th percentile taken down each column so the lettering is excluded, as a percentage of each tab's own body: page edge ................ outer edge selected 91 (in shadow) ... 115 (bright lip) raised 109 (bright lip) ... 92 (falls away) A raised tab is lit from the page side and falls away. The selected one is lit from the outer side: it is a trough, so its near wall is in shadow and its far wall catches the light. Both span about the same range, and their means differ by only 1.5% -- the fill is the same gold, and the flipped gradient is the whole of the effect. These are read off the rectified frame, where the face occupies columns 777..799 exactly, rather than off the high-resolution crop at an assumed offset -- that put the first sample inside the border shadow and drew the leading column of the face 20% too dark. Three earlier readings were wrong in other ways. Measuring between the edges of the gold run put one sample on a boundary line, which cancelled the selected tab's ramp and made it read flat. A 21-pixel crop gave a wild 54-62%, both ends being off the tab entirely. And sampling only the middle missed the lips at both edges, which are the most telling part. */ static const int pressed_profile[TAB_SURFACE] = { 89, 94, 93, 93, 93, 94, 97, 101, 100, 101, 103, 104, 106, 104, 104, 102, 103, 103, 103, 101, 99, 98, 113 }; static const int raised_profile[TAB_SURFACE] = { 110, 107, 105, 101, 101, 101, 101, 104, 104, 103, 103, 102, 103, 103, 100, 100, 98, 97, 95, 93, 93, 98, 82 }; int raised_num, pressed_num; ground_num = (int) (102.0 + 4.0 * cos (t * 1.6) - 5.0 * t); /* The three border columns on the page side. The lip is white for its whole length; the shadow under it deepens where it runs along the selected tab, and starts a column earlier there. */ if (surf < 0) { int lo = MAX (0, BOUND (current_view, y)); int hi = MIN (height, BOUND (current_view, y) + TAB_HEIGHT); SHADE (page == 2 ? edge_shade : edge_lip, 100); if (lo > 0) gdk_draw_line (scratch, sgc, 0, y, lo - 1, y); if (hi < height) gdk_draw_line (scratch, sgc, hi, y, height - 1, y); SHADE (page == 0 ? edge_lip : (page == 1 ? edge_mid : edge_deep), 100); if (hi > lo) gdk_draw_line (scratch, sgc, lo, y, hi - 1, y); continue; } raised_num = raised_profile[surf]; pressed_num = pressed_profile[surf]; SHADE (ground, ground_num); gdk_draw_line (scratch, sgc, 0, y, height - 1, y); for (k = 0; k < (int) N_TABS; k++) { int x0 = BOUND (k, y); int x1 = x0 + TAB_HEIGHT; gboolean pressed = (k == current_view); int num = pressed ? pressed_num : raised_num; if (x0 >= height) break; if (x1 > height) x1 = height; SHADE (pressed ? sel : unsel, num); gdk_draw_line (scratch, sgc, MAX (0, x0), y, x1 - 1, y); /* The ends of the tab, graded over four pixels rather than a hard two-pixel line, so the edge rolls instead of stepping. The top end is overdrawn by the seam below, which is measured; only the first tab keeps its own top, because nothing joins it there. */ for (d = 0; d < 4; d++) { int bot = x1 - 1 - d; int fall = (4 - d); /* 4,3,2,1 */ /* Only a raised tab rolls off at its foot. Brightening the SELECTED tab here put a light band immediately above the seam, and with the seam's own dark line below it the pair read as a physical gap between the two tabs. In the photograph the selected tab's gold simply runs into the seam and the white belongs to the tab below. */ if (!pressed) { SHADE (unsel, num - 4 * fall); if (bot >= 0 && bot < height) gdk_draw_point (scratch, sgc, bot, y); } if (k == 0) { int top = x0 + d; SHADE (pressed ? sel : unsel, pressed ? num - 4 * fall : num + 4 * fall); if (top >= 0 && top < height) gdk_draw_point (scratch, sgc, top, y); } } } /* The seams, drawn after every tab body so nothing overwrites them. There are two of them, and they are not the same. Measured on the seam's own centre line (averaging whole columns smears it, because the seam moves about two rows across five columns and the average of a moving dark line is a shallow wide one), as a percentage of the body of the tab BELOW the seam: -1 0 +1 +2 +3 +4 between raised tabs, page 78 70 98 113 103 99 outer 67 62 87 116 110 101 below the SELECTED tab, pg 88 84 96 94 103 115 outer 114 96 101 99 98 102 So a seam between two coplanar tabs is a real cut: down to 70% at the page edge and 62% at the outer, deepening and widening as it goes, with a bright lip just past it. Where the tab above is the selected one there is no cut at all -- only a shallow 84% dip and then a white edge at 115%, which is the lower tab standing proud of the depression rather than being shadowed by it. Drawing one seam for both cases, which is what this did before, loses that distinction entirely. The white under the selected tab is strong. Comparing the render against the photograph column by column, anchored on the same join line in both and taken as a percentage of each column's own local body, the photograph peaks at 117-130% right across the face while an earlier pass here reached only 100-111%. The troughs already agreed to within five points, so it was the highlight alone that was missing -- and without it the boundary reads as a dark line rather than as the lower tab standing proud. */ for (k = 1; k <= (int) N_TABS; k++) { static const int plain_in [6] = { 78, 70, 98, 113, 103, 99 }; static const int plain_out[6] = { 67, 62, 87, 116, 110, 101 }; /* 0 = leave the pixel alone. There is NO dark line above the white one: in the photograph the selected tab's gold runs straight into the lit edge, and the darkest point anywhere near the join sits three to five pixels higher, which is just the tab's own shaded body. Dipping here drew a dark line against the white and broke the depressed read. */ static const int sel_in [6] = { 0, 116, 122, 110, 103, 100 }; static const int sel_out [6] = { 0, 124, 130, 114, 105, 101 }; int x0 = BOUND (k, y); gboolean below_sel = (k - 1 == current_view); gboolean last = (k == (int) N_TABS); gboolean lower_pressed = (!last && k == current_view); const int *in = below_sel ? sel_in : plain_in; const int *out = below_sel ? sel_out : plain_out; /* the plain bar under the group is ground, not a tab */ int lnum = last ? ground_num : (lower_pressed ? pressed_num : raised_num); if (x0 - 1 >= height) break; for (d = 0; d < 6; d++) { int xs = x0 - 1 + d; int pc = (in[d] * (TAB_SURFACE - 1 - surf) + out[d] * surf) / (TAB_SURFACE - 1); if (pc == 0) continue; if (pc > 100) { /* 100% -> no blend, 140% -> fully white */ int w = MIN (100, (pc - 100) * 100 / 40); SHADE_LIT (last ? ground : (lower_pressed ? sel : unsel), lnum, w); } else SHADE (last ? ground : (lower_pressed ? sel : unsel), lnum * pc / 100); if (xs >= 0 && xs < height) gdk_draw_point (scratch, sgc, xs, y); } } } #undef SHADE #undef BOUND fd = pango_font_description_from_string ("Sans 7"); for (i = 0; i < (int) N_TABS; i++) { int x0 = i * TAB_HEIGHT; if (x0 + TAB_HEIGHT > height) break; /* The tab labels are tracked apart, like the category labels are. Measured on the high-resolution crop, where 2.77 photo pixels cover one screen pixel and the gaps resolve cleanly, the space between letters is: TODAY 2.9 0.7 2.5 2.5 2.9 PROGRAMS 2.2 2.5 2.2 2.5 2.5 2.9 0.7 2.2 TASKS 3.3 3.3 2.9 3.3 so 2-3 screen pixels, against about 1 that the font gives on its own. Letter to letter the pitch is 8.7 where this set it in 6.5. Set solid the labels read as a blob, which is what made the strip look coarser than the photograph at the same pixel size. Pango's letter-spacing attribute is 1.6 and newer and the device has 1.4, so the glyphs go down one at a time with the gap added -- the same way make_label() does it. */ { const int track = 2; const char *c = tabstrip[i].label; int total, x; layout = gtk_widget_create_pango_layout (programs_fixed, tabstrip[i].label); pango_layout_set_font_description (layout, fd); pango_layout_get_pixel_size (layout, &tw, &th); total = tw + track * (g_utf8_strlen (tabstrip[i].label, -1) - 1); x = x0 + (TAB_HEIGHT - total) / 2; gdk_gc_set_foreground (sgc, &ink); while (c && *c) { const char *next = g_utf8_next_char (c); char ch[8]; int n = next - c, cw, chh; memcpy (ch, c, n); ch[n] = 0; pango_layout_set_text (layout, ch, -1); pango_layout_get_pixel_size (layout, &cw, &chh); gdk_draw_layout (scratch, sgc, x, (W - th) / 2, layout); x += cw + track; c = next; } g_object_unref (layout); } } buf = gdk_pixbuf_get_from_drawable (NULL, scratch, cmap, 0, 0, 0, 0, height, W); rot = gdk_pixbuf_rotate_simple (buf, GDK_PIXBUF_ROTATE_CLOCKWISE); pixmap = gdk_pixmap_new (programs_fixed->window, W, height, -1); gdk_drawable_set_colormap (pixmap, cmap); gc = gdk_gc_new (pixmap); gdk_draw_pixbuf (pixmap, gc, rot, 0, 0, 0, 0, W, height, GDK_RGB_DITHER_NONE, 0, 0); /* The red power button at the foot of the strip. It is in the photograph, at the bottom of the gold strip with a small gap above the dock, and nothing in GPE draws it -- gpe-logout.png is a little monitor, not this. So it is drawn here, like the rest of the strip. Drawn onto the finished pixmap rather than into `scratch`, which is laid out sideways and rotated clockwise at the end; here the glyph can be drawn upright instead of backwards. Rendered at SS times size and scaled back down, because GDK's arcs and lines are hard-edged: a 20px ring with a 2px stroke drawn directly comes out visibly blocky. The strip underneath is blown up first and used as the canvas, so the softened edges land on the right gold and follow the shading across the strip rather than sitting on a flat patch. The colour is an estimate, not a measurement. The button is about sixteen pixels across in a hand-held photograph, so JPEG blur mixes the white glyph into every red pixel of it: the darkest quarter of its reds corrects to (172, 90, 58) and the most saturated to (226, 126, 104). This sits between them. */ usable = MIN (height, gdk_screen_height () - PANEL_HEIGHT); if (usable > N_TABS * TAB_HEIGHT + POWER_SIZE + 2 * POWER_GAP) { const int SS = 4; const int S = POWER_SIZE * SS; GdkColor face = { 0, 0xcccc, 0x4444, 0x3030 }; GdkColor shade = { 0, 0xa6a6, 0x3232, 0x2222 }; GdkColor glyph = { 0, 0xffff, 0xffff, 0xffff }; int bx = (W - POWER_SIZE) / 2; int by = usable - POWER_SIZE - POWER_GAP; GdkPixbuf *bg, *big, *shot, *down; GdkPixmap *bp; GdkGC *bgc; int cx = S / 2, cy = S / 2 + SS; int r = 4 * SS; /* the ring */ int rad = 3 * SS; /* the body's corner radius */ gdk_colormap_alloc_color (cmap, &face, FALSE, TRUE); gdk_colormap_alloc_color (cmap, &shade, FALSE, TRUE); gdk_colormap_alloc_color (cmap, &glyph, FALSE, TRUE); bg = gdk_pixbuf_get_from_drawable (NULL, pixmap, cmap, bx, by, 0, 0, POWER_SIZE, POWER_SIZE); big = gdk_pixbuf_scale_simple (bg, S, S, GDK_INTERP_BILINEAR); bp = gdk_pixmap_new (programs_fixed->window, S, S, -1); gdk_drawable_set_colormap (bp, cmap); bgc = gdk_gc_new (bp); gdk_draw_pixbuf (bp, bgc, big, 0, 0, 0, 0, S, S, GDK_RGB_DITHER_NONE, 0, 0); /* body: a rounded rectangle, a cross plus four corner pies */ gdk_gc_set_foreground (bgc, &face); gdk_draw_rectangle (bp, bgc, TRUE, rad, 0, S - 2 * rad, S); gdk_draw_rectangle (bp, bgc, TRUE, 0, rad, S, S - 2 * rad); gdk_draw_arc (bp, bgc, TRUE, 0, 0, 2 * rad, 2 * rad, 90 * 64, 90 * 64); gdk_draw_arc (bp, bgc, TRUE, S - 2 * rad, 0, 2 * rad, 2 * rad, 0, 90 * 64); gdk_draw_arc (bp, bgc, TRUE, 0, S - 2 * rad, 2 * rad, 2 * rad, 180 * 64, 90 * 64); gdk_draw_arc (bp, bgc, TRUE, S - 2 * rad, S - 2 * rad, 2 * rad, 2 * rad, 270 * 64, 90 * 64); /* a darker lip along the bottom and right, as the tabs have */ gdk_gc_set_foreground (bgc, &shade); gdk_gc_set_line_attributes (bgc, SS, GDK_LINE_SOLID, GDK_CAP_BUTT, GDK_JOIN_MITER); gdk_draw_line (bp, bgc, rad, S - SS / 2, S - rad, S - SS / 2); gdk_draw_line (bp, bgc, S - SS / 2, rad, S - SS / 2, S - rad); /* the mark: a ring broken at the top, a bar rising through the break. gdk angles are 1/64 degree, zero at three o'clock, counter-clockwise, so the gap is centred on 90. */ gdk_gc_set_foreground (bgc, &glyph); gdk_gc_set_line_attributes (bgc, 2 * SS, GDK_LINE_SOLID, GDK_CAP_ROUND, GDK_JOIN_ROUND); gdk_draw_arc (bp, bgc, FALSE, cx - r, cy - r, 2 * r, 2 * r, 125 * 64, 290 * 64); gdk_draw_line (bp, bgc, cx, cy - r - 2 * SS, cx, cy - SS); shot = gdk_pixbuf_get_from_drawable (NULL, bp, cmap, 0, 0, 0, 0, S, S); down = gdk_pixbuf_scale_simple (shot, POWER_SIZE, POWER_SIZE, GDK_INTERP_HYPER); gdk_draw_pixbuf (pixmap, gc, down, 0, 0, bx, by, POWER_SIZE, POWER_SIZE, GDK_RGB_DITHER_NORMAL, 0, 0); g_object_unref (bg); g_object_unref (big); g_object_unref (shot); g_object_unref (down); g_object_unref (bgc); g_object_unref (bp); } pango_font_description_free (fd); g_object_unref (buf); g_object_unref (rot); g_object_unref (sgc); g_object_unref (gc); g_object_unref (scratch); return gtk_image_new_from_pixmap (pixmap, NULL); } /* The rebuild must NOT happen inside the button handler. refresh_callback() destroys tabstrip_widget, and tabstrip_widget is the very widget whose button_press_event is running: GTK unwinds back into it afterwards to finish the press (releasing the implicit pointer grab), finds its GdkWindow gone, and takes gpe-appmgr down with Gdk-CRITICAL: gdk_drawable_get_display: assertion GDK_IS_DRAWABLE failed Gdk-CRITICAL: gdk_pointer_grab_info_libgtk_only: assertion GDK_IS_DISPLAY failed which is the PROGRAMS screen drawing once and then going black with only the panel left, since matchbox-panel is a separate process. Deferring to an idle lets the handler return and the dispatch finish first. This only ever bites on a real tap. At startup tabstrip_widget is NULL so nothing is destroyed mid-dispatch, which is why booting straight to PROGRAMS looked fine under emulation. */ static gboolean deferred_refresh (gpointer data) { refresh_callback (); return FALSE; /* one shot */ } /* A tap anywhere in the strip picks the tab it landed in. */ static gboolean tabstrip_press (GtkWidget *w, GdkEventButton *ev, gpointer data) { int i = ((int) ev->y) / TAB_HEIGHT; int h = programs_fixed ? programs_fixed->allocation.height : 0; /* Same bottom the button is drawn against, and tested before the tab arithmetic -- ev->y / TAB_HEIGHT would otherwise call this tab 5. */ if (h > 0) h = MIN (h, gdk_screen_height () - PANEL_HEIGHT); if (h > 0 && ev->y >= h - POWER_SIZE - POWER_GAP && ev->y < h - POWER_GAP) { /* Sleep, not log out. The lx user has an empty password field (`lx::1000:1000:` in /etc/passwd), so Xinit.d/99psion-session takes its `su lx -c Xsession` branch and never shows a login at all -- logging out would drop the session and be put straight back into it. On a machine with a lid, a power button means suspend. `apm --suspend` is this system's own way to do it: it is the built-in default sleep command of ipaq-sleep, which is already running as lx and suspends the machine on idle. It works unprivileged. Not `pm -pp`, which is what the Suspend menu entry runs and is broken here: both paths it tries, /sbin/pm_helper and /proc/sys/pm/suspend, are absent from this build. */ if (fork () == 0) { execlp ("apm", "apm", "--suspend", (char *) NULL); _exit (1); } return TRUE; } if (i < 0 || i >= (int) N_TABS) return FALSE; if (tabstrip[i].exec) /* TASKS runs Psion's task scripts */ { if (fork () == 0) { execlp (tabstrip[i].exec, tabstrip[i].exec, (char *) NULL); _exit (1); } return TRUE; } if (i != current_view) /* TODAY and PROGRAMS swap in place */ { current_view = i; g_idle_add (deferred_refresh, NULL); } return TRUE; } /* Make the contents for a notebook tab. * Generally we only want one group, but NULL means * ignore group setting (ie. "All"). */ static GtkWidget * create_row (GList *all_items, char *current_group) { GtkWidget *view; GList *this_item; view = gpe_icon_list_view_new (); gpe_icon_list_view_set_icon_size (GPE_ICON_LIST_VIEW (view), 32); gpe_icon_list_view_set_icon_xmargin (GPE_ICON_LIST_VIEW (view), 24); gpe_icon_list_view_set_bg_pixmap (GPE_ICON_LIST_VIEW (view), background); gpe_icon_list_view_set_bg_color (GPE_ICON_LIST_VIEW (view), 0x00ff8080); gpe_icon_list_view_set_border_color (GPE_ICON_LIST_VIEW (view), 0x00bfaea5); gpe_icon_list_view_set_border_width (GPE_ICON_LIST_VIEW (view), 8); views = g_slist_append (views, view); this_item = all_items; while (this_item) { GnomeDesktopFile *p; GObject *item; gchar *name = NULL; p = (GnomeDesktopFile *) this_item->data; gnome_desktop_file_get_string (p, NULL, "Name", &name); item = gpe_icon_list_view_add_item (GPE_ICON_LIST_VIEW (view), name, get_icon_fn (p, 48), (gpointer)p); g_signal_connect (item, "button-release", G_CALLBACK (run_callback), p); this_item = this_item->next; } gtk_widget_show (view); return view; } static int add_row (GtkWidget *row, gint cur_y, gchar *label) { GtkAllocation alloc; GtkRequisition req; guint width; width = programs_fixed->allocation.width; alloc.x = ICONLIST_XOFFSET; alloc.y = cur_y; alloc.width = width - (2 * ICONLIST_XOFFSET) + LABEL_WIDTH - TABSTRIP_WIDTH; alloc.height = 1; gtk_widget_size_allocate (row, &alloc); gtk_widget_size_request (row, &req); alloc.height = req.height; gtk_widget_size_allocate (row, &alloc); gtk_widget_set_usize (row, alloc.width, -1); cur_y += req.height + 8; gtk_fixed_put (GTK_FIXED (programs_fixed), row, alloc.x, alloc.y); if (label) { GtkWidget *w; w = make_label (label, req.height); alloc.x -= LABEL_WIDTH; gtk_widget_size_request (w, &req); alloc.width = req.width; alloc.height = req.height; gtk_widget_size_allocate (w, &alloc); gtk_fixed_put (GTK_FIXED (programs_fixed), w, alloc.x, alloc.y); gtk_widget_show (w); labels = g_slist_prepend (labels, w); } return cur_y; } /* One-shot re-layout, used when the first pass overflows the screen. The PROGRAMS rows come out 8px taller each on some boots than on others -- four groups, seven rows, 56px cumulative, which is enough to push the last row of OTHER under the panel. There is no scrolled window here, so anything below the panel is simply unreachable. Restarting the session has always cleared it, which says the layout is right once whatever it depends on has settled and wrong when it is computed too early. Rather than guess at the dependency, notice the overflow and lay out once more. It runs at most once per process, and does nothing at all on the boots that were already correct. */ static gboolean relayout_pending; static gboolean relayout_done; static gboolean relayout_once (gpointer data) { relayout_pending = FALSE; refresh_callback (); return FALSE; } static void refresh_callback (void) { GList *l; GSList *ll; int cur_y; GtkWidget *row; cur_y = 16; for (ll = rows; ll; ll = ll->next) { gtk_widget_destroy (ll->data); } g_slist_free (rows); rows = NULL; for (ll = labels; ll; ll = ll->next) { gtk_widget_destroy (ll->data); } g_slist_free (labels); /* upstream writes "rows = NULL" here, a copy-paste slip: labels is freed and then left dangling, so the NEXT refresh walks freed memory and calls gtk_widget_destroy() on garbage. Upstream never noticed because nothing refreshed the desktop twice; the TODAY/PROGRAMS tabs refresh on every tap, so it took gpe-appmgr down and left the bare black root window with only the panel on it. */ labels = NULL; destroy_today (); if (current_view < 0) current_view = today_at_startup () ? VIEW_TODAY : VIEW_PROGRAMS; if (current_view == VIEW_TODAY) { if (programs_fixed && programs_fixed->window) build_today (); goto strip; } for (l = groups; l; l = l->next) { struct package_group *group = l->data; if (group->hide) continue; if (group->items == NULL) continue; row = create_row (group->items, group->name); g_object_set_data (G_OBJECT (row), "group", group); rows = g_slist_prepend (rows, row); cur_y = add_row (row, cur_y, group->name); } /* Did the rows run past the bottom of the usable area? */ if (!relayout_done && !relayout_pending && cur_y > gdk_screen_height () - PANEL_HEIGHT) { relayout_done = TRUE; relayout_pending = TRUE; g_timeout_add (1500, relayout_once, NULL); } strip: /* The TODAY / PROGRAMS / TASKS strip down the right-hand edge. The event box is created ONCE and then kept for the life of the process; only the image inside it is swapped on a refresh. It must not be destroyed, because the tap that triggers the refresh is a button PRESS on this very widget and the press holds an implicit pointer grab until the matching release. Destroying the grab holder inside that window kills gpe-appmgr with Gdk-CRITICAL: gdk_drawable_get_display: assertion GDK_IS_DRAWABLE failed Gdk-CRITICAL: gdk_pointer_grab_info_libgtk_only: assertion GDK_IS_DISPLAY failed leaving the bare root window with only matchbox-panel on it. Deferring the rebuild to an idle is not enough on its own: the idle runs as soon as the loop is idle, which is still before the button release, so the grab is live either way. Keeping the widget is what actually fixes it. */ if (programs_fixed && programs_fixed->window) { int w = programs_fixed->allocation.width; int h = programs_fixed->allocation.height; GtkWidget *img = make_tabstrip (h); if (img && tabstrip_widget) { GtkWidget *old_img = GTK_BIN (tabstrip_widget)->child; if (old_img) gtk_container_remove (GTK_CONTAINER (tabstrip_widget), old_img); gtk_container_add (GTK_CONTAINER (tabstrip_widget), img); gtk_widget_show_all (tabstrip_widget); } else if (img) { GtkWidget *ebox = gtk_event_box_new (); gtk_container_add (GTK_CONTAINER (ebox), img); gtk_widget_add_events (ebox, GDK_BUTTON_PRESS_MASK); g_signal_connect (G_OBJECT (ebox), "button_press_event", G_CALLBACK (tabstrip_press), NULL); gtk_fixed_put (GTK_FIXED (programs_fixed), ebox, w - TABSTRIP_WIDTH, 0); gtk_widget_show_all (ebox); tabstrip_widget = ebox; } } } gboolean draw_programs (GtkWidget *widget, GdkEventExpose *ev) { if (background) { GdkRectangle r, br; br.x = 0; br.y = 0; br.width = gdk_pixbuf_get_width (background); br.height = gdk_pixbuf_get_height (background); if (gdk_rectangle_intersect (&ev->area, &br, &r)) { gdk_draw_pixbuf (widget->window, widget->style->fg_gc[widget->state], background, r.x, r.y, r.x, r.y, r.width, r.height, GDK_RGB_DITHER_NORMAL, 0, 0); } } else gdk_draw_rectangle (widget->window, bg_gc, TRUE, ev->area.x, ev->area.y, ev->area.width, ev->area.height); return FALSE; } void set_bg_color (GtkWidget *w, guint32 c) { GdkColormap *gcm; GdkColor color; color.red = (c & 0xff); color.green = (c & 0xff00) >> 8; color.blue = (c & 0xff0000) >> 16; color.red |= (color.red << 8); color.green |= (color.green << 8); color.blue |= (color.blue << 8); gcm = gdk_drawable_get_colormap (w->window); gdk_colormap_alloc_color (gcm, &color, FALSE, TRUE); gdk_gc_set_foreground (bg_gc, &color); } void realize_programs (GtkWidget *w) { bg_gc = gdk_gc_new (w->window); set_bg_color (w, 0xff0000); } GtkWidget * build_programs (void) { GtkWidget *draw; draw = gtk_fixed_new (); gtk_widget_set_name (draw, "row-widget"); gtk_fixed_set_has_window (GTK_FIXED (draw), TRUE); g_signal_connect (G_OBJECT (draw), "expose_event", G_CALLBACK (draw_programs), NULL); g_signal_connect_after (G_OBJECT (draw), "realize", G_CALLBACK (realize_programs), NULL); programs_fixed = draw; return draw; } GtkWidget * create_row_view (void) { GtkWidget *progs_draw; progs_draw = build_programs (); gtk_widget_show (progs_draw); g_object_set_data (G_OBJECT (progs_draw), "refresh_func", refresh_callback); toplevel = progs_draw; return progs_draw; }