From 2b48464ac400547682195fe64dc4ac0b890492f0 Mon Sep 17 00:00:00 2001
From: Antoine Martin <totaam@xpra.org>
Date: Tue, 12 May 2020 10:05:11 +0000
Subject: [PATCH] #2539 add window info to header menu

git-svn-id: https://xpra.org/svn/Xpra/trunk@26322 3bb7dfac-3a0b-4e04-842a-767bc560f471
---
 src/xpra/client/gtk3/window_menu.py     |  18 +-
 src/xpra/client/gtk_base/window_info.py | 247 ++++++++++++++++++++++++
 2 files changed, 261 insertions(+), 4 deletions(-)
 create mode 100644 src/xpra/client/gtk_base/window_info.py

diff --git a/src/xpra/client/gtk3/window_menu.py b/src/xpra/client/gtk3/window_menu.py
index e87a13f8c9..aaf800f4f3 100644
--- a/src/xpra/client/gtk3/window_menu.py
+++ b/src/xpra/client/gtk3/window_menu.py
@@ -19,16 +19,26 @@ def __init__(self, client, window):
 
     def setup_menu(self):
         menu = Gtk.Menu()
-        menu.append(self.make_closemenuitem())
+        #menu.append(self.make_closemenuitem())
         menu.connect("deactivate", self.menu_deactivated)
-        menu.append(self.make_aboutmenuitem())
-        if self.client.client_supports_opengl:
-            menu.append(self.make_openglmenuitem())
+        #menu.append(self.make_aboutmenuitem())
+        menu.append(self.make_infomenuitem())
+        #if self.client.client_supports_opengl:
+        #    menu.append(self.make_openglmenuitem())
         menu.append(self.make_refreshmenuitem())
         menu.append(self.make_reinitmenuitem())
         menu.show_all()
         return menu
 
+    def make_infomenuitem(self):
+        def show_info(*_args):
+            from xpra.client.gtk_base.window_info import WindowInfo
+            wi = WindowInfo(self.client, self.window)
+            wi.show()
+        gl = self.menuitem("Window Information", "information.png", "Window state and details", show_info)
+        gl.set_tooltip_text()
+        return gl
+
     def make_openglmenuitem(self):
         gl = self.checkitem("OpenGL")
         gl.set_tooltip_text("hardware accelerated rendering using OpenGL")
diff --git a/src/xpra/client/gtk_base/window_info.py b/src/xpra/client/gtk_base/window_info.py
new file mode 100644
index 0000000000..ba92c58a8d
--- /dev/null
+++ b/src/xpra/client/gtk_base/window_info.py
@@ -0,0 +1,247 @@
+# -*- coding: utf-8 -*-
+# This file is part of Xpra.
+# Copyright (C) 2020 Antoine Martin <antoine@xpra.org>
+# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
+# later version. See the file COPYING for details.
+
+from gi.repository import Gtk, GLib
+
+from xpra.util import typedict, csv, WORKSPACE_UNSET
+from xpra.os_util import bytestostr
+from xpra.client import mixin_features
+from xpra.common import GRAVITY_STR
+from xpra.gtk_common.gtk_util import (
+    add_close_accel, label,
+    TableBuilder, imagebutton, get_gtk_version_info,
+    )
+from xpra.log import Logger
+
+log = Logger("info")
+
+
+def slabel(text="", tooltip=None, font=None):
+    l = label(text, tooltip, font)
+    l.set_selectable(True)
+    return l
+
+
+def x(self):
+    self.size_constraints = typedict()
+    self.geometry_hints = {}
+    self.pending_refresh = []
+
+
+class WindowInfo(Gtk.Window):
+
+    def __init__(self, client, window):
+        Gtk.Window.__init__(self)
+        add_close_accel(self, self.destroy)
+        self._client = client
+        self._window = window
+        self.is_closed = False
+        self.set_title("Window Information for %s" % window.get_title())
+        self.set_destroy_with_parent(True)
+        self.set_resizable(True)
+        self.set_decorated(True)
+        self.set_size_request(480, 640)
+        self.set_transient_for(window)
+        self.set_icon(client.get_pixbuf("information.png"))
+        self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
+        def window_deleted(*_args):
+            self.is_closed = True
+        self.connect('delete_event', window_deleted)
+
+        tb = TableBuilder(1, 2)
+        self.wid_label = slabel()
+        tb.new_row("Window ID", self.wid_label)
+        self.title_label = slabel()
+        self.title_label.set_line_wrap(True)
+        self.title_label.set_size_request(400, -1)
+        tb.new_row("Title", self.title_label)
+        self.or_image = Gtk.Image()
+        tb.new_row("Override-Redirect", self.or_image)
+        self.state_label = slabel()
+        tb.new_row("State", self.state_label)
+        self.attributes_label = slabel()
+        tb.new_row("Attributes", self.attributes_label)
+        self.focus_image = Gtk.Image()
+        tb.new_row("Focus", self.focus_image)
+        self.button_state_label = slabel()
+        tb.new_row("Button State", self.button_state_label)
+        #self.group_leader_label = slabel()
+        #tb.new_row("Group Leader", self.group_leader_label)
+        tb.new_row("", slabel())
+        self.gravity_label = slabel()
+        tb.new_row("Gravity", self.gravity_label)
+        self.content_type_label = slabel()
+        tb.new_row("Content Type", self.content_type_label)
+        tb.new_row("", slabel())
+        self.pixel_depth_label = slabel()
+        tb.new_row("Pixel Depth", self.pixel_depth_label)
+        self.alpha_image = Gtk.Image()
+        tb.new_row("Alpha Channel", self.alpha_image)
+        self.opengl_image = Gtk.Image()
+        tb.new_row("OpenGL", self.opengl_image)
+        tb.new_row("", slabel())
+        self.geometry_label = slabel()
+        tb.new_row("Geometry", self.geometry_label)
+        self.outer_geometry_label = slabel()
+        tb.new_row("Outer Geometry", self.outer_geometry_label)
+        self.inner_geometry_label = slabel()
+        tb.new_row("Inner Geometry", self.inner_geometry_label)
+        self.offsets_label = slabel()
+        tb.new_row("Offsets", self.offsets_label)
+        self.frame_extents_label = slabel()
+        tb.new_row("Frame Extents", self.frame_extents_label)
+        self.max_size_label = slabel()
+        tb.new_row("Maximum Size", self.max_size_label)
+        self.size_constraints_label = slabel()
+        tb.new_row("Size Constraints", self.size_constraints_label)
+        self.add(tb.get_table())
+
+    def destroy(self, *_args):
+        self.is_closed = True
+        Gtk.Window.destroy(self)
+
+    def show(self):
+        self.populate()
+        super().show_all()
+        GLib.timeout_add(1000, self.populate)
+
+    def populate(self):
+        if self.is_closed:
+            return False
+        self.do_populate()
+        return True
+
+    def do_populate(self):
+        w = self._window
+        if not w:
+            return
+        def dict_str(d):
+            return "\n".join("%s : %s" % (k,v) for k,v in d.items())
+        def get_window_state():
+            state = []
+            for s in ("fullscreen", "maximized",
+                      "above", "below", "shaded", "sticky",
+                      "skip-pager", "skip-taskbar",
+                      "iconified"):
+                #ie: "skip-pager" -> self.window._skip_pager
+                if getattr(w, "_%s" % s.replace("-", "_"), False):
+                    state.append(s)
+            for s in ("modal", ):
+                fn = getattr(w, "get_%s" % s, None)
+                if fn and fn():
+                    state.append(s)
+            return csv(state) or "none"
+        def get_attributes():
+            attr = {}
+            workspace = w.get_desktop_workspace()
+            if workspace>0 and workspace!=WORKSPACE_UNSET:
+                attr["workspace"] = workspace
+            opacity = w.get_opacity()
+            if opacity<1:
+                attr["opacity"] = opacity
+            role = w.get_role()
+            if role:
+                attr["role"] = role
+            #get_type_hint
+            return dict_str(attr)
+        self.wid_label.set_text(str(w._id))
+        self.title_label.set_text(w.get_title())
+        self.bool_icon(self.or_image, w._override_redirect)
+        self.state_label.set_text(get_window_state())
+        self.attributes_label.set_text(get_attributes())
+        self.bool_icon(self.focus_image, w._focused)
+        self.button_state_label.set_text(csv(b for b,s in w.button_state.items() if s) or "none")
+        #self.group_leader_label.set_text(str(w.group_leader))
+        self.gravity_label.set_text(GRAVITY_STR.get(w.window_gravity, "invalid"))
+        self.content_type_label.set_text(w.content_type or "unknown")
+        #geometry:
+        self.pixel_depth_label.set_text(str(w.pixel_depth or 24))
+        self.bool_icon(self.alpha_image, w._window_alpha)
+        self.bool_icon(self.opengl_image, getattr(w._backing, "gl_setup", False))
+        #tells us if this window instance can paint with alpha
+        self._window_alpha = False
+        def geom_str(geom):
+            return "%ix%i at %i,%i" % (geom[2], geom[3], geom[0], geom[1])
+        geom = list(w._pos)+list(w._size)
+        self.geometry_label.set_text(geom_str(geom))
+        geom = list(w.get_position()) + list(w.get_size())
+        self.outer_geometry_label.set_text(geom_str(geom))
+        self.inner_geometry_label.set_text(geom_str(w.get_drawing_area_geometry()))
+        self.offsets_label.set_text(",".join(w.window_offset or []) or "none")
+        self.frame_extents_label.set_text(csv(w._current_frame_extents or []) or "none")
+        self.max_size_label.set_text(csv(w.max_window_size))
+        def hsc(sc):
+            #make the dict more human readable
+            ssc = dict((bytestostr(k),v) for k,v in sc.items())
+            ssc.pop("gravity", None)
+            return dict_str(ssc)
+        self.size_constraints_label.set_text(hsc(w.size_constraints))
+
+    def bool_icon(self, image, on_off):
+        c = self._client
+        if not c:
+            return
+        if on_off:
+            icon = c.get_pixbuf("ticked-small.png")
+        else:
+            icon = c.get_pixbuf("unticked-small.png")
+        image.set_from_pixbuf(icon)
+
+    def show_opengl_state(self):
+        if self.client.opengl_enabled:
+            glinfo = "%s / %s" % (
+                self.client.opengl_props.get("vendor", ""),
+                self.client.opengl_props.get("renderer", ""),
+                )
+            display_mode = self.client.opengl_props.get("display_mode", [])
+            bit_depth = self.client.opengl_props.get("depth", 0)
+            info = []
+            if bit_depth:
+                info.append("%i-bit" % bit_depth)
+            if "DOUBLE" in display_mode:
+                info.append("double buffering")
+            elif "SINGLE" in display_mode:
+                info.append("single buffering")
+            else:
+                info.append("unknown buffering")
+            if "ALPHA" in display_mode:
+                info.append("with transparency")
+            else:
+                info.append("without transparency")
+        else:
+            #info could be telling us that the gl bindings are missing:
+            glinfo = self.client.opengl_props.get("info", "disabled")
+            info = ["n/a"]
+        self.client_opengl_label.set_text(glinfo)
+        self.opengl_buffering.set_text(" ".join(info))
+
+    def show_window_renderers(self):
+        if not mixin_features.windows:
+            return
+        wr = []
+        renderers = {}
+        for wid, window in tuple(self.client._id_to_window.items()):
+            renderers.setdefault(window.get_backing_class(), []).append(wid)
+        for bclass, windows in renderers.items():
+            wr.append("%s (%i)" % (bclass.__name__.replace("Backing", ""), len(windows)))
+        self.window_rendering.set_text("GTK3: %s" % csv(wr))
+
+        self.bool_icon(self.client_opengl_icon, self.client.client_supports_opengl)
+
+    def get_window_encoder_stats(self):
+        window_encoder_stats = {}
+        #new-style server with namespace (easier):
+        window_dict = self.client.server_last_info.get("window")
+        if window_dict and isinstance(window_dict, dict):
+            for k,v in window_dict.items():
+                try:
+                    wid = int(k)
+                    encoder_stats = v.get("encoder")
+                    if encoder_stats:
+                        window_encoder_stats[wid] = encoder_stats
+                except Exception:
+                    log.error("Error: cannot lookup window dict", exc_info=True)
+        return window_encoder_stats