Hacker-friendly Metacity window selector, v0.0.1




The title is just a tiny bit misleading.  As suggested by Rob Adams

   http://bugzilla.gnome.org/show_bug.cgi?id=140925

I wrote a simple window list popup for metacity, see below (please
bear with me, my first contact with gtk+).

I'd want to ask you how I use EWHM/libwnck/Metacity to make a
keybinding that pops-up my window list.  This should be possible, see
the bug report discussion, but I cannot seem to find how.

Maybe I should look at the `extremely complicated Metacity extensions
mechanism', but somehow that does sound too appealing.

Greetings,
Jan.


[is this more on toping for the desktop-devel-list?]

/*
  keywise.c: Keyboard Window Selector v0.0.1

  Hacker-friendly Metacity window selector.

  The idea of Keywise is to popup a window list (a bit similar to
  WindowMaker) that shows all windows grouped by class, with
  predictable accelerators.  For example:

     M-TAB 1  (better yet: KP_1 KP_1)

  should focus your Emacs or Browser window if you have only one open,
  or cycle through the list of Emacs windows to choose from.


RATIONALE

  Metacity is a simple, solid and well supported window manager for
  GNOME.  It so lightweight, that it does not have sensible or
  customisable window selection, which makes it very awkward to use.
  Metacity does support a minimalist keybinding and popup for cycling
  through all available windows that happen to be in the active
  workspace, which is most unfriendly, inefficient, and hardly ever
  what you want [I've now learned that this marvelless pick-and-pray
  selection method is inspired by Microsoft Windows XP Home Edition --
  sigh].

  Adding any custom, customisable or window-class based focussing that
  supports keyboard accelerators to Metacity is [sadly] not an option
  (see http://bugzilla.gnome.org/show_bug.cgi?id=140925).  I think
  that is an oversight not only because every decent window manager
  since fvwm has supported it, but also because advocating use of the
  mouse is evil.  The good news is that full extensibility of GNOME
  window management is supposedly supported by using libwnck (should
  study EWMH: keybinding to menu popup from Metacity).


USAGE

  * Metacity -- FIXME how to bind key to: focus keywise menu?

    hack: bind KP_1 -> run_command_1 -> keywise --standalone

  * Sawfish -- silly, but handy for testing

  cat >> ~/.sawfishrc <<EOF

(defun focus-window (w)
  (unshade-window w)
  (display-window w)
  (warp-cursor-to-window w 15 10))

(defun windows-of-name (name)
  (filter (lambda (w) (string-match name (window-name w))) (managed-windows)))

(bind-keys global-keymap "H-TAB"
  '(focus-window (car (windows-of-name "keywise"))))
;; ' /*
EOF

    sawfish-client -f restart


  Copyright (C) 2004 Jan Nieuwenhuizen <janneke gnu org>

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 of the License, or
  (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA 02111-1307, USA.

  This started as a hacked-up window-menu.c, the keen eye may still
  find pieces:

  Copyright (C) 2003 Sun Microsystems, Inc.
  Copyright (C) 2001 Free Software Foundation, Inc.
  Copyright (C) 2000 Helix Code, Inc.

  window-menu.c Authors:
  Mark McLoughlin <mark skynet ie>
  George Lebl <jirka 5z com>
  Jacob Berkman <jacob helixcode com>
*/

#include <gnome.h>

/* But I'm not told whether this unstability is good (active hacking)
   or bad (will be dropped), and how much at that.  */
#define WNCK_I_KNOW_THIS_IS_UNSTABLE
#include <libwnck/libwnck.h>
#include <libwnck/class-group.h>

/* Do something like
    gdk-pixbuf-csource --raw --build-list default_icon_data default_icon.png \
        > icons.h */
#include "icons.h"

#define STANDALONE_HACK
gboolean standalone_p = 0;

/* Ugh.  -- hello world mode  */
#define CLASS_GROUP_COUNT 10
#define APP_NAME_COUNT 5
char const *CLASS_NAME_GROUPS[CLASS_GROUP_COUNT][APP_NAME_COUNT] =
  {
    { "emacs", 0, 0, 0, 0 },
    { "terminal", "xterm", "rxvt", 0, 0 },
    { "xdvi", "xpdf", "gv", "ggv", "gpdf" },
    { "mozilla", "galeon", "firebird", "epiphany", 0 },
    { "eog", "gimp", "display", 0, 0 },
    { 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0 },
    { 0, 0, 0, 0, 0 },
    { "ungrouped", 0, 0, 0, 0 }
  };


typedef struct
{
  GtkWidget *main;
  GtkWidget *frame;
  GtkWidget *menu;

  GtkWidget *no_windows_item;
  int size;
} Window_list;



/* Utility.  */

static int
class_group_number (WnckWindow *window)
{
  int i, j;

  WnckClassGroup *group = wnck_window_get_class_group (window);
  char const *name = wnck_class_group_get_name (group);
  size_t len = strlen (name);

  for (i = 0; i < CLASS_GROUP_COUNT && CLASS_NAME_GROUPS[i]; i++)
    for (j = 0; j < APP_NAME_COUNT && CLASS_NAME_GROUPS[i][j]; j++)
      if (!strncasecmp (name, CLASS_NAME_GROUPS[i][j],
                        MIN (strlen (CLASS_NAME_GROUPS[i][j]), len)))
        return i;

  return CLASS_GROUP_COUNT - 1;
}

static GdkPixbuf *
get_default_window_icon (void)
{
  static GdkPixbuf *icon = NULL;

  if (!icon)
    icon = gdk_pixbuf_new_from_inline (-1, default_icon_data, FALSE, NULL);

  return icon;
}

static void
menu_item_set_window_icon (GtkWidget *menu_item, WnckWindow *window)
{
  GdkPixbuf *pixbuf;
  int width, height;
  int icon_size;

  icon_size = -1;
        
  if (window)
    pixbuf = wnck_window_get_icon (window);

  if (!pixbuf)
    pixbuf = get_default_window_icon ();

  if (icon_size == -1)
    gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, NULL, &icon_size);

  width = gdk_pixbuf_get_width  (pixbuf);
  height = gdk_pixbuf_get_height (pixbuf);

  GtkWidget *image = gtk_image_new ();

  if (icon_size != -1
      && (width > icon_size || height > icon_size))
    {
      double scale = ((double) icon_size) / MAX (width, height);
      GdkPixbuf *newbuf = gdk_pixbuf_scale_simple (pixbuf, width * scale,
                                                   height * scale,
                                                   GDK_INTERP_BILINEAR);
      gtk_image_set_from_pixbuf (GTK_IMAGE (image), newbuf);
      g_object_unref (newbuf);
    }
  else
    gtk_image_set_from_pixbuf (GTK_IMAGE (image), pixbuf);

  gtk_image_menu_item_set_image (GTK_IMAGE_MENU_ITEM (menu_item),
                                 GTK_WIDGET (image));
  gtk_widget_show (image);
}

GtkAccelGroup *
widget_get_accel_group (GtkWidget *widget)
{
  GtkAccelGroup *group = g_object_get_data (G_OBJECT (widget),
                                            "keywise-accel-group");
  if (!group)
    {
      group = gtk_accel_group_new ();
      g_object_set_data_full (G_OBJECT (widget), "keywise-accel-group",
                              group, (GDestroyNotify) g_object_unref);
    }
  return group;
}


/* Gtk Widget callbacks.  */

static gint
main_widget_delete_event (GtkWidget *widget, GdkEvent *event, gpointer data)
{
  gtk_widget_destroy (widget);
  gtk_main_quit ();
  return TRUE;
}

#ifdef STANDALONE_HACK
static gint
standalone_main_widget_delete_event (GtkWidget *widget, GdkEvent *event,
                                     gpointer data)
{
  gtk_main_quit ();
  return TRUE;
}
#endif

static void
window_workspace_window_activate (WnckWindow *window)
{
  WnckWorkspace *workspace = wnck_window_get_workspace (window);
  wnck_workspace_activate (workspace);

  if (wnck_window_is_minimized (window))
    wnck_window_unminimize (window);
        
  wnck_window_activate (window);
}

static void
widget_destroy_window_list (GtkWidget *widget, Window_list *window_list)
{
  if (window_list->menu)
    gtk_widget_destroy (window_list->menu);
  window_list->menu = NULL;

  if (window_list->no_windows_item)
    g_object_unref (window_list->no_windows_item);
  window_list->no_windows_item = NULL;

  g_free (window_list);
}

static void
widget_destroy_window_list_menu (GtkWidget *widget, Window_list *window_list)
{
  window_list->menu = NULL;
}

static void
widget_hide_menu (GtkWidget *menu, Window_list *window_list)
{
  gtk_frame_set_shadow_type (GTK_FRAME (window_list->frame), GTK_SHADOW_NONE);
}


/* Window_list */

static WnckScreen *
window_list_get_screen (Window_list *window_list)
{
  GdkScreen *screen = gtk_widget_get_screen (window_list->main);
  return wnck_screen_get (gdk_screen_get_number (screen));
}

static WnckWorkspace *
window_list_get_active_workspace (Window_list *window_list)
{
  WnckScreen *screen = window_list_get_screen (window_list);
  return wnck_screen_get_active_workspace (screen);
}


static void
window_list_position_menu (GtkMenu *menu,
                           int *x, int *y,
                           gboolean *push_in, gpointer user_data)
{
  GtkWidget *widget = GTK_WIDGET (user_data);
  GtkRequisition requisition;
  gint menu_xpos;
  gint menu_ypos;

  gtk_widget_size_request (GTK_WIDGET (menu), &requisition);

  /* Hmm, we're called too early, before having a window?  */
  if (widget->window)
    gdk_window_get_origin (widget->window, &menu_xpos, &menu_ypos);

  menu_xpos += widget->allocation.x;
  menu_ypos += widget->allocation.y;

  GdkScreen *screen = gtk_widget_get_screen (widget);
  int height = gdk_screen_get_height (screen);
  int width = gdk_screen_get_width (screen);

  *x = (width - requisition.width) / 2;
  *y = (height - requisition.height) / 2;
  *push_in = TRUE;
}

static GtkWidget *
_menu_add_window (GtkWidget *menu, WnckWindow *window, char const *label)
{
  GtkWidget *menu_item = gtk_image_menu_item_new_with_mnemonic (label);
  gtk_menu_item_set_accel_path (GTK_MENU_ITEM (menu_item), NULL);
  menu_item_set_window_icon (menu_item, window);
  gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
  gtk_widget_show (menu_item);
  return menu_item;
}

char const *
window_get_basename (WnckWindow *window)
{
  WnckClassGroup *group = wnck_window_get_class_group (window);
  char const *group_name = wnck_class_group_get_name (group);
  char const *name = wnck_window_get_name (window);

  size_t len = 0;
  if (group_name)
    len = strlen (group_name);

  if (name && group_name && len && strlen (name) > len
      && !strncasecmp (name, group_name, len))
    return name + len;

  if (name)
    return name;

  if (group_name)
    return group_name;

  return _ ("Unknown Window");
}

char *
window_get_label (WnckWindow *window, int i, gboolean strip_group_name,
                  WnckWorkspace *active)
{
  char *label;
  WnckClassGroup *group = wnck_window_get_class_group (window);
  char const *group_name = wnck_class_group_get_name (group);
  char const *name = window_get_basename (window);
  char const *sep = " - ";

  if (strip_group_name || !strcasecmp (name, group_name))
    {
      sep = "";
      group_name = "";
    }

  WnckWorkspace *workspace = wnck_window_get_workspace (window);

  char *workspace_name;
  if (active != workspace)
    workspace_name = g_strdup_printf ("  [%s]",
                                      wnck_workspace_get_name (workspace));
  else
    workspace_name = g_strdup_printf ("");

  if (!wnck_window_is_minimized (window))
    label = g_strdup_printf ("_%d %s%s%s%s", (i + 1) % 10, group_name, sep,
                             name, workspace_name);
  else
    label = g_strdup_printf ("[_%d %s%s%s%s]", (i + 1) % 10, group_name, sep,
                             name, workspace_name);
  g_free (workspace_name);
  return label;
}

static GtkWidget *
menu_add_window (GtkWidget *menu, WnckWindow *window, int i,
                 gboolean strip_group_name, WnckWorkspace *active)
{
  WnckClassGroup *group = wnck_window_get_class_group (window);
  char const *group_name = wnck_class_group_get_name (group);
  char *label = window_get_label (window, i, strip_group_name, active);
  GtkWidget *menu_item = _menu_add_window (menu, window, label);
  if (window)
    {
      g_signal_connect_swapped (menu_item, "activate",
                                G_CALLBACK (window_workspace_window_activate),
                                window);
#ifdef STANDALONE_HACK
      if (standalone_p)
        g_signal_connect (menu_item, "activate",
                          G_CALLBACK (standalone_main_widget_delete_event), 0);
#endif
    }

  g_free (label);
  return menu_item;
}

static gboolean
window_list_menu_keypad (Window_list *window_list)
{
  int i;
  GList *car, *list;
  GSList *scar;
  WnckScreen *screen;
  GList *windows;

  if (window_list->no_windows_item)
    g_object_unref (window_list->no_windows_item);
  window_list->no_windows_item = NULL;

  g_signal_connect (window_list->menu, "destroy",
                    G_CALLBACK (widget_destroy_window_list_menu), window_list);

  screen = window_list_get_screen (window_list);
  windows = wnck_screen_get_windows (screen);

  GSList *grouped[CLASS_GROUP_COUNT];
  for (i = 0; i < CLASS_GROUP_COUNT; i++)
    grouped[i] = NULL;

  /* TODO: ordering of group, use currently selected and workspace in
     ordering.  */
  for (car = windows; car; car = car->next)
    if (!wnck_window_is_skip_tasklist (car->data))
      {
        int n = class_group_number (car->data);
        grouped[n] = g_slist_append (grouped[n], car->data);
      }

  WnckWorkspace *active = window_list_get_active_workspace (window_list);

  GtkAccelGroup *accel_group = widget_get_accel_group (window_list->main);
  for (i = 0; i < CLASS_GROUP_COUNT; i++)
    if (windows && grouped[i])
      {
        WnckClassGroup *group = wnck_window_get_class_group (grouped[i]->data);
        char const *group_name = wnck_class_group_get_name (group);

        if (!grouped[i]->next)
          {
            GtkWidget *menu_item = menu_add_window (window_list->menu,
                                                    grouped[i]->data, i, 0,
                                                    active);
          }
        else
          {
            char *label = g_strdup_printf ("_%d %s", (i + 1) % 10, group_name);
            GtkWidget *menu_item = _menu_add_window (window_list->menu,
                                                     grouped[i]->data, label);

            GtkWidget *sub_menu = gtk_menu_new ();
            gtk_menu_item_set_submenu (GTK_MENU_ITEM (menu_item), sub_menu);
            for (scar = grouped[i]; scar; scar = scar->next)
              menu_add_window (sub_menu, scar->data, i, 1, active);
          }
      }

  car = GTK_MENU_SHELL (window_list->menu)->children;
  if (0 && !car)
    {
      window_list->no_windows_item
        = gtk_image_menu_item_new_with_mnemonic (_ ("No Windows Open"));

      gtk_widget_set_sensitive (window_list->no_windows_item, FALSE);
      gtk_widget_show (window_list->no_windows_item);   
      gtk_menu_shell_append (GTK_MENU_SHELL (window_list->menu),
                             window_list->no_windows_item);
    }
  return car != 0;
}

static gboolean
window_list_popup_menu (Window_list *window_list)
{
  GList *car, *list;
  int is_first = 0;

  if (!window_list->menu)
    {
      GtkAccelGroup *accel_group = widget_get_accel_group (window_list->main);
      is_first = 1;
      window_list->menu = gtk_menu_new ();
      g_signal_connect (window_list->menu, "hide",
                        G_CALLBACK (widget_hide_menu), window_list);

      gtk_window_add_accel_group (GTK_WINDOW (window_list->main), accel_group);
      gtk_menu_set_accel_group (GTK_MENU (window_list->menu), accel_group);

      gtk_widget_show (window_list->menu);
      gtk_menu_set_screen (GTK_MENU (window_list->menu),
                           gtk_widget_get_screen (window_list->main));

      gtk_frame_set_shadow_type (GTK_FRAME (window_list->frame),
                                 GTK_SHADOW_IN);
    }

  list = gtk_container_get_children (GTK_CONTAINER (window_list->menu));
  for (car = list; car; car = car->next)
    {
      if (window_list->no_windows_item
          && car->data == window_list->no_windows_item)
        window_list->no_windows_item = NULL;
      gtk_container_remove (GTK_CONTAINER (window_list->menu), car->data);
    }
  g_list_free (list);

  gboolean have_windows_p;
  /* Your gnome-guile hook here.  */
  have_windows_p = window_list_menu_keypad (window_list);

  if (is_first || ! have_windows_p)
    return FALSE;

  int button, activate_time = 0;
  gtk_menu_popup (GTK_MENU (window_list->menu),
                  NULL, NULL,
                  window_list_position_menu,
                  window_list->frame,
                  button, activate_time);

  return TRUE;
}

static gboolean
window_list_popup (GtkWidget *main, GdkEventFocus *event,
                   Window_list *window_list)
{
  if (!standalone_p)
    gtk_window_iconify (GTK_WINDOW (main));
  window_list_popup_menu (window_list);
  return TRUE;
}

Window_list *
main_fill (GtkWidget *main)
{
  Window_list *window_list;
  GtkWidget *alignment;
  AtkObject *atk_obj;

  window_list = g_new0 (Window_list, 1);
  window_list->main = main;

  atk_obj = gtk_widget_get_accessible (window_list->main);
  atk_object_set_name (atk_obj, _("Keyboard Window Selector"));
  atk_object_set_description (atk_obj,
                              _ ("Select window using keyboard."));

  g_signal_connect (window_list->main, "destroy",
                    G_CALLBACK (widget_destroy_window_list), window_list);

  window_list->frame = gtk_frame_new (NULL);
  gtk_container_set_border_width (GTK_CONTAINER (window_list->frame), 0);
  gtk_frame_set_shadow_type (GTK_FRAME (window_list->frame), GTK_SHADOW_NONE);
  gtk_widget_show (window_list->frame);
  gtk_container_add (GTK_CONTAINER (window_list->main), window_list->frame);
  return window_list;
}

int
main (int argc, char **argv)
{
  Window_list *window_list;
  GtkWidget *toplevel_window;

  gnome_init ("keywise", "1.0", argc, argv);
  toplevel_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_signal_connect (GTK_OBJECT (toplevel_window), "delete_event",
                      (GtkSignalFunc) main_widget_delete_event, NULL);
  window_list = main_fill (toplevel_window);
  window_list_popup_menu (window_list);

  /* This is kludgy: the toplevel window that we do not want to show,
     gets a focus/uniconify event, and we catch that to popup the
     menu.  */
  g_signal_connect (GTK_WIDGET (toplevel_window), "focus-in-event",
                    G_CALLBACK (window_list_popup), window_list);

  gtk_window_set_decorated (GTK_WINDOW (toplevel_window), FALSE);
  gtk_window_set_skip_taskbar_hint (GTK_WINDOW (toplevel_window), TRUE);

  gtk_window_set_gravity (GTK_WINDOW (toplevel_window),
                          GDK_GRAVITY_NORTH_WEST);
  gtk_window_move (GTK_WINDOW (toplevel_window), 0, 0);

#ifdef STANDALONE_HACK
  /* Metacity hack: bind run_command to keywise --standalone.  */
  if (argc >= 2 && !strcmp ("--standalone", argv[1]))
    {
      standalone_p = 1;
      //gtk_window_deiconify (GTK_WINDOW (toplevel_window));
      window_list_popup_menu (window_list);
      window_list_popup_menu (window_list);
    }
  else if (argc >= 2 && !strcmp ("--emacs", argv[1]))
    {
      standalone_p = 1;
      fprintf (stderr, "select EMACS\n");
      window_list_popup_menu (window_list);
    }
  else
#endif
    /* We do not want to see the window... */
    gtk_window_iconify (GTK_WINDOW (toplevel_window));

  /* Show the toplevel window to get it to appear in sawfish's
     (managed-windows), which enables to focus it with a
     keystroke.  */
  gtk_widget_show (toplevel_window);

  gtk_main ();
  return 0;
}


-- 
Jan Nieuwenhuizen <janneke gnu org> | GNU LilyPond - The music typesetter
http://www.xs4all.nl/~jantien       | http://www.lilypond.org




[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]