[mutter/wip/carlosg/frames-client: 26/39] frames: Add new X11 frames client




commit 364f57b391e55f1bc243805017a6a9fff8eda7f2
Author: Carlos Garnacho <carlosg gnome org>
Date:   Tue Dec 21 21:11:05 2021 +0100

    frames: Add new X11 frames client
    
    This small X11 client takes care of creating frames for client
    windows, Mutter will use this client to delegate window frame
    rendering and event handling.
    
    The MetaWindowTracker object will keep track of windows created
    from other clients, and will await for _MUTTER_NEEDS_FRAME property
    updates on those (coming from Mutter), indicating the need for a
    frame window.
    
    This process is resilient to restarts of the frames client, existing
    windows will be queried during start, and the existence of relevant
    properties checked. Mutter will be able to just hide/show
    SSD-decorated windows while the frames client restarts.
    
    The frames are created through GTK4 widgets, the MetaWindowContent
    widget acts as a replacement prop for the actual client window,
    and the MetaFrameHeader wraps GtkHeaderBar so that windows can be
    overshrunk, but otherwise a MetaFrame is a 100% true GTK4 GtkWindow.
    
    After a frame window is created for a client window, the
    _MUTTER_FRAME_FOR property will be set on the frame window,
    indicating to mutter the correspondence between both Windows.
    
    Additionally, the pixel sizes of the visible left/right/top/bottom
    borders of the frame will be set through the _MUTTER_FRAME_EXTENTS
    property, set on the frame window.
    
    In order to make the frame window behave as the frame for the
    client window, a number of properties will be tracked from the
    client window to update the relevant frame behavior (window title,
    resizability, availability of actions...), and also some forwarding
    of events happening in the frame will be forwarded to the client
    window (mainly, WM_DELETE_WINDOW when the close button is clicked).
    
    Other than that, the frames are pretty much CSD GTK4 windows, so
    window drags and resizes, and window context menus are forwarded for
    the WM to handle.

 meson.build                      |   2 +
 src/frames/main.c                |  61 +++++++
 src/frames/meson.build           |  24 +++
 src/frames/meta-frame-content.c  | 199 ++++++++++++++++++++
 src/frames/meta-frame-content.h  |  37 ++++
 src/frames/meta-frame-header.c   | 125 +++++++++++++
 src/frames/meta-frame-header.h   |  31 ++++
 src/frames/meta-frame.c          | 312 ++++++++++++++++++++++++++++++++
 src/frames/meta-frame.h          |  35 ++++
 src/frames/meta-window-tracker.c | 380 +++++++++++++++++++++++++++++++++++++++
 src/frames/meta-window-tracker.h |  33 ++++
 src/meson.build                  |   1 +
 12 files changed, 1240 insertions(+)
---
diff --git a/meson.build b/meson.build
index 2605a65585..baff22bf43 100644
--- a/meson.build
+++ b/meson.build
@@ -20,6 +20,7 @@ glib_req = '>= 2.69.0'
 gi_req = '>= 0.9.5'
 graphene_req = '>= 1.10.2'
 gtk3_req = '>= 3.19.8'
+gtk4_req = '>= 4.0.0'
 gdk_pixbuf_req = '>= 2.0'
 uprof_req = '>= 0.3'
 pango_req = '>= 1.46.0'
@@ -105,6 +106,7 @@ mutter_installed_tests_libexecdir = join_paths(
 m_dep = cc.find_library('m', required: true)
 graphene_dep = dependency('graphene-gobject-1.0', version: graphene_req)
 gtk3_dep = dependency('gtk+-3.0', version: gtk3_req)
+gtk4_dep = dependency('gtk4', version: gtk4_req)
 gdk_pixbuf_dep = dependency('gdk-pixbuf-2.0')
 pango_dep = dependency('pango', version: pango_req)
 cairo_dep = dependency('cairo', version: cairo_req)
diff --git a/src/frames/main.c b/src/frames/main.c
new file mode 100644
index 0000000000..e808c85bca
--- /dev/null
+++ b/src/frames/main.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-window-tracker.h"
+
+#include <gdk/x11/gdkx.h>
+#include <X11/extensions/Xfixes.h>
+
+int
+main (int   argc,
+      char *argv[])
+{
+  g_autoptr (MetaWindowTracker) window_tracker = NULL;
+  GdkDisplay *display;
+  GMainLoop *loop;
+  Display *xdisplay;
+
+  /* We do know the desired GDK backend, don't let
+   * anyone tell us otherwise.
+   */
+  g_unsetenv ("GDK_BACKEND");
+
+  /* Rely on the simplest renderer available */
+  g_setenv ("GSK_RENDERER", "cairo", TRUE);
+
+  gdk_set_allowed_backends ("x11");
+
+  gtk_init ();
+
+  display = gdk_display_get_default ();
+
+  xdisplay = gdk_x11_display_get_xdisplay (display);
+  XFixesSetClientDisconnectMode (xdisplay,
+                                 XFixesClientDisconnectFlagTerminate);
+
+  window_tracker = meta_window_tracker_new (display);
+
+  loop = g_main_loop_new (NULL, FALSE);
+  g_main_loop_run (loop);
+  g_main_loop_unref (loop);
+
+  return 0;
+}
diff --git a/src/frames/meson.build b/src/frames/meson.build
new file mode 100644
index 0000000000..1ddccff23d
--- /dev/null
+++ b/src/frames/meson.build
@@ -0,0 +1,24 @@
+x11_frames_sources = [
+  'main.c',
+  'meta-frame.c',
+  'meta-frame-content.c',
+  'meta-frame-header.c',
+  'meta-window-tracker.c',
+]
+
+x11_frames = executable('mutter-x11-frames',
+  sources: x11_frames_sources,
+  dependencies: [
+    gtk4_dep,
+    x11_dep,
+    xext_dep,
+    xfixes_dep,
+    xi_dep,
+  ],
+  c_args: [
+    '-DG_LOG_DOMAIN="mutter-x11-frames"',
+  ],
+  include_directories: top_includepath,
+  install: true,
+  install_dir: get_option('libexecdir'),
+)
diff --git a/src/frames/meta-frame-content.c b/src/frames/meta-frame-content.c
new file mode 100644
index 0000000000..bd9965179a
--- /dev/null
+++ b/src/frames/meta-frame-content.c
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-frame-content.h"
+
+struct _MetaFrameContent
+{
+  GtkWidget parent_instance;
+  Window window;
+  GtkBorder border;
+};
+
+enum {
+  PROP_0,
+  PROP_XWINDOW,
+  PROP_BORDER,
+  N_PROPS
+};
+
+static GParamSpec *props[N_PROPS] = { 0, };
+
+G_DEFINE_TYPE (MetaFrameContent, meta_frame_content, GTK_TYPE_WIDGET)
+
+static void
+meta_frame_content_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  MetaFrameContent *frame_content = META_FRAME_CONTENT (object);
+
+  switch (prop_id)
+    {
+    case PROP_XWINDOW:
+      frame_content->window = (Window) g_value_get_ulong (value);
+      break;
+    case PROP_BORDER:
+      frame_content->border = *(GtkBorder*) g_value_get_boxed (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+meta_frame_content_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  MetaFrameContent *frame_content = META_FRAME_CONTENT (object);
+
+  switch (prop_id)
+    {
+    case PROP_XWINDOW:
+      g_value_set_ulong (value, (gulong) frame_content->window);
+      break;
+    case PROP_BORDER:
+      g_value_set_boxed (value, &frame_content->border);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+meta_frame_content_measure (GtkWidget      *widget,
+                            GtkOrientation  orientation,
+                            int             for_size,
+                            int            *minimum,
+                            int            *natural,
+                            int            *minimum_baseline,
+                            int            *natural_baseline)
+{
+  *minimum_baseline = *natural_baseline = -1;
+  *minimum = *natural = 1;
+}
+
+static void
+meta_frame_content_update_border (MetaFrameContent *content,
+                                  GtkBorder         border)
+{
+  if (content->border.left == border.left &&
+      content->border.right == border.right &&
+      content->border.top == border.top &&
+      content->border.bottom == border.bottom)
+    return;
+
+  content->border = border;
+  g_object_notify (G_OBJECT (content), "border");
+}
+
+static void
+meta_frame_content_size_allocate (GtkWidget *widget,
+                                  int        width,
+                                  int        height,
+                                  int        baseline)
+{
+  MetaFrameContent *content = META_FRAME_CONTENT (widget);
+  GtkWindow *window = GTK_WINDOW (gtk_widget_get_root (widget));
+  double x = 0, y = 0, scale;
+
+  gtk_widget_translate_coordinates (widget,
+                                    GTK_WIDGET (window),
+                                    x, y,
+                                    &x, &y);
+
+  scale = gdk_surface_get_scale_factor (gtk_native_get_surface (GTK_NATIVE (window)));
+
+  meta_frame_content_update_border (content,
+                                    /* FIXME: right/bottom are broken, if they
+                                     * are ever other than 0.
+                                     */
+                                    (GtkBorder) {
+                                      x * scale, 0,
+                                      y * scale, 0,
+                                    });
+}
+
+static void
+meta_frame_content_class_init (MetaFrameContentClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->set_property = meta_frame_content_set_property;
+  object_class->get_property = meta_frame_content_get_property;
+
+  widget_class->measure = meta_frame_content_measure;
+  widget_class->size_allocate = meta_frame_content_size_allocate;
+
+  props[PROP_XWINDOW] = g_param_spec_ulong ("xwindow",
+                                            "X window",
+                                            "X window",
+                                            0, G_MAXULONG, 0,
+                                            G_PARAM_READWRITE |
+                                            G_PARAM_CONSTRUCT_ONLY |
+                                            G_PARAM_STATIC_NAME |
+                                            G_PARAM_STATIC_NICK |
+                                            G_PARAM_STATIC_BLURB);
+  props[PROP_BORDER] = g_param_spec_boxed ("border",
+                                           "Border",
+                                           "Border",
+                                           GTK_TYPE_BORDER,
+                                           G_PARAM_READABLE |
+                                           G_PARAM_EXPLICIT_NOTIFY |
+                                           G_PARAM_STATIC_NAME |
+                                           G_PARAM_STATIC_NICK |
+                                           G_PARAM_STATIC_BLURB);
+
+  g_object_class_install_properties (object_class,
+                                     G_N_ELEMENTS (props),
+                                     props);
+}
+
+static void
+meta_frame_content_init (MetaFrameContent *content)
+{
+}
+
+GtkWidget *
+meta_frame_content_new (Window window)
+{
+  return g_object_new (META_TYPE_FRAME_CONTENT,
+                       "xwindow", window,
+                       NULL);
+}
+
+Window
+meta_frame_content_get_window (MetaFrameContent *content)
+{
+  return content->window;
+}
+
+GtkBorder
+meta_frame_content_get_border (MetaFrameContent *content)
+{
+  return content->border;
+}
diff --git a/src/frames/meta-frame-content.h b/src/frames/meta-frame-content.h
new file mode 100644
index 0000000000..6125ef931b
--- /dev/null
+++ b/src/frames/meta-frame-content.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_FRAME_CONTENT_H
+#define META_FRAME_CONTENT_H
+
+#include <gtk/gtk.h>
+
+#include <X11/Xlib.h>
+
+#define META_TYPE_FRAME_CONTENT (meta_frame_content_get_type ())
+G_DECLARE_FINAL_TYPE (MetaFrameContent, meta_frame_content,
+                      META, FRAME_CONTENT, GtkWidget)
+
+GtkWidget * meta_frame_content_new (Window window);
+
+Window meta_frame_content_get_window (MetaFrameContent *content);
+
+GtkBorder meta_frame_content_get_border (MetaFrameContent *content);
+
+#endif /* META_FRAME_CONTENT_H */
diff --git a/src/frames/meta-frame-header.c b/src/frames/meta-frame-header.c
new file mode 100644
index 0000000000..5f2cda1c5b
--- /dev/null
+++ b/src/frames/meta-frame-header.c
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-frame-header.h"
+
+struct _MetaFrameHeader
+{
+  GtkWidget parent_instance;
+};
+
+G_DEFINE_TYPE (MetaFrameHeader, meta_frame_header, GTK_TYPE_WIDGET)
+
+static void
+meta_frame_header_dispose (GObject *object)
+{
+  GtkWidget *widget = GTK_WIDGET (object);
+  GtkWidget *child;
+
+  child = gtk_widget_get_first_child (widget);
+  if (child)
+    gtk_widget_unparent (child);
+
+  G_OBJECT_CLASS (meta_frame_header_parent_class)->dispose (object);
+}
+
+static void
+meta_frame_header_measure (GtkWidget      *widget,
+                           GtkOrientation  orientation,
+                           int             for_size,
+                           int            *minimum,
+                           int            *natural,
+                           int            *minimum_baseline,
+                           int            *natural_baseline)
+{
+  *minimum_baseline = *natural_baseline = -1;
+
+  if (orientation == GTK_ORIENTATION_HORIZONTAL)
+    {
+      *minimum = *natural = 1;
+    }
+  else
+    {
+      GtkWidget *child;
+
+      child = gtk_widget_get_first_child (widget);
+      gtk_widget_measure (child,
+                          orientation, for_size,
+                          minimum, natural,
+                          minimum_baseline,
+                          natural_baseline);
+    }
+}
+
+static void
+meta_frame_header_size_allocate (GtkWidget *widget,
+                                 int        width,
+                                 int        height,
+                                 int        baseline)
+{
+  GtkWidget *child;
+  int minimum;
+  gboolean shrunk;
+  GtkAllocation child_allocation;
+
+  child = gtk_widget_get_first_child (widget);
+
+  gtk_widget_measure (child,
+                      GTK_ORIENTATION_HORIZONTAL,
+                      height,
+                      &minimum, NULL, NULL, NULL);
+
+  shrunk = width < minimum;
+
+  child_allocation.x = shrunk ? width - minimum : 0;
+  child_allocation.y = 0;
+  child_allocation.width = shrunk ? minimum : width;
+  child_allocation.height = height;
+
+  gtk_widget_size_allocate (child, &child_allocation, baseline);
+}
+
+static void
+meta_frame_header_class_init (MetaFrameHeaderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = meta_frame_header_dispose;
+
+  widget_class->measure = meta_frame_header_measure;
+  widget_class->size_allocate = meta_frame_header_size_allocate;
+}
+
+static void
+meta_frame_header_init (MetaFrameHeader *content)
+{
+  GtkWidget *header_bar;
+
+  header_bar = gtk_header_bar_new ();
+  gtk_widget_insert_before (header_bar, GTK_WIDGET (content), NULL);
+}
+
+GtkWidget *
+meta_frame_header_new (void)
+{
+  return g_object_new (META_TYPE_FRAME_HEADER, NULL);
+}
diff --git a/src/frames/meta-frame-header.h b/src/frames/meta-frame-header.h
new file mode 100644
index 0000000000..758fac8e75
--- /dev/null
+++ b/src/frames/meta-frame-header.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_FRAME_HEADER_H
+#define META_FRAME_HEADER_H
+
+#include <gtk/gtk.h>
+
+#define META_TYPE_FRAME_HEADER (meta_frame_header_get_type ())
+G_DECLARE_FINAL_TYPE (MetaFrameHeader, meta_frame_header,
+                      META, FRAME_HEADER, GtkWidget)
+
+GtkWidget * meta_frame_header_new (void);
+
+#endif /* META_FRAME_HEADER_H */
diff --git a/src/frames/meta-frame.c b/src/frames/meta-frame.c
new file mode 100644
index 0000000000..b137d4f710
--- /dev/null
+++ b/src/frames/meta-frame.c
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-frame.h"
+
+#include "meta-frame-content.h"
+#include "meta-frame-header.h"
+
+#include <gdk/x11/gdkx.h>
+#include <X11/Xatom.h>
+
+struct _MetaFrame
+{
+  GtkWindow parent_instance;
+  GtkWidget *content;
+};
+
+typedef struct {
+  unsigned long flags;
+  unsigned long functions;
+  unsigned long decorations;
+  long input_mode;
+  unsigned long status;
+} MotifWmHints;
+
+#define MWM_FUNC_ALL (1L << 0)
+#define MWM_FUNC_RESIZE (1L << 1)
+#define MWM_FUNC_MINIMIZE (1L << 3)
+#define MWM_FUNC_MAXIMIZE (1L << 4)
+#define MWM_FUNC_CLOSE (1L << 5)
+
+G_DEFINE_TYPE (MetaFrame, meta_frame, GTK_TYPE_WINDOW)
+
+static void
+meta_frame_class_init (MetaFrameClass *klass)
+{
+}
+
+static gboolean
+on_frame_close_request (GtkWindow *window,
+                        gpointer   user_data)
+{
+  GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (window));
+  GtkWidget *content;
+  XClientMessageEvent ev;
+  Window client_xwindow;
+
+  content = gtk_window_get_child (window);
+  if (!content)
+    return FALSE;
+
+  client_xwindow =
+    meta_frame_content_get_window (META_FRAME_CONTENT (content));
+
+  ev.type = ClientMessage;
+  ev.window = client_xwindow;
+  ev.message_type =
+    gdk_x11_get_xatom_by_name_for_display (display, "WM_PROTOCOLS");
+  ev.format = 32;
+  ev.data.l[0] =
+    gdk_x11_get_xatom_by_name_for_display (display, "WM_DELETE_WINDOW");
+  ev.data.l[1] = 0; /* FIXME: missing timestamp */
+
+  gdk_x11_display_error_trap_push (display);
+  XSendEvent (gdk_x11_display_get_xdisplay (display),
+              client_xwindow, False, 0, (XEvent*) &ev);
+  gdk_x11_display_error_trap_pop_ignored (display);
+
+  return TRUE;
+}
+
+static void
+meta_frame_init (MetaFrame *frame)
+{
+  g_signal_connect (frame, "close-request",
+                    G_CALLBACK (on_frame_close_request), NULL);
+}
+
+static void
+meta_frame_update_extents (MetaFrame *frame,
+                           GtkBorder  border)
+{
+  GtkWindow *window = GTK_WINDOW (frame);
+  GdkDisplay *display = gtk_widget_get_display (GTK_WIDGET (frame));
+  GdkSurface *surface;
+  Window xframe;
+  unsigned long data[4];
+
+  surface = gtk_native_get_surface (GTK_NATIVE (window));
+  if (!surface)
+    return;
+
+  data[0] = border.left;
+  data[1] = border.right;
+  data[2] = border.top;
+  data[3] = border.bottom;
+
+  xframe = gdk_x11_surface_get_xid (surface);
+  XChangeProperty (gdk_x11_display_get_xdisplay (display),
+                   xframe,
+                   gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_FRAME_EXTENTS"),
+                   XA_CARDINAL,
+                   32,
+                   PropModeReplace,
+                   (guchar *) &data, 4);
+}
+
+static void
+on_border_changed (GObject    *object,
+                   GParamSpec *pspec,
+                   gpointer    user_data)
+{
+  MetaFrame *frame = user_data;
+  GtkWidget *content;
+  GtkBorder border;
+
+  content = gtk_window_get_child (GTK_WINDOW (frame));
+  border = meta_frame_content_get_border (META_FRAME_CONTENT (content));
+  meta_frame_update_extents (frame, border);
+}
+
+static void
+frame_sync_title (GtkWindow *frame,
+                  Window     client_window)
+{
+  GdkDisplay *display;
+  char *title = NULL;
+  int format;
+  Atom type;
+  unsigned long nitems, bytes_after;
+
+  display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+  XGetWindowProperty (gdk_x11_display_get_xdisplay (display),
+                      client_window,
+                      gdk_x11_get_xatom_by_name_for_display (display,
+                                                             "_NET_WM_NAME"),
+                      0, G_MAXLONG, False,
+                      gdk_x11_get_xatom_by_name_for_display (display,
+                                                             "UTF8_STRING"),
+                      &type, &format,
+                      &nitems, &bytes_after,
+                      (unsigned char **) &title);
+
+  gtk_window_set_title (frame, title);
+  g_free (title);
+}
+
+static void
+frame_sync_motif_wm_hints (GtkWindow *frame,
+                           Window     client_window)
+{
+  GdkDisplay *display;
+  MotifWmHints *mwm_hints = NULL;
+  int format;
+  Atom type;
+  unsigned long nitems, bytes_after;
+  gboolean deletable = TRUE;
+
+  display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+  XGetWindowProperty (gdk_x11_display_get_xdisplay (display),
+                      client_window,
+                      gdk_x11_get_xatom_by_name_for_display (display,
+                                                             "_MOTIF_WM_HINTS"),
+                      0, sizeof (MotifWmHints) / sizeof (long),
+                      False, AnyPropertyType,
+                      &type, &format,
+                      &nitems, &bytes_after,
+                      (unsigned char **) &mwm_hints);
+
+  if (mwm_hints)
+    {
+      if ((mwm_hints->functions & MWM_FUNC_ALL) == 0)
+        deletable = (mwm_hints->functions & MWM_FUNC_CLOSE) != 0;
+      else
+        deletable = (mwm_hints->functions & MWM_FUNC_CLOSE) == 0;
+
+      g_free (mwm_hints);
+    }
+
+  gtk_window_set_deletable (frame, deletable);
+}
+
+static void
+frame_sync_wm_normal_hints (GtkWindow *frame,
+                            Window     client_window)
+{
+  GdkDisplay *display;
+  XSizeHints size_hints;
+  long nitems;
+  gboolean resizable = TRUE;
+
+  display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+  XGetWMNormalHints (gdk_x11_display_get_xdisplay (display),
+                     client_window,
+                     &size_hints,
+                     &nitems);
+
+  if (nitems > 0)
+    {
+      resizable = ((size_hints.flags & PMinSize) == 0 ||
+                   (size_hints.flags & PMaxSize) == 0 ||
+                   size_hints.min_width != size_hints.max_width ||
+                   size_hints.min_height != size_hints.max_height);
+    }
+
+  gtk_window_set_resizable (frame, resizable);
+}
+
+GtkWidget *
+meta_frame_new (Window window)
+{
+  GtkWidget *frame, *header, *content;
+  GdkSurface *surface;
+  int frame_height;
+  double scale;
+
+  frame = g_object_new (META_TYPE_FRAME, NULL);
+
+  header = meta_frame_header_new ();
+
+  gtk_window_set_titlebar (GTK_WINDOW (frame), header);
+
+  content = meta_frame_content_new (window);
+  gtk_window_set_child (GTK_WINDOW (frame), content);
+
+  g_signal_connect (content, "notify::border",
+                    G_CALLBACK (on_border_changed), frame);
+
+  gtk_widget_realize (GTK_WIDGET (frame));
+  surface = gtk_native_get_surface (GTK_NATIVE (frame));
+  gdk_x11_surface_set_frame_sync_enabled (surface, FALSE);
+
+  gtk_widget_measure (header,
+                      GTK_ORIENTATION_VERTICAL, 1,
+                      &frame_height,
+                      NULL, NULL, NULL);
+
+  scale = gdk_surface_get_scale_factor (gtk_native_get_surface (GTK_NATIVE (frame)));
+
+  meta_frame_update_extents (META_FRAME (frame),
+                             (GtkBorder) {
+                               0, 0,
+                               frame_height * scale, 0,
+                             });
+
+  frame_sync_title (GTK_WINDOW (frame), window);
+  frame_sync_motif_wm_hints (GTK_WINDOW (frame), window);
+
+  return frame;
+}
+
+void
+meta_frame_handle_xevent (MetaFrame *frame,
+                          Window     window,
+                          XEvent    *xevent)
+{
+  GdkDisplay *display;
+  GtkWidget *content;
+  gboolean is_frame, is_content;
+  GdkSurface *surface;
+
+  surface = gtk_native_get_surface (GTK_NATIVE (frame));
+  if (!surface)
+    return;
+
+  content = gtk_window_get_child (GTK_WINDOW (frame));
+  if (!content)
+    return;
+
+  is_frame = window == gdk_x11_surface_get_xid (surface);
+  is_content =
+    window == meta_frame_content_get_window (META_FRAME_CONTENT (content));
+
+  if (!is_frame && !is_content)
+    return;
+
+  display = gtk_widget_get_display (GTK_WIDGET (frame));
+
+  if (is_content && xevent->type == PropertyNotify)
+    {
+      if (xevent->xproperty.atom ==
+          gdk_x11_get_xatom_by_name_for_display (display, "_NET_WM_NAME"))
+        frame_sync_title (GTK_WINDOW (frame), xevent->xproperty.window);
+      else if (xevent->xproperty.atom ==
+               gdk_x11_get_xatom_by_name_for_display (display, "_MOTIF_WM_HINTS"))
+        frame_sync_motif_wm_hints (GTK_WINDOW (frame), xevent->xproperty.window);
+      else if (xevent->xproperty.atom ==
+               gdk_x11_get_xatom_by_name_for_display (display, "WM_NORMAL_HINTS"))
+        frame_sync_wm_normal_hints (GTK_WINDOW (frame), xevent->xproperty.window);
+    }
+}
diff --git a/src/frames/meta-frame.h b/src/frames/meta-frame.h
new file mode 100644
index 0000000000..d39dc8218d
--- /dev/null
+++ b/src/frames/meta-frame.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_FRAME_H
+#define META_FRAME_H
+
+#include <gtk/gtk.h>
+#include <X11/Xlib.h>
+
+#define META_TYPE_FRAME (meta_frame_get_type ())
+G_DECLARE_FINAL_TYPE (MetaFrame, meta_frame, META, FRAME, GtkWindow)
+
+GtkWidget * meta_frame_new (Window window);
+
+void meta_frame_handle_xevent (MetaFrame *frame,
+                               Window     window,
+                               XEvent    *xevent);
+
+#endif /* META_FRAME_CONTENT_H */
diff --git a/src/frames/meta-window-tracker.c b/src/frames/meta-window-tracker.c
new file mode 100644
index 0000000000..2408d1d99e
--- /dev/null
+++ b/src/frames/meta-window-tracker.c
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#include "config.h"
+
+#include "meta-window-tracker.h"
+
+#include "meta-frame.h"
+
+#include <gdk/x11/gdkx.h>
+#include <X11/Xatom.h>
+#include <X11/extensions/XInput2.h>
+
+struct _MetaWindowTracker
+{
+  GObject parent_instance;
+  GdkDisplay *display;
+  GHashTable *frames;
+  GHashTable *client_windows;
+  int xinput_opcode;
+};
+
+enum {
+  PROP_0,
+  PROP_DISPLAY,
+  N_PROPS
+};
+
+static GParamSpec *props[N_PROPS] = { 0, };
+
+G_DEFINE_TYPE (MetaWindowTracker, meta_window_tracker, G_TYPE_OBJECT)
+
+static void
+meta_window_tracker_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DISPLAY:
+      window_tracker->display = g_value_get_object (value);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+meta_window_tracker_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DISPLAY:
+      g_value_set_object (value, window_tracker->display);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+set_up_frame (MetaWindowTracker *window_tracker,
+              Window             xwindow)
+{
+  GdkDisplay *display = window_tracker->display;
+  Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+  GdkSurface *surface;
+  Window xframe;
+  unsigned long data[1];
+  GtkWidget *frame;
+
+  /* Double check it's not a request for a frame of our own. */
+  if (g_hash_table_contains (window_tracker->frames,
+                             GUINT_TO_POINTER (xwindow)))
+    return;
+
+  /* Create a frame window */
+  frame = meta_frame_new (xwindow);
+  surface = gtk_native_get_surface (GTK_NATIVE (frame));
+  xframe = gdk_x11_surface_get_xid (surface);
+
+  XAddToSaveSet (xdisplay, xwindow);
+
+  data[0] = xwindow;
+  XChangeProperty (xdisplay,
+                   xframe,
+                   gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_FRAME_FOR"),
+                   XA_WINDOW,
+                   32,
+                   PropModeReplace,
+                   (guchar *) data, 1);
+
+  g_hash_table_insert (window_tracker->frames,
+                       GUINT_TO_POINTER (xframe), frame);
+  g_hash_table_insert (window_tracker->client_windows,
+                       GUINT_TO_POINTER (xwindow), frame);
+  gtk_widget_show (GTK_WIDGET (frame));
+}
+
+static void
+listen_set_up_frame (MetaWindowTracker *window_tracker,
+                     Window             xwindow)
+{
+  GdkDisplay *display = window_tracker->display;
+  Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+  int format;
+  Atom type;
+  unsigned long nitems, bytes_after;
+  unsigned char *data;
+
+  gdk_x11_display_error_trap_push (display);
+
+  XSelectInput (xdisplay, xwindow,
+                PropertyChangeMask | StructureNotifyMask);
+
+  XGetWindowProperty (xdisplay,
+                      xwindow,
+                      gdk_x11_get_xatom_by_name_for_display (display,
+                                                             "_MUTTER_NEEDS_FRAME"),
+                      0, 1,
+                      False, XA_CARDINAL,
+                      &type, &format,
+                      &nitems, &bytes_after,
+                      (unsigned char **) &data);
+
+  if (gdk_x11_display_error_trap_pop (display))
+    return;
+
+  if (nitems > 0 && data[0])
+    set_up_frame (window_tracker, xwindow);
+
+  XFree (data);
+}
+
+static void
+remove_frame (MetaWindowTracker *window_tracker,
+              Window             xwindow)
+{
+  GdkDisplay *display = window_tracker->display;
+  Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+  GtkWidget *frame;
+  GdkSurface *surface;
+  Window xframe;
+
+  frame = g_hash_table_lookup (window_tracker->client_windows,
+                               GUINT_TO_POINTER (xwindow));
+  if (!frame)
+    return;
+
+  surface = gtk_native_get_surface (GTK_NATIVE (frame));
+  xframe = gdk_x11_surface_get_xid (surface);
+
+  gdk_x11_display_error_trap_push (display);
+  XRemoveFromSaveSet (xdisplay, xwindow);
+  gdk_x11_display_error_trap_pop_ignored (display);
+
+  g_hash_table_remove (window_tracker->client_windows,
+                       GUINT_TO_POINTER (xwindow));
+  g_hash_table_remove (window_tracker->frames,
+                       GUINT_TO_POINTER (xframe));
+}
+
+static gboolean
+on_xevent (GdkDisplay *display,
+           XEvent     *xevent,
+           gpointer    user_data)
+{
+  Window xroot = gdk_x11_display_get_xrootwindow (display);
+  Window xwindow = xevent->xany.window;
+  MetaWindowTracker *window_tracker = user_data;
+  GtkWidget *frame;
+
+  if (xevent->type == CreateNotify &&
+      xevent->xcreatewindow.parent == xroot &&
+      !xevent->xcreatewindow.override_redirect &&
+      !g_hash_table_contains (window_tracker->frames,
+                              GUINT_TO_POINTER (xevent->xcreatewindow.window)))
+    {
+      xwindow = xevent->xcreatewindow.window;
+      listen_set_up_frame (window_tracker, xwindow);
+    }
+  else if (xevent->type == DestroyNotify)
+    {
+      xwindow = xevent->xdestroywindow.window;
+      remove_frame (window_tracker, xwindow);
+    }
+  else if (xevent->type == PropertyNotify &&
+           xevent->xproperty.atom ==
+           gdk_x11_get_xatom_by_name_for_display (display, "_MUTTER_NEEDS_FRAME"))
+    {
+      if (xevent->xproperty.state == PropertyNewValue)
+        set_up_frame (window_tracker, xwindow);
+      else if (xevent->xproperty.state == PropertyDelete)
+        remove_frame (window_tracker, xwindow);
+    }
+  else if (xevent->type == PropertyNotify)
+    {
+      frame = g_hash_table_lookup (window_tracker->frames,
+                                   GUINT_TO_POINTER (xwindow));
+
+      if (!frame)
+        {
+          frame = g_hash_table_lookup (window_tracker->client_windows,
+                                       GUINT_TO_POINTER (xwindow));
+        }
+
+      if (frame)
+        meta_frame_handle_xevent (META_FRAME (frame), xwindow, xevent);
+    }
+  else if (xevent->type == GenericEvent &&
+           xevent->xcookie.extension == window_tracker->xinput_opcode)
+    {
+      Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+      XIEvent *xi_event;
+
+      xi_event = (XIEvent *) xevent->xcookie.data;
+
+      if (xi_event->evtype == XI_Leave)
+        {
+          XILeaveEvent *crossing = (XILeaveEvent *) xi_event;
+
+          xwindow = crossing->event;
+          frame = g_hash_table_lookup (window_tracker->frames,
+                                       GUINT_TO_POINTER (xwindow));
+
+          /* When crossing from the frame to the client
+           * window, we may need to restore the cursor to
+           * its default.
+           */
+          if (frame && crossing->detail == XINotifyInferior)
+            XIUndefineCursor (xdisplay, crossing->deviceid, xwindow);
+        }
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+query_xi_extension (MetaWindowTracker *window_tracker,
+                    Display           *xdisplay)
+{
+  int major = 2, minor = 3;
+  int unused;
+
+  if (XQueryExtension (xdisplay,
+                       "XInputExtension",
+                       &window_tracker->xinput_opcode,
+                       &unused,
+                       &unused))
+    {
+      if (XIQueryVersion (xdisplay, &major, &minor) == Success)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+meta_window_tracker_constructed (GObject *object)
+{
+  MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+  GdkDisplay *display = window_tracker->display;
+  Display *xdisplay = gdk_x11_display_get_xdisplay (display);
+  Window xroot = gdk_x11_display_get_xrootwindow (display);
+  Window *windows, ignored1, ignored2;
+  unsigned int i, n_windows;
+
+  G_OBJECT_CLASS (meta_window_tracker_parent_class)->constructed (object);
+
+  query_xi_extension (window_tracker, xdisplay);
+
+  XSelectInput (xdisplay, xroot,
+                KeyPressMask |
+                PropertyChangeMask);
+
+  g_signal_connect (display, "xevent",
+                    G_CALLBACK (on_xevent), object);
+
+  XQueryTree (xdisplay,
+              xroot,
+              &ignored1, &ignored2,
+              &windows, &n_windows);
+
+  for (i = 0; i < n_windows; i++)
+    {
+      XWindowAttributes attrs;
+
+      XGetWindowAttributes (xdisplay,
+                            windows[i],
+                            &attrs);
+
+      if (attrs.override_redirect)
+        continue;
+
+      listen_set_up_frame (window_tracker, windows[i]);
+    }
+
+  XFree (windows);
+}
+
+static void
+meta_window_tracker_finalize (GObject *object)
+{
+  MetaWindowTracker *window_tracker = META_WINDOW_TRACKER (object);
+
+  g_clear_pointer (&window_tracker->frames,
+                   g_hash_table_unref);
+  g_clear_pointer (&window_tracker->client_windows,
+                   g_hash_table_unref);
+
+  G_OBJECT_CLASS (meta_window_tracker_parent_class)->finalize (object);
+}
+
+static void
+meta_window_tracker_class_init (MetaWindowTrackerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->set_property = meta_window_tracker_set_property;
+  object_class->get_property = meta_window_tracker_get_property;
+  object_class->constructed = meta_window_tracker_constructed;
+  object_class->finalize = meta_window_tracker_finalize;
+
+  props[PROP_DISPLAY] = g_param_spec_object ("display",
+                                             "Display",
+                                             "Display",
+                                             GDK_TYPE_DISPLAY,
+                                             G_PARAM_READWRITE |
+                                             G_PARAM_CONSTRUCT_ONLY |
+                                             G_PARAM_STATIC_NAME |
+                                             G_PARAM_STATIC_NICK |
+                                             G_PARAM_STATIC_BLURB);
+
+  g_object_class_install_properties (object_class,
+                                     G_N_ELEMENTS (props),
+                                     props);
+}
+
+static void
+meta_window_tracker_init (MetaWindowTracker *window_tracker)
+{
+  window_tracker->frames =
+    g_hash_table_new_full (NULL, NULL, NULL,
+                           (GDestroyNotify) gtk_window_destroy);
+  window_tracker->client_windows = g_hash_table_new (NULL, NULL);
+}
+
+MetaWindowTracker *
+meta_window_tracker_new (GdkDisplay *display)
+{
+  return g_object_new (META_TYPE_WINDOW_TRACKER,
+                       "display", display,
+                       NULL);
+}
diff --git a/src/frames/meta-window-tracker.h b/src/frames/meta-window-tracker.h
new file mode 100644
index 0000000000..4b52b33e9b
--- /dev/null
+++ b/src/frames/meta-window-tracker.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Carlos Garnacho <carlosg gnome org>
+ */
+
+#ifndef META_WINDOW_TRACKER_H
+#define META_WINDOW_TRACKER_H
+
+#include <gtk/gtk.h>
+#include <X11/Xlib.h>
+
+#define META_TYPE_WINDOW_TRACKER (meta_window_tracker_get_type ())
+G_DECLARE_FINAL_TYPE (MetaWindowTracker, meta_window_tracker,
+                      META, WINDOW_TRACKER,
+                      GObject)
+
+MetaWindowTracker * meta_window_tracker_new (GdkDisplay *display);
+
+#endif /* META_WINDOW_TRACKER_H */
diff --git a/src/meson.build b/src/meson.build
index 5fa5f5f9d0..02c4e3a3ca 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1187,6 +1187,7 @@ pkg.generate(libmutter,
 )
 
 subdir('compositor/plugins')
+subdir('frames')
 
 if have_core_tests
   subdir('tests')


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