Blob Blame History Raw
From 60d38a7c6683001ee2beb72b8f0b0beee4f04bb4 Mon Sep 17 00:00:00 2001
From: "Owen W. Taylor" <otaylor@fishsoup.net>
Date: Wed, 21 Oct 2009 18:07:12 -0400
Subject: [PATCH] Add no-focus-windows preference to list windows that
 shouldn't be focused

Notification windows from legacy software that don't set _NET_WM_USER_TIME
can be a huge annoyance for users, since they will pop up and steal focus.

Add:

 no-focus-windows

which is a list of expressions identifying new windows that shouldn't ever
be focused. For example:

 (and (eq class 'Mylegacyapp') (glob name 'New mail*'))

https://bugzilla.gnome.org/show_bug.cgi?id=599248
---
 src/Makefile.am                       |    2 +
 src/core/prefs.c                      |   55 +++
 src/core/window-matcher.c             |  582 +++++++++++++++++++++++++++++++++
 src/core/window-matcher.h             |   46 +++
 src/core/window.c                     |    9 +-
 src/include/prefs.h                   |    6 +-
 src/metacity-schemas.convert          |    1 +
 src/org.gnome.metacity.gschema.xml.in |   21 ++
 8 files changed, 720 insertions(+), 2 deletions(-)
 create mode 100644 src/core/window-matcher.c
 create mode 100644 src/core/window-matcher.h

diff --git a/src/Makefile.am b/src/Makefile.am
index 4d405bf..2befe33 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -66,6 +66,8 @@ metacity_SOURCES= 				\
 	core/stack.h				\
 	core/util.c				\
 	include/util.h				\
+	core/window-matcher.c			\
+	core/window-matcher.h			\
 	core/window-props.c			\
 	core/window-props.h			\
 	core/window.c				\
diff --git a/src/core/prefs.c b/src/core/prefs.c
index 58f11e9..24a98cd 100644
--- a/src/core/prefs.c
+++ b/src/core/prefs.c
@@ -26,6 +26,7 @@
 
 #include <config.h>
 #include "prefs.h"
+#include "window-matcher.h"
 #include "ui.h"
 #include "util.h"
 #include <glib.h>
@@ -70,6 +71,7 @@ static PangoFontDescription *titlebar_font = NULL;
 static MetaVirtualModifier mouse_button_mods = Mod1Mask;
 static GDesktopFocusMode focus_mode = G_DESKTOP_FOCUS_MODE_CLICK;
 static GDesktopFocusNewWindows focus_new_windows = G_DESKTOP_FOCUS_NEW_WINDOWS_SMART;
+static GSList *no_focus_windows = NULL;
 static gboolean raise_on_click = TRUE;
 static char* current_theme = NULL;
 static int num_workspaces = 4;
@@ -120,6 +122,7 @@ static void maybe_give_disable_workarounds_warning (void);
 
 static gboolean titlebar_handler (GVariant*, gpointer*, gpointer);
 static gboolean theme_name_handler (GVariant*, gpointer*, gpointer);
+static gboolean no_focus_windows_handler (GVariant*, gpointer*, gpointer);
 static gboolean mouse_button_mods_handler (GVariant*, gpointer*, gpointer);
 static gboolean button_layout_handler (GVariant*, gpointer*, gpointer);
 
@@ -367,6 +370,14 @@ static MetaStringPreference preferences_string[] =
       NULL,
     },
     {
+      { "no-focus-windows",
+        SCHEMA_METACITY,
+        META_PREF_NO_FOCUS_WINDOWS,
+      },
+      no_focus_windows_handler,
+      NULL
+    },
+    {
       { KEY_TITLEBAR_FONT,
         SCHEMA_GENERAL,
         META_PREF_TITLEBAR_FONT,
@@ -998,6 +1009,39 @@ theme_name_handler (GVariant *value,
 }
 
 static gboolean
+no_focus_windows_handler (GVariant *value,
+                          gpointer *result,
+                          gpointer  data)
+{
+  const gchar *string_value;
+
+  *result = NULL; /* ignored */
+  string_value = g_variant_get_string (value, NULL);
+
+  if (no_focus_windows)
+    {
+      meta_window_matcher_list_free (no_focus_windows);
+      no_focus_windows = NULL;
+    }
+
+  if (string_value)
+    {
+      GError *error = NULL;
+      no_focus_windows = meta_window_matcher_list_from_string (string_value, &error);
+      if (error != NULL)
+        {
+          meta_warning ("Error parsing no_focus_windows='%s': %s\n",
+                        string_value, error->message);
+          g_error_free (error);
+
+          return FALSE;
+        }
+    }
+
+  return TRUE;
+}
+
+static gboolean
 mouse_button_mods_handler (GVariant *value,
                            gpointer *result,
                            gpointer  data)
@@ -1414,6 +1458,9 @@ meta_preference_to_string (MetaPreference pref)
 
     case META_PREF_FORCE_FULLSCREEN:
       return "FORCE_FULLSCREEN";
+
+    case META_PREF_NO_FOCUS_WINDOWS:
+      return "NO_FOCUS_WINDOWS";
     }
 
   return "(unknown)";
@@ -1710,6 +1757,14 @@ meta_prefs_get_action_right_click_titlebar (void)
 }
 
 gboolean
+meta_prefs_window_is_no_focus (const char *window_name,
+                               const char *window_class)
+{
+  return meta_window_matcher_list_matches (no_focus_windows,
+                                           window_name, window_class);
+}
+
+gboolean
 meta_prefs_get_auto_raise (void)
 {
   return auto_raise;
diff --git a/src/core/window-matcher.c b/src/core/window-matcher.c
new file mode 100644
index 0000000..df889eb
--- /dev/null
+++ b/src/core/window-matcher.c
@@ -0,0 +1,582 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Tiny language for matching against windows */
+
+/*
+ * Copyright (C) 2009 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ */
+
+#include <glib.h>
+#include <string.h>
+
+#include "window-matcher.h"
+
+typedef struct _MetaWindowMatcher MetaWindowMatcher;
+
+typedef enum {
+  MATCHER_OPERAND_CLASS,
+  MATCHER_OPERAND_NAME
+} MatcherOperand;
+
+typedef enum {
+  MATCHER_TOKEN_AND = G_TOKEN_LAST + 1,
+  MATCHER_TOKEN_OR,
+  MATCHER_TOKEN_NOT,
+  MATCHER_TOKEN_EQ,
+  MATCHER_TOKEN_GLOB,
+  MATCHER_TOKEN_NAME,
+  MATCHER_TOKEN_CLASS
+} MatcherToken;
+
+struct _MetaWindowMatcher {
+  enum {
+    MATCHER_AND,
+    MATCHER_OR,
+    MATCHER_NOT,
+    MATCHER_EQ,
+    MATCHER_GLOB
+  } type;
+
+  union {
+    struct {
+      MetaWindowMatcher *a;
+      MetaWindowMatcher *b;
+    } and;
+    struct {
+      MetaWindowMatcher *a;
+      MetaWindowMatcher *b;
+    } or;
+    struct {
+      MetaWindowMatcher *a;
+    } not;
+    struct {
+      MatcherOperand operand;
+      char *str;
+    } eq;
+    struct {
+      MatcherOperand operand;
+      char *str;
+      GPatternSpec *pattern;
+    } glob;
+  } u;
+};
+
+static void
+meta_window_matcher_free (MetaWindowMatcher *matcher)
+{
+  switch (matcher->type)
+    {
+    case MATCHER_AND:
+      meta_window_matcher_free (matcher->u.and.a);
+      meta_window_matcher_free (matcher->u.and.b);
+      break;
+    case MATCHER_OR:
+      meta_window_matcher_free (matcher->u.or.a);
+      meta_window_matcher_free (matcher->u.or.b);
+      break;
+    case MATCHER_NOT:
+      meta_window_matcher_free (matcher->u.or.a);
+      break;
+    case MATCHER_EQ:
+      g_free (matcher->u.eq.str);
+      break;
+    case MATCHER_GLOB:
+      g_free (matcher->u.glob.str);
+      g_pattern_spec_free (matcher->u.glob.pattern);
+      break;
+    }
+
+  g_slice_free (MetaWindowMatcher, matcher);
+}
+
+void
+meta_window_matcher_list_free (GSList *list)
+{
+  g_slist_foreach (list, (GFunc)meta_window_matcher_free, NULL);
+  g_slist_free (list);
+}
+
+static gboolean
+meta_window_matcher_matches (MetaWindowMatcher *matcher,
+                             const char        *window_name,
+                             const char        *window_class)
+{
+  switch (matcher->type)
+    {
+    case MATCHER_AND:
+      return (meta_window_matcher_matches (matcher->u.and.a, window_name, window_class) &&
+              meta_window_matcher_matches (matcher->u.and.b, window_name, window_class));
+    case MATCHER_OR:
+      return (meta_window_matcher_matches (matcher->u.or.a, window_name, window_class) ||
+              meta_window_matcher_matches(matcher->u.or.b, window_name, window_class));
+    case MATCHER_NOT:
+      return !meta_window_matcher_matches (matcher->u.not.a, window_name, window_class);
+    case MATCHER_EQ:
+      if (matcher->u.eq.operand == MATCHER_OPERAND_NAME)
+        return window_name && strcmp (matcher->u.eq.str, window_name) == 0;
+      else
+        return window_class && strcmp (matcher->u.eq.str, window_class) == 0;
+    case MATCHER_GLOB:
+      if (matcher->u.glob.operand == MATCHER_OPERAND_NAME)
+        return window_name && g_pattern_match_string (matcher->u.glob.pattern, window_name);
+      else
+        return window_class && g_pattern_match_string (matcher->u.glob.pattern, window_class);
+    }
+
+  g_assert_not_reached();
+  return FALSE;
+}
+
+gboolean
+meta_window_matcher_list_matches (GSList     *list,
+                                  const char *window_name,
+                                  const char *window_class)
+{
+  GSList *l;
+
+  for (l = list; l; l = l->next)
+    {
+      if (meta_window_matcher_matches (l->data, window_name, window_class))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static const GScannerConfig scanner_config =
+{
+  " \t\r\n"             /* cset_skip_characters */,
+  (
+   G_CSET_a_2_z
+   "_"
+   G_CSET_A_2_Z
+   )                    /* cset_identifier_first */,
+  (
+   G_CSET_a_2_z
+   "_"
+   G_CSET_A_2_Z
+   G_CSET_DIGITS
+   G_CSET_LATINS
+   G_CSET_LATINC
+   )                    /* cset_identifier_nth */,
+  NULL                  /* cpair_comment_single */,
+  TRUE                  /* case_sensitive */,
+  TRUE                  /* skip_comment_multi */,
+  FALSE                 /* skip_comment_single */,
+  TRUE                  /* scan_comment_multi */,
+  TRUE                  /* scan_identifier */,
+  TRUE                  /* scan_identifier_1char */,
+  FALSE                 /* scan_identifier_NULL */,
+  TRUE                  /* scan_symbols */,
+  FALSE                 /* scan_binary */,
+  TRUE                  /* scan_octal */,
+  TRUE                  /* scan_float */,
+  TRUE                  /* scan_hex */,
+  FALSE                 /* scan_hex_dollar */,
+  TRUE                  /* scan_string_sq */,
+  TRUE                  /* scan_string_dq */,
+  TRUE                  /* numbers_2_int */,
+  FALSE                 /* int_2_float */,
+  FALSE                 /* identifier_2_string */,
+  TRUE                  /* char_2_token */,
+  TRUE                  /* symbol_2_token */,
+  FALSE                 /* scope_0_fallback */,
+  FALSE                 /* store_int64 */,
+};
+
+static void
+set_error (GScanner   *scanner,
+           GError    **error,
+           const char *message)
+{
+  g_set_error (error, 0, 0,
+               "Parse error at %d:%d: %s",
+               g_scanner_cur_line (scanner),
+               g_scanner_cur_position (scanner),
+               message);
+}
+
+static MetaWindowMatcher *
+meta_window_matcher_new_and (MetaWindowMatcher *a,
+                             MetaWindowMatcher *b)
+{
+  MetaWindowMatcher *matcher = g_slice_new0 (MetaWindowMatcher);
+
+  matcher->type = MATCHER_AND;
+  matcher->u.and.a = a;
+  matcher->u.and.b = b;
+
+  return matcher;
+}
+
+static MetaWindowMatcher *
+meta_window_matcher_new_or (MetaWindowMatcher *a,
+                            MetaWindowMatcher *b)
+{
+  MetaWindowMatcher *matcher = g_slice_new0 (MetaWindowMatcher);
+
+  matcher->type = MATCHER_OR;
+  matcher->u.or.a = a;
+  matcher->u.or.b = b;
+
+  return matcher;
+}
+
+static MetaWindowMatcher *
+meta_window_matcher_new_not (MetaWindowMatcher *a)
+{
+  MetaWindowMatcher *matcher = g_slice_new0 (MetaWindowMatcher);
+
+  matcher->type = MATCHER_NOT;
+  matcher->u.not.a = a;
+
+  return matcher;
+}
+
+static MetaWindowMatcher *
+meta_window_matcher_new_eq (MatcherOperand operand,
+                            const char    *str)
+{
+  MetaWindowMatcher *matcher = g_slice_new0 (MetaWindowMatcher);
+
+  matcher->type = MATCHER_EQ;
+  matcher->u.eq.operand = operand;
+  matcher->u.eq.str = g_strdup (str);
+
+  return matcher;
+}
+
+static MetaWindowMatcher *
+meta_window_matcher_new_glob (MatcherOperand operand,
+                              const char    *str)
+{
+  MetaWindowMatcher *matcher = g_slice_new0 (MetaWindowMatcher);
+
+  matcher->type = MATCHER_GLOB;
+  matcher->u.glob.operand = operand;
+  matcher->u.glob.str = g_strdup (str);
+  matcher->u.glob.pattern = g_pattern_spec_new (str);
+
+  return matcher;
+}
+
+static MetaWindowMatcher *
+meta_window_matcher_from_scanner (GScanner *scanner,
+                                  GError  **error)
+{
+  MetaWindowMatcher *matcher = NULL;
+  GTokenType token;
+  GTokenValue value;
+
+  token = g_scanner_get_next_token (scanner);
+  if (token != G_TOKEN_LEFT_PAREN)
+    {
+      set_error (scanner, error, "expected '('");
+      return NULL;
+    }
+
+  token = g_scanner_get_next_token (scanner);
+  switch ((MatcherToken) token)
+    {
+    case MATCHER_TOKEN_AND:
+    case MATCHER_TOKEN_OR:
+    case MATCHER_TOKEN_NOT:
+      {
+        MetaWindowMatcher *a, *b;
+
+        a = meta_window_matcher_from_scanner (scanner, error);
+        if (!a)
+          return NULL;
+
+        if ((MatcherToken) token != MATCHER_TOKEN_NOT)
+          {
+            b = meta_window_matcher_from_scanner (scanner, error);
+            if (!b)
+              {
+                meta_window_matcher_free (a);
+                return NULL;
+              }
+          }
+
+        switch ((MatcherToken) token)
+          {
+          case MATCHER_TOKEN_AND:
+            matcher = meta_window_matcher_new_and (a, b);
+            break;
+          case MATCHER_TOKEN_OR:
+            matcher = meta_window_matcher_new_or (a, b);
+            break;
+          case MATCHER_TOKEN_NOT:
+            matcher = meta_window_matcher_new_not (a);
+            break;
+          default:
+            g_assert_not_reached();
+            break;
+          }
+      }
+      break;
+    case MATCHER_TOKEN_EQ:
+    case MATCHER_TOKEN_GLOB:
+      {
+        MatcherOperand operand;
+
+        switch ((MatcherToken) g_scanner_get_next_token (scanner))
+          {
+          case MATCHER_TOKEN_NAME:
+            operand = MATCHER_OPERAND_NAME;
+            break;
+          case MATCHER_TOKEN_CLASS:
+            operand = MATCHER_OPERAND_CLASS;
+            break;
+          default:
+            set_error (scanner, error, "expected name/class");
+            return NULL;
+          }
+
+        if (g_scanner_get_next_token (scanner) != G_TOKEN_STRING)
+          {
+            set_error (scanner, error, "expected string");
+            return NULL;
+          }
+
+        value = g_scanner_cur_value (scanner);
+
+        switch ((MatcherToken) token)
+          {
+          case MATCHER_TOKEN_EQ:
+            matcher = meta_window_matcher_new_eq (operand, value.v_string);
+            break;
+          case MATCHER_TOKEN_GLOB:
+            matcher = meta_window_matcher_new_glob (operand, value.v_string);
+            break;
+          default:
+            g_assert_not_reached();
+          }
+      }
+      break;
+    default:
+      set_error (scanner, error, "expected and/or/not/eq/glob");
+      return NULL;
+    }
+
+  if (g_scanner_get_next_token (scanner) != G_TOKEN_RIGHT_PAREN)
+    {
+      set_error (scanner, error, "expected ')'");
+      return NULL;
+    }
+
+  return matcher;
+}
+
+GSList *
+meta_window_matcher_list_from_string (const char *str,
+                                      GError    **error)
+{
+  GScanner *scanner = g_scanner_new (&scanner_config);
+  GSList *result = NULL;
+
+  g_scanner_scope_add_symbol (scanner, 0, "and", GINT_TO_POINTER (MATCHER_TOKEN_AND));
+  g_scanner_scope_add_symbol (scanner, 0, "or", GINT_TO_POINTER (MATCHER_TOKEN_OR));
+  g_scanner_scope_add_symbol (scanner, 0, "not", GINT_TO_POINTER (MATCHER_TOKEN_NOT));
+  g_scanner_scope_add_symbol (scanner, 0, "eq", GINT_TO_POINTER (MATCHER_TOKEN_EQ));
+  g_scanner_scope_add_symbol (scanner, 0, "glob", GINT_TO_POINTER (MATCHER_TOKEN_GLOB));
+  g_scanner_scope_add_symbol (scanner, 0, "name", GINT_TO_POINTER (MATCHER_TOKEN_NAME));
+  g_scanner_scope_add_symbol (scanner, 0, "class", GINT_TO_POINTER (MATCHER_TOKEN_CLASS));
+
+  g_scanner_input_text (scanner, str, strlen (str));
+
+  while (g_scanner_peek_next_token (scanner) != G_TOKEN_EOF)
+    {
+      MetaWindowMatcher *matcher = meta_window_matcher_from_scanner (scanner, error);
+      if (!matcher)
+        {
+          meta_window_matcher_list_free (result);
+          return NULL;
+        }
+
+      result = g_slist_prepend (result, matcher);
+    }
+
+  g_scanner_destroy (scanner);
+
+  return g_slist_reverse (result);
+}
+
+#ifdef BUILD_MATCHER_TESTS
+
+static void
+append_operand_to_string (GString       *string,
+                          MatcherOperand operand)
+{
+  if (operand == MATCHER_OPERAND_NAME)
+    g_string_append (string, "name");
+  else
+    g_string_append (string, "class");
+}
+
+static void
+append_string_to_string (GString            *str,
+                         const char         *to_append)
+{
+  const char *p;
+
+  g_string_append_c (str, '"');
+  for (p = to_append; *p; p++)
+    {
+      if (*p == '"')
+        g_string_append (str, "\\\"");
+      else
+        g_string_append_c (str, *p);
+    }
+  g_string_append_c (str, '"');
+}
+
+static void
+append_matcher_to_string (GString           *str,
+                          MetaWindowMatcher *matcher)
+{
+  switch (matcher->type)
+    {
+    case MATCHER_AND:
+      g_string_append (str, "(and ");
+      append_matcher_to_string (str, matcher->u.and.a);
+      g_string_append_c (str, ' ');
+      append_matcher_to_string (str, matcher->u.and.b);
+      break;
+    case MATCHER_OR:
+      g_string_append (str, "(or ");
+      append_matcher_to_string (str, matcher->u.or.a);
+      g_string_append_c (str, ' ');
+      append_matcher_to_string (str, matcher->u.or.b);
+      break;
+    case MATCHER_NOT:
+      g_string_append (str, "(not ");
+      append_matcher_to_string (str, matcher->u.not.a);
+      break;
+    case MATCHER_EQ:
+      g_string_append (str, "(eq ");
+      append_operand_to_string (str, matcher->u.eq.operand);
+      g_string_append_c (str, ' ');
+      append_string_to_string (str, matcher->u.eq.str);
+      break;
+    case MATCHER_GLOB:
+      g_string_append (str, "(glob ");
+      append_operand_to_string (str, matcher->u.glob.operand);
+      g_string_append_c (str, ' ');
+      append_string_to_string (str, matcher->u.glob.str);
+      break;
+    }
+
+  g_string_append_c (str, ')');
+}
+
+static char *
+meta_window_matcher_list_to_string (GSList *list)
+{
+  GSList *l;
+  GString *str = g_string_new (NULL);
+
+  for (l = list; l; l = l->next)
+    {
+      if (str->len > 0)
+        g_string_append_c (str, ' ');
+
+      append_matcher_to_string (str, l->data);
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+static void
+test_roundtrip (const char *str)
+{
+  GError *error = NULL;
+  GSList *list = meta_window_matcher_list_from_string (str, &error);
+  char *result;
+
+  if (error != NULL)
+    g_error ("Failed to parse '%s': %s\n", str, error->message);
+
+  result = meta_window_matcher_list_to_string (list);
+  if (strcmp (result, str) != 0)
+    g_error ("Round-trip conversion of '%s' gave '%s'\n", str, result);
+
+  g_free (result);
+  meta_window_matcher_list_free (list);
+}
+
+static void
+test_matches (const char *str,
+              const char *window_name,
+              const char *window_class,
+              gboolean    expected)
+{
+  GError *error = NULL;
+  GSList *list = meta_window_matcher_list_from_string (str, &error);
+  gboolean matches;
+
+  if (error != NULL)
+    g_error ("Failed to parse '%s': %s\n", str, error->message);
+
+  matches = meta_window_matcher_list_matches (list, window_name, window_class))
+  if (matches != expected)
+    {
+      g_error ("Tested '%s' against name=%s, class=%s, expected %s, got %s\n",
+               str, window_name, window_class,
+               expected ? "true" : "false",
+               matches ? "true" : "false");
+    }
+
+
+  meta_window_matcher_list_free (list);
+}
+
+int main (int argc, char **argv)
+{
+  test_roundtrip ("(eq name \"foo\")");
+  test_roundtrip ("(eq name \"fo\\\"o\")");
+  test_roundtrip ("(glob class \"*bar?baz\")");
+  test_roundtrip ("(and (eq name \"foo\") (glob class \"*bar?baz\"))");
+  test_roundtrip ("(or (eq name \"foo\") (glob class \"*bar?baz\"))");
+  test_roundtrip ("(not (eq name \"foo\"))");
+
+  test_roundtrip ("(eq name \"foo\") (glob class \"*bar?baz\")");
+
+  test_matches ("(eq name 'foo')", "foo", NULL, TRUE);
+  test_matches ("(eq name 'foo')", "foob", NULL, FALSE);
+  test_matches ("(eq name 'foo')", NULL, NULL, FALSE);
+  test_matches ("(eq class 'bar')", "foo", "bar", TRUE);
+  test_matches ("(eq class 'bar')", NULL, NULL, FALSE);
+
+  test_matches ("(glob name 'foo*')", "foooo", NULL, TRUE);
+  test_matches ("(glob name 'foo*')", NULL, NULL, FALSE);
+  test_matches ("(glob class 'b*r')", "foooo", "baaaar", TRUE);
+  test_matches ("(glob class 'b*r')", NULL, NULL, FALSE);
+
+  test_matches ("(and (eq name 'foo') (eq class 'bar'))", "foo", "bar", TRUE);
+  test_matches ("(and (eq name 'foo') (eq class 'bar'))", "foo", "baz", FALSE);
+  test_matches ("(and (eq name 'foo') (not (eq class 'bar')))", "foo", "bar", FALSE);
+  test_matches ("(and (eq name 'foo') (not (eq class 'bar')))", "foo", "baz", TRUE);
+
+  test_matches ("(or (eq name 'foo') (eq class 'bar'))", "foo", "baz", TRUE);
+  test_matches ("(or (eq name 'foo') (eq class 'bar'))", "fof", "baz", FALSE);
+
+  return 0;
+}
+
+#endif /* BUILD_MATCHER_TESTS */
diff --git a/src/core/window-matcher.h b/src/core/window-matcher.h
new file mode 100644
index 0000000..7fc7826
--- /dev/null
+++ b/src/core/window-matcher.h
@@ -0,0 +1,46 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/* Tiny language for matching against windows
+ *
+ * Expression Syntax:
+ *
+ *  (and <expr> <expr>)
+ *  (or <expr> <expr>)
+ *  (not <expr>)
+ *  (eq [name|class] "<value>")
+ *  (glob [name|class] "<glob>")
+ *
+ * A "matcher list" is a whitespace-separated list of expressions that are
+ * implicitly or'ed together. Globs are shell style patterns with
+ * matching 0 or more characters and ? matching one character.
+ */
+
+/*
+ * Copyright (C) 2009 Red Hat, Inc.
+ *
+ * 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.
+ */
+
+#ifndef META_WINDOW_MATCHER_H
+#define META_WINDOW_MATCHER_H
+
+GSList * meta_window_matcher_list_from_string (const char         *str,
+					       GError            **error);
+void     meta_window_matcher_list_free        (GSList             *list);
+gboolean meta_window_matcher_list_matches     (GSList             *list,
+					       const char         *window_name,
+					       const char         *window_class);
+#endif /* META_WINDOW_MATCHER_H */
diff --git a/src/core/window.c b/src/core/window.c
index 2f2f800..5440160 100644
--- a/src/core/window.c
+++ b/src/core/window.c
@@ -1981,7 +1981,14 @@ window_state_on_map (MetaWindow *window,
 {
   gboolean intervening_events;
 
-  intervening_events = intervening_user_event_occurred (window);
+  /* A 'no focus' window is a window that has been configured in GConf
+   * to never take focus on map; typically it will be a notification
+   * window from a legacy app that doesn't support _NET_WM_USER_TIME.
+   */
+  if (meta_prefs_window_is_no_focus (window->title, window->res_class))
+    intervening_events = TRUE;
+  else
+    intervening_events = intervening_user_event_occurred (window);
 
   *takes_focus = !intervening_events;
   *places_on_top = *takes_focus;
diff --git a/src/include/prefs.h b/src/include/prefs.h
index 673cb36..b86843c 100644
--- a/src/include/prefs.h
+++ b/src/include/prefs.h
@@ -60,7 +60,8 @@ typedef enum
   META_PREF_CURSOR_SIZE,
   META_PREF_COMPOSITING_MANAGER,
   META_PREF_RESIZE_WITH_RIGHT_BUTTON,
-  META_PREF_FORCE_FULLSCREEN
+  META_PREF_FORCE_FULLSCREEN,
+  META_PREF_NO_FOCUS_WINDOWS
 } MetaPreference;
 
 typedef void (* MetaPrefsChangedFunc) (MetaPreference pref,
@@ -105,6 +106,9 @@ GDesktopTitlebarAction       meta_prefs_get_action_double_click_titlebar (void);
 GDesktopTitlebarAction       meta_prefs_get_action_middle_click_titlebar (void);
 GDesktopTitlebarAction       meta_prefs_get_action_right_click_titlebar (void);
 
+gboolean                    meta_prefs_window_is_no_focus (const char *window_name,
+                                                           const char *window_class);
+
 void meta_prefs_set_num_workspaces (int n_workspaces);
 
 const char* meta_prefs_get_workspace_name    (int         i);
diff --git a/src/metacity-schemas.convert b/src/metacity-schemas.convert
index 46f3104..9c271c6 100644
--- a/src/metacity-schemas.convert
+++ b/src/metacity-schemas.convert
@@ -1,3 +1,4 @@
 [org.gnome.metacity]
 compositing-manager = /apps/metacity/general/compositing_manager
 reduced-resources = /apps/metacity/general/reduced_resources
+no-focus-windows = /apps/metacity/general/no_focus_windows
diff --git a/src/org.gnome.metacity.gschema.xml.in b/src/org.gnome.metacity.gschema.xml.in
index 8fcdd7c..6900fa6 100644
--- a/src/org.gnome.metacity.gschema.xml.in
+++ b/src/org.gnome.metacity.gschema.xml.in
@@ -22,6 +22,27 @@
         However, the wireframe feature is disabled when accessibility is on.
       </_description>
     </key>
+    <key name="no-focus-windows" type="s">
+      <default>''</default>
+      <_summary>New windows that shouldn't get focus</_summary>
+      <_description>
+        This option provides a way to specify new windows that shouldn't get
+	focus. Normally an application specifies whether or not it gets focus
+	by setting the _NET_WM_USER_TIME property, but legacy applications
+	may not set this, which can cause unwanted focus stealing.
+
+	The contents of this property is a space-separated list of expressions
+	to match against windows. If any of the expressions match a window
+	then the window will not get focus. The syntax of expressions is:
+
+	(eq [name|class] "&lt;value&gt;"): window name (title) or the class from
+	WM_CLASS matches &lt;value&gt; exactly.
+	(glob [name|class] "&lt;glob&gt;"): window name (title) or the class from
+	WM_CLASS matches the shell-style glob pattern &lt;glob&gt;.
+	(and &lt;expr&gt; &lt;expr&gt;) (or &lt;expr&gt; &lt;expr&gt;) (not &lt;expr): Boolean combinations
+	of expressions.
+      </_description>
+    </key>
   </schema>
 
 </schemalist>
-- 
1.7.9