/* * (C) 2006, 2007 OpenedHand Ltd. * * Authors: Jorn Baayen * Ross Burton * * Licensed under the GPL v2 or greater. */ #include #include #include #include #include #include #include #include #define DEFAULT_WINDOW_ICON_NAME "application-x-executable" enum { _MB_APP_WINDOW_LIST_STACKING, _MB_CURRENT_APP_WINDOW, UTF8_STRING, _NET_WM_VISIBLE_NAME, _NET_WM_NAME, _NET_ACTIVE_WINDOW, _NET_WM_ICON, N_ATOMS }; typedef enum { MODE_STATIC_ICON, MODE_DYNAMIC_ICON, MODE_ICON_NAME, MODE_NAME } WindowSelectorAppletMode; typedef struct { GtkWidget *button; GtkWidget *menu; GtkWidget *image; GtkWidget *static_image; GtkWidget *label; /* If the menu will be showing images. */ gboolean show_images; Atom atoms[N_ATOMS]; GdkWindow *root_window; WindowSelectorAppletMode mode; } WindowSelectorApplet; static GdkFilterReturn filter_func (GdkXEvent *xevent, GdkEvent *event, WindowSelectorApplet *applet); static void window_selector_applet_free (WindowSelectorApplet *applet) { if (applet->root_window) { gdk_window_remove_filter (applet->root_window, (GdkFilterFunc) filter_func, applet); } g_slice_free (WindowSelectorApplet, applet); } /* Retrieves the UTF-8 property @atom from @window */ static char * get_utf8_property (WindowSelectorApplet *applet, Window window, Atom atom) { GdkDisplay *display; Atom type; int format, result; gulong nitems, bytes_after; guchar *val; char *ret; display = gtk_widget_get_display (GTK_WIDGET (applet->button)); type = None; val = NULL; gdk_error_trap_push (); result = XGetWindowProperty (GDK_DISPLAY_XDISPLAY (display), window, atom, 0, G_MAXLONG, False, applet->atoms[UTF8_STRING], &type, &format, &nitems, &bytes_after, (gpointer) &val); if (gdk_error_trap_pop () || result != Success) return NULL; if (type != applet->atoms[UTF8_STRING] || format != 8 || nitems == 0) { if (val) XFree (val); return NULL; } if (!g_utf8_validate ((char *) val, nitems, NULL)) { g_warning ("Invalid UTF-8 in window title"); XFree (val); return NULL; } ret = g_strndup ((char *) val, nitems); XFree (val); return ret; } /* Retrieves the text property @atom from @window */ static char * get_text_property (WindowSelectorApplet *applet, Window window, Atom atom) { GdkDisplay *display; XTextProperty text; char *ret, **list; int result, count; display = gtk_widget_get_display (GTK_WIDGET (applet->button)); gdk_error_trap_push (); result = XGetTextProperty (GDK_DISPLAY_XDISPLAY (display), window, &text, atom); if (gdk_error_trap_pop () || result == 0) return NULL; count = gdk_text_property_to_utf8_list_for_display (display, gdk_x11_xatom_to_atom (text.encoding), text.format, text.value, text.nitems, &list); if (count > 0) { int i; ret = list[0]; for (i = 1; i < count; i++) g_free (list[i]); g_free (list); } else ret = NULL; if (text.value) XFree (text.value); return ret; } /* Retrieves the name for @window */ static char * window_get_name (WindowSelectorApplet *applet, Window window) { char *name; name = get_utf8_property (applet, window, applet->atoms[_NET_WM_VISIBLE_NAME]); if (name == NULL) { name = get_utf8_property (applet, window, applet->atoms[_NET_WM_NAME]); } if (name == NULL) { name = get_text_property (applet, window, XA_WM_NAME); } if (name == NULL) { name = g_strdup (_("(untitled)")); } return name; } /* Retrieves the icon for @window */ static GdkPixbuf * window_get_icon (WindowSelectorApplet *applet, Window window) { GdkPixbuf *pixbuf; GdkDisplay *display; Atom type; int format, result; int ideal_width, ideal_height, ideal_size; int best_width, best_height, best_size; int i, npixels, ip; gulong nitems, bytes_after, *data, *datap, *best_data; GtkSettings *settings; guchar *pixdata; /* First, we read the contents of the _NET_WM_ICON property */ display = gtk_widget_get_display (GTK_WIDGET (applet->button)); type = 0; gdk_error_trap_push (); result = XGetWindowProperty (GDK_DISPLAY_XDISPLAY (display), window, applet->atoms[_NET_WM_ICON], 0, G_MAXLONG, False, XA_CARDINAL, &type, &format, &nitems, &bytes_after, (gpointer) &data); if (gdk_error_trap_pop () || result != Success) return NULL; if (type != XA_CARDINAL || nitems < 3) { XFree (data); return NULL; } /* Got it. Now what size icon are we looking for? */ settings = gtk_widget_get_settings (GTK_WIDGET (applet->button)); gtk_icon_size_lookup_for_settings (settings, GTK_ICON_SIZE_MENU, &ideal_width, &ideal_height); ideal_size = (ideal_width + ideal_height) / 2; /* Try to find the closest match */ best_data = NULL; best_width = best_height = best_size = 0; datap = data; while (nitems > 0) { int cur_width, cur_height, cur_size; gboolean replace; if (nitems < 3) break; cur_width = datap[0]; cur_height = datap[1]; cur_size = (cur_width + cur_height) / 2; if (nitems < (2 + cur_width * cur_height)) break; if (!best_data) { replace = TRUE; } else { /* Always prefer bigger to smaller */ if (best_size < ideal_size && cur_size > best_size) replace = TRUE; /* Prefer smaller bigger */ else if (best_size > ideal_size && cur_size >= ideal_size && cur_size < best_size) replace = TRUE; else replace = FALSE; } if (replace) { best_data = datap + 2; best_width = cur_width; best_height = cur_height; best_size = cur_size; } datap += (2 + cur_width * cur_height); nitems -= (2 + cur_width * cur_height); } if (!best_data) { XFree (data); return NULL; } /* Got it. Load it into a pixbuf. */ npixels = best_width * best_height; pixdata = g_new (guchar, npixels * 4); for (i = 0, ip = 0; i < npixels; i++) { /* red */ pixdata[ip] = (best_data[i] >> 16) & 0xff; ip++; /* green */ pixdata[ip] = (best_data[i] >> 8) & 0xff; ip++; /* blue */ pixdata[ip] = best_data[i] & 0xff; ip++; /* alpha */ pixdata[ip] = best_data[i] >> 24; ip++; } pixbuf = gdk_pixbuf_new_from_data (pixdata, GDK_COLORSPACE_RGB, TRUE, 8, best_width, best_height, best_width * 4, (GdkPixbufDestroyNotify) g_free, NULL); /* Scale if necessary */ if (best_width != ideal_width && best_height != ideal_height) { GdkPixbuf *scaled; scaled = gdk_pixbuf_scale_simple (pixbuf, ideal_width, ideal_height, GDK_INTERP_BILINEAR); g_object_unref (pixbuf); pixbuf = scaled; } /* Cleanup */ XFree (data); /* Return */ return pixbuf; } static Window get_current_app_window (WindowSelectorApplet *applet) { GdkDisplay *display; Atom type; int format, result; gulong nitems, bytes_after, *data; Window window; display = gtk_widget_get_display (GTK_WIDGET (applet->button)); type = 0; gdk_error_trap_push (); result = XGetWindowProperty (GDK_DISPLAY_XDISPLAY (display), GDK_WINDOW_XID (applet->root_window), applet->atoms[_MB_CURRENT_APP_WINDOW], 0, 1, False, AnyPropertyType, &type, &format, &nitems, &bytes_after, (gpointer) &data); if (gdk_error_trap_pop () || result != Success) return 0; if (type != XA_WINDOW || nitems < 1) { XFree (data); return 0; } window = (Window)data[0]; XFree (data); return window; } /* Window menu item activated. Activate the associated window. */ static void window_menu_item_activate_cb (GtkWidget *widget, WindowSelectorApplet *applet) { Window window; Screen *screen; GtkWidget *toplevel; XEvent xev; window = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (widget), "window")); screen = GDK_SCREEN_XSCREEN (gtk_widget_get_screen (widget)); toplevel = gtk_widget_get_toplevel (GTK_WIDGET (applet->button)); /* Send _NET_ACTIVE_WINDOW message */ xev.xclient.type = ClientMessage; xev.xclient.serial = 0; xev.xclient.send_event = True; xev.xclient.display = DisplayOfScreen (screen); xev.xclient.window = window; xev.xclient.message_type = applet->atoms[_NET_ACTIVE_WINDOW]; xev.xclient.format = 32; xev.xclient.data.l[0] = 2; xev.xclient.data.l[1] = gtk_get_current_event_time (); xev.xclient.data.l[2] = GDK_WINDOW_XID (gtk_widget_get_window (toplevel)); xev.xclient.data.l[3] = 0; xev.xclient.data.l[4] = 0; XSendEvent (DisplayOfScreen (screen), RootWindowOfScreen (screen), False, SubstructureRedirectMask, &xev); } /* Rebuild the selector menu */ static void rebuild_menu (WindowSelectorApplet *applet) { GList *kids; GdkDisplay *display; Atom type; int format, result, i; gulong nitems, bytes_after; Window *windows; /* Empty menu */ kids = gtk_container_get_children (GTK_CONTAINER (applet->menu)); while (kids) { gtk_widget_destroy (kids->data); kids = g_list_delete_link (kids, kids); } /* Retrieve list of app windows from root window */ display = gtk_widget_get_display (GTK_WIDGET (applet->button)); type = None; gdk_error_trap_push (); result = XGetWindowProperty (GDK_DISPLAY_XDISPLAY (display), GDK_WINDOW_XID (applet->root_window), applet->atoms [_MB_APP_WINDOW_LIST_STACKING], 0, G_MAXLONG, False, XA_WINDOW, &type, &format, &nitems, &bytes_after, (gpointer) &windows); if (gdk_error_trap_pop () || result != Success) return; if (type != XA_WINDOW) { XFree (windows); return; } /* Load into menu */ for (i = 0; i < nitems; i++) { char *name; GtkWidget *menu_item; name = window_get_name (applet, windows[i]); menu_item = gtk_image_menu_item_new_with_label (name); g_free (name); if (applet->show_images) { GtkWidget *image; GdkPixbuf *icon; image = gtk_image_new (); icon = window_get_icon (applet, windows[i]); if (icon) { gtk_image_set_from_pixbuf (GTK_IMAGE (image), icon); g_object_unref (icon); } else { gtk_image_set_from_icon_name (GTK_IMAGE (image), DEFAULT_WINDOW_ICON_NAME, GTK_ICON_SIZE_MENU); } gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item), image); gtk_widget_show (image); } g_object_set_data (G_OBJECT (menu_item), "window", GUINT_TO_POINTER (windows[i])); g_signal_connect (menu_item, "activate", G_CALLBACK (window_menu_item_activate_cb), applet); gtk_menu_shell_prepend (GTK_MENU_SHELL (applet->menu), menu_item); gtk_widget_show (menu_item); } /* If no windows were found, insert an insensitive "No tasks" item */ if (nitems == 0) { GtkWidget *menu_item; menu_item = gtk_menu_item_new_with_label (_("No tasks")); gtk_widget_set_sensitive (menu_item, FALSE); gtk_menu_shell_prepend (GTK_MENU_SHELL (applet->menu), menu_item); gtk_widget_show (menu_item); } /* Cleanup */ XFree (windows); } /* Menu was deactivated */ static void menu_selection_done_cb (GtkMenuShell *menu_shell, WindowSelectorApplet *applet) { gtk_widget_destroy (GTK_WIDGET (menu_shell)); gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (applet->button), FALSE); } static void position_menu (GtkMenu *menu, int *x, int *y, gboolean *push_in, gpointer user_data) { WindowSelectorApplet *applet = user_data; GtkAllocation allocation; gdk_window_get_origin (gtk_widget_get_window (applet->button), x, y); gtk_widget_get_allocation (applet->button, &allocation); *x += allocation.x; *y += allocation.height; *push_in = TRUE; } static void toggled_cb (GtkToggleButton *button, WindowSelectorApplet *applet) { GtkWidget *menu; if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (applet->button))) return; /* Set up menu */ menu = gtk_menu_new (); applet->menu = menu; g_object_add_weak_pointer (G_OBJECT (menu), (gpointer) &applet->menu); g_signal_connect (menu, "selection-done", G_CALLBACK (menu_selection_done_cb), applet); rebuild_menu (applet); gtk_menu_popup (GTK_MENU (menu), NULL, NULL, position_menu, applet, 0, gtk_get_current_event_time ()); } static void update_current_app (WindowSelectorApplet *applet) { Window window; if (applet->mode == MODE_STATIC_ICON) return; window = get_current_app_window (applet); if (window) { if (applet->mode == MODE_DYNAMIC_ICON || applet->mode == MODE_ICON_NAME) { gtk_image_set_from_pixbuf (GTK_IMAGE (applet->image), window_get_icon (applet, window)); gtk_widget_show (applet->image); } if (applet->mode == MODE_ICON_NAME || applet->mode == MODE_NAME) { gchar *name = NULL; name = window_get_name (applet, window); gtk_label_set_text (GTK_LABEL (applet->label), name); g_free (name); } } else { if (applet->mode == MODE_DYNAMIC_ICON || applet->mode == MODE_ICON_NAME) { gtk_widget_hide (applet->image); } if (applet->mode == MODE_ICON_NAME || applet->mode == MODE_NAME) { gtk_label_set_text (GTK_LABEL (applet->label), _("No tasks")); } } } /* Something happened on the root window */ static GdkFilterReturn filter_func (GdkXEvent *xevent, GdkEvent *event, WindowSelectorApplet *applet) { XEvent *xev; xev = (XEvent *) xevent; if (xev->type == PropertyNotify) { if (xev->xproperty.atom == applet->atoms[_MB_APP_WINDOW_LIST_STACKING]) { /* _MB_APP_WINDOW_LIST_STACKING changed. * Rebuild menu if around. */ if (applet->menu && gtk_widget_get_visible (applet->menu)) rebuild_menu (applet); } if (xev->xproperty.atom == applet->atoms[_MB_CURRENT_APP_WINDOW]) { update_current_app (applet); } } return GDK_FILTER_CONTINUE; } /* Screen changed */ static void screen_changed_cb (GtkWidget *button, GdkScreen *old_screen, WindowSelectorApplet *applet) { GdkScreen *screen; GdkDisplay *display; GdkEventMask events; if (applet->root_window) { gdk_window_remove_filter (applet->root_window, (GdkFilterFunc) filter_func, applet); } screen = gtk_widget_get_screen (button); display = gdk_screen_get_display (screen); /* VILE The property GtkSetting::gtk-image-menus doesn't exist until a GtkImageMenuItem has been created, and that happens after the initial screen-changed signal. The clean alternative is to check the value of the property every time the menu is created, which is probably only fractionally slower. */ gtk_widget_destroy (gtk_image_menu_item_new ()); /* Get settings */ g_object_get (gtk_settings_get_for_screen (screen), "gtk-menu-images", &applet->show_images, NULL); /* Get atoms */ applet->atoms[_MB_APP_WINDOW_LIST_STACKING] = gdk_x11_get_xatom_by_name_for_display (display, "_MB_APP_WINDOW_LIST_STACKING"); applet->atoms[_MB_CURRENT_APP_WINDOW] = gdk_x11_get_xatom_by_name_for_display (display, "_MB_CURRENT_APP_WINDOW"); applet->atoms[UTF8_STRING] = gdk_x11_get_xatom_by_name_for_display (display, "UTF8_STRING"); applet->atoms[_NET_WM_NAME] = gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_NAME"); applet->atoms[_NET_WM_VISIBLE_NAME] = gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_VISIBLE_NAME"); applet->atoms[_NET_WM_ICON] = gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_ICON"); applet->atoms[_NET_ACTIVE_WINDOW] = gdk_x11_get_xatom_by_name_for_display (display, "_NET_ACTIVE_WINDOW"); /* Get root window */ applet->root_window = gdk_screen_get_root_window (screen); /* Watch _MB_APP_WINDOW_LIST_STACKING */ events = gdk_window_get_events (applet->root_window); if ((events & GDK_PROPERTY_CHANGE_MASK) == 0) { gdk_window_set_events (applet->root_window, events & GDK_PROPERTY_CHANGE_MASK); } gdk_window_add_filter (applet->root_window, (GdkFilterFunc) filter_func, applet); /* Rebuild menu if around */ if (applet->menu && gtk_widget_get_visible (applet->menu)) rebuild_menu (applet); update_current_app (applet); } G_MODULE_EXPORT GtkWidget * mb_panel_applet_create (const char *id, GtkOrientation orientation) { WindowSelectorApplet *applet; GtkWidget *hbox; /* Create applet data structure */ applet = g_slice_new0 (WindowSelectorApplet); /* identify the mode */ if (id == NULL || strlen (id) == 0) { applet->mode = MODE_STATIC_ICON; } else if (strcmp (id, "static-icon") == 0) { applet->mode = MODE_STATIC_ICON; } else if (strcmp (id, "dynamic-icon") == 0) { applet->mode = MODE_DYNAMIC_ICON; } else if (strcmp (id, "icon-name") == 0) { applet->mode = MODE_ICON_NAME; } else if (strcmp (id, "name") == 0) { applet->mode = MODE_NAME; } else { g_warning ("Unknown mode given as id"); applet->mode = MODE_STATIC_ICON; } /* The button itself */ applet->button = gtk_toggle_button_new (); gtk_button_set_relief (GTK_BUTTON (applet->button), GTK_RELIEF_NONE); gtk_widget_set_name (applet->button, "MatchboxPanelWindowSelector"); switch (applet->mode) { case MODE_STATIC_ICON: applet->image = mb_panel_scaling_image2_new (orientation, "panel-task-switcher"); gtk_container_add (GTK_CONTAINER (applet->button), applet->image); break; case MODE_DYNAMIC_ICON: applet->image = gtk_image_new (); gtk_container_add (GTK_CONTAINER (applet->button), applet->image); break; case MODE_ICON_NAME: hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4); applet->label = gtk_label_new (NULL); applet->image = gtk_image_new (); gtk_box_pack_start (GTK_BOX (hbox), applet->image, FALSE, FALSE, 2); gtk_box_pack_start (GTK_BOX (hbox), applet->label, TRUE, TRUE, 0); gtk_container_add (GTK_CONTAINER (applet->button), hbox); break; case MODE_NAME: applet->label = gtk_label_new (NULL); gtk_container_add (GTK_CONTAINER (applet->button), applet->label); break; } /* TODO: also pack an arrow? */ g_signal_connect (applet->button, "screen-changed", G_CALLBACK (screen_changed_cb), applet); g_signal_connect (applet->button, "toggled", G_CALLBACK (toggled_cb), applet); g_object_weak_ref (G_OBJECT (applet->button), (GWeakNotify) window_selector_applet_free, applet); /* Show! */ gtk_widget_show_all (applet->button); return applet->button; };