1# -*- coding: utf-8 -*-
2# This file is part of Xpra.
3# Copyright (C) 2011-2021 Antoine Martin <antoine@xpra.org>
4# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
5# later version. See the file COPYING for details.
6
7import os.path
8import cairo
9
10import gi
11gi.require_version("Gdk", "3.0")
12gi.require_version("Gtk", "3.0")
13gi.require_version("Pango", "1.0")
14gi.require_version("GdkPixbuf", "2.0")
15from gi.repository import GLib, GdkPixbuf, Pango, GObject, Gtk, Gdk     #@UnresolvedImport
16
17from xpra.util import first_time, envint, envbool
18from xpra.os_util import strtobytes, WIN32, OSX
19from xpra.log import Logger
20
21log = Logger("gtk", "util")
22screenlog = Logger("gtk", "screen")
23alphalog = Logger("gtk", "alpha")
24
25SHOW_ALL_VISUALS = False
26#try to get workarea from GTK:
27GTK_WORKAREA = envbool("XPRA_GTK_WORKAREA", True)
28SMOOTH_SCROLL = envbool("XPRA_SMOOTH_SCROLL", True)
29
30GTK_VERSION_INFO = {}
31def get_gtk_version_info() -> dict:
32    #update props given:
33    global GTK_VERSION_INFO
34    def av(k, v):
35        GTK_VERSION_INFO.setdefault(k, {})["version"] = v
36    def V(k, module, *fields):
37        for field in fields:
38            v = getattr(module, field, None)
39            if v is not None:
40                av(k, v)
41                return True
42        return False
43
44    if not GTK_VERSION_INFO:
45        V("gobject",    GObject,    "pygobject_version")
46
47        #this isn't the actual version, (only shows as "3.0")
48        #but still better than nothing:
49        V("gi",         gi,         "__version__")
50        V("gtk",        Gtk,        "_version")
51        V("gdk",        Gdk,        "_version")
52        V("gobject",    GObject,    "_version")
53        V("pixbuf",     GdkPixbuf,     "_version")
54
55        V("pixbuf",     GdkPixbuf,     "PIXBUF_VERSION")
56        def MAJORMICROMINOR(name, module):
57            try:
58                v = tuple(getattr(module, x) for x in ("MAJOR_VERSION", "MICRO_VERSION", "MINOR_VERSION"))
59                av(name, ".".join(str(x) for x in v))
60            except Exception:
61                pass
62        MAJORMICROMINOR("gtk",  Gtk)
63        MAJORMICROMINOR("glib", GLib)
64
65        av("cairo", cairo.version_info)  #pylint: disable=no-member
66        av("pango", Pango.version_string())
67    return GTK_VERSION_INFO.copy()
68
69
70def pixbuf_save_to_memory(pixbuf, fmt="png") -> bytes:
71    buf = []
72    def save_to_memory(data, *_args, **_kwargs):
73        buf.append(strtobytes(data))
74        return True
75    pixbuf.save_to_callbackv(save_to_memory, None, fmt, [], [])
76    return b"".join(buf)
77
78
79def GDKWindow(*args, **kwargs) -> Gdk.Window:
80    return new_GDKWindow(Gdk.Window, *args, **kwargs)
81
82def new_GDKWindow(gdk_window_class,
83                  parent=None, width=1, height=1, window_type=Gdk.WindowType.TOPLEVEL,
84                  event_mask=0, wclass=Gdk.WindowWindowClass.INPUT_OUTPUT, title=None,
85                  x=None, y=None, override_redirect=False, visual=None) -> Gdk.Window:
86    attributes_mask = 0
87    attributes = Gdk.WindowAttr()
88    if x is not None:
89        attributes.x = x
90        attributes_mask |= Gdk.WindowAttributesType.X
91    if y is not None:
92        attributes.y = y
93        attributes_mask |= Gdk.WindowAttributesType.Y
94    #attributes.type_hint = Gdk.WindowTypeHint.NORMAL
95    #attributes_mask |= Gdk.WindowAttributesType.TYPE_HINT
96    attributes.width = width
97    attributes.height = height
98    attributes.window_type = window_type
99    if title:
100        attributes.title = title
101        attributes_mask |= Gdk.WindowAttributesType.TITLE
102    if visual:
103        attributes.visual = visual
104        attributes_mask |= Gdk.WindowAttributesType.VISUAL
105    #OR:
106    attributes.override_redirect = override_redirect
107    attributes_mask |= Gdk.WindowAttributesType.NOREDIR
108    #events:
109    attributes.event_mask = event_mask
110    #wclass:
111    attributes.wclass = wclass
112    mask = Gdk.WindowAttributesType(attributes_mask)
113    return gdk_window_class(parent, attributes, mask)
114
115def set_visual(window, alpha : bool=True) -> bool:
116    screen = window.get_screen()
117    if alpha:
118        visual = screen.get_rgba_visual()
119    else:
120        visual = screen.get_system_visual()
121    alphalog("set_visual(%s, %s) screen=%s, visual=%s", window, alpha, screen, visual)
122    #we can't do alpha on win32 with plain GTK,
123    #(though we handle it in the opengl backend)
124    if WIN32 or not first_time("no-rgba"):
125        l = alphalog
126    else:
127        l = alphalog.warn
128    if alpha and visual is None or (not WIN32 and not screen.is_composited()):
129        l("Warning: cannot handle window transparency")
130        if visual is None:
131            l(" no RGBA visual")
132        else:
133            assert not screen.is_composited()
134            l(" screen is not composited")
135        return None
136    alphalog("set_visual(%s, %s) using visual %s", window, alpha, visual)
137    if visual:
138        window.set_visual(visual)
139    return visual
140
141
142def get_pixbuf_from_data(rgb_data, has_alpha : bool, w : int, h : int, rowstride : int) -> GdkPixbuf.Pixbuf:
143    data = GLib.Bytes(rgb_data)
144    return GdkPixbuf.Pixbuf.new_from_bytes(data, GdkPixbuf.Colorspace.RGB,
145                                           has_alpha, 8, w, h, rowstride)
146
147def color_parse(*args) -> Gdk.Color:
148    v = Gdk.RGBA()
149    ok = v.parse(*args)
150    if ok:
151        return v.to_color()
152    ok, v = Gdk.Color.parse(*args)
153    if ok:
154        return v
155    return None
156
157def get_default_root_window() -> Gdk.Window:
158    screen = Gdk.Screen.get_default()
159    if screen is None:
160        return None
161    return screen.get_root_window()
162
163def get_root_size():
164    if OSX:
165        #the easy way:
166        root = get_default_root_window()
167        w, h = root.get_geometry()[2:4]
168    else:
169        #GTK3 on win32 triggers this warning:
170        #"GetClientRect failed: Invalid window handle."
171        #if we try to use the root window,
172        #and on Linux with Wayland, we get bogus values...
173        screen = Gdk.Screen.get_default()
174        if screen is None:
175            return 1920, 1024
176        w = screen.get_width()
177        h = screen.get_height()
178    if w<=0 or h<=0 or w>32768 or h>32768:
179        if first_time("Gtk root window dimensions"):
180            log.warn("Warning: Gdk returned invalid root window dimensions: %ix%i", w, h)
181            w, h = 1920, 1080
182            log.warn(" using %ix%i instead", w, h)
183            if WIN32:
184                log.warn(" no access to the display?")
185    return w, h
186
187def get_default_cursor() -> Gdk.Cursor:
188    display = Gdk.Display.get_default()
189    return Gdk.Cursor.new_from_name(display, "default")
190
191BUTTON_MASK = {
192    Gdk.ModifierType.BUTTON1_MASK : 1,
193    Gdk.ModifierType.BUTTON2_MASK : 2,
194    Gdk.ModifierType.BUTTON3_MASK : 3,
195    Gdk.ModifierType.BUTTON4_MASK : 4,
196    Gdk.ModifierType.BUTTON5_MASK : 5,
197    }
198
199em = Gdk.EventMask
200WINDOW_EVENT_MASK = em.STRUCTURE_MASK | em.KEY_PRESS_MASK | em.KEY_RELEASE_MASK \
201        | em.POINTER_MOTION_MASK | em.BUTTON_PRESS_MASK | em.BUTTON_RELEASE_MASK \
202        | em.PROPERTY_CHANGE_MASK | em.SCROLL_MASK | em.SMOOTH_SCROLL_MASK
203if SMOOTH_SCROLL:
204    WINDOW_EVENT_MASK |= em.SMOOTH_SCROLL_MASK
205
206del em
207
208
209orig_pack_start = Gtk.Box.pack_start
210def pack_start(self, child, expand=True, fill=True, padding=0):
211    orig_pack_start(self, child, expand, fill, padding)
212Gtk.Box.pack_start = pack_start
213
214GRAB_STATUS_STRING = {
215    Gdk.GrabStatus.SUCCESS          : "SUCCESS",
216    Gdk.GrabStatus.ALREADY_GRABBED  : "ALREADY_GRABBED",
217    Gdk.GrabStatus.INVALID_TIME     : "INVALID_TIME",
218    Gdk.GrabStatus.NOT_VIEWABLE     : "NOT_VIEWABLE",
219    Gdk.GrabStatus.FROZEN           : "FROZEN",
220    }
221
222VISUAL_NAMES = {
223    Gdk.VisualType.STATIC_GRAY      : "STATIC_GRAY",
224    Gdk.VisualType.GRAYSCALE        : "GRAYSCALE",
225    Gdk.VisualType.STATIC_COLOR     : "STATIC_COLOR",
226    Gdk.VisualType.PSEUDO_COLOR     : "PSEUDO_COLOR",
227    Gdk.VisualType.TRUE_COLOR       : "TRUE_COLOR",
228    Gdk.VisualType.DIRECT_COLOR     : "DIRECT_COLOR",
229    }
230
231BYTE_ORDER_NAMES = {
232                Gdk.ByteOrder.LSB_FIRST   : "LSB",
233                Gdk.ByteOrder.MSB_FIRST   : "MSB",
234                }
235
236
237def get_screens_info() -> dict:
238    display = Gdk.Display.get_default()
239    info = {}
240    for i in range(display.get_n_screens()):
241        screen = display.get_screen(i)
242        info[i] = get_screen_info(display, screen)
243    return info
244
245def get_screen_sizes(xscale=1, yscale=1):
246    from xpra.platform.gui import get_workarea, get_workareas
247    def xs(v):
248        return round(v/xscale)
249    def ys(v):
250        return round(v/yscale)
251    def swork(*workarea):
252        return xs(workarea[0]), ys(workarea[1]), xs(workarea[2]), ys(workarea[3])
253    display = Gdk.Display.get_default()
254    if not display:
255        return ()
256    MIN_DPI = envint("XPRA_MIN_DPI", 10)
257    MAX_DPI = envint("XPRA_MIN_DPI", 500)
258    def dpi(size_pixels, size_mm):
259        if size_mm==0:
260            return 0
261        return round(size_pixels * 254 / size_mm / 10)
262    #GTK 3.22 onwards always returns just a single screen,
263    #potentially with multiple monitors
264    n_monitors = display.get_n_monitors()
265    workareas = get_workareas()
266    if workareas and len(workareas)!=n_monitors:
267        screenlog(" workareas: %s", workareas)
268        screenlog(" number of monitors does not match number of workareas!")
269        workareas = []
270    monitors = []
271    for j in range(n_monitors):
272        monitor = display.get_monitor(j)
273        geom = monitor.get_geometry()
274        manufacturer, model = monitor.get_manufacturer(), monitor.get_model()
275        if manufacturer=="unknown":
276            manufacturer = ""
277        if model=="unknown":
278            model = ""
279        if manufacturer and model:
280            plug_name = "%s %s" % (manufacturer, model)
281        elif manufacturer:
282            plug_name = manufacturer
283        elif model:
284            plug_name = model
285        else:
286            plug_name = "%i" % j
287        wmm, hmm = monitor.get_width_mm(), monitor.get_height_mm()
288        monitor_info = [plug_name, xs(geom.x), ys(geom.y), xs(geom.width), ys(geom.height), wmm, hmm]
289        screenlog(" monitor %s: %s, model=%s, manufacturer=%s",
290                  j, type(monitor).__name__, monitor.get_model(), monitor.get_manufacturer())
291        if GTK_WORKAREA and hasattr(monitor, "get_workarea"):
292            rect = monitor.get_workarea()
293            monitor_info += list(swork(rect.x, rect.y, rect.width, rect.height))
294        elif workareas:
295            w = workareas[j]
296            monitor_info += list(swork(*w))
297        monitors.append(tuple(monitor_info))
298    screen = display.get_default_screen()
299    sw, sh = screen.get_width(), screen.get_height()
300    work_x, work_y, work_width, work_height = swork(0, 0, sw, sh)
301    workarea = get_workarea()   #pylint: disable=assignment-from-none
302    if workarea:
303        work_x, work_y, work_width, work_height = swork(*workarea)  #pylint: disable=not-an-iterable
304    screenlog(" workarea=%s", workarea)
305    wmm = screen.get_width_mm()
306    hmm = screen.get_height_mm()
307    xdpi = dpi(sw, wmm)
308    ydpi = dpi(sh, hmm)
309    if xdpi<MIN_DPI or xdpi>MAX_DPI or ydpi<MIN_DPI or ydpi>MAX_DPI:
310        log("ignoring invalid screen size %ix%imm", wmm, hmm)
311        if os.environ.get("WAYLAND_DISPLAY"):
312            log(" (wayland display?)")
313        if n_monitors>0:
314            wmm = 0
315            for mi in range(n_monitors):
316                monitor = display.get_monitor(mi)
317                log(" monitor %i: %s, model=%s, manufacturer=%s",
318                    mi, monitor, monitor.get_model(), monitor.get_manufacturer())
319                wmm += monitor.get_width_mm()
320                hmm += monitor.get_height_mm()
321            wmm /= n_monitors
322            hmm /= n_monitors
323            xdpi = dpi(sw, wmm)
324            ydpi = dpi(sh, hmm)
325        if xdpi<MIN_DPI or xdpi>MAX_DPI or ydpi<MIN_DPI or ydpi>MAX_DPI:
326            #still invalid, generate one from DPI=96
327            wmm = round(sw*25.4/96)
328            hmm = round(sh*25.4/96)
329        log(" using %ix%i mm", wmm, hmm)
330    screen0 = (screen.make_display_name(), xs(sw), ys(sh),
331                wmm, hmm,
332                monitors,
333                work_x, work_y, work_width, work_height)
334    screenlog(" screen: %s", screen0)
335    return [screen0]
336
337def get_screen_info(display, screen) -> dict:
338    info = {}
339    if not WIN32:
340        try:
341            w = screen.get_root_window()
342            if w:
343                info["root"] = w.get_geometry()
344        except Exception:
345            pass
346    info["name"] = screen.make_display_name()
347    for x in ("width", "height", "width_mm", "height_mm", "resolution", "primary_monitor"):
348        fn = getattr(screen, "get_"+x)
349        try:
350            info[x] = int(fn())
351        except Exception:
352            pass
353    info["monitors"] = screen.get_n_monitors()
354    m_info = info.setdefault("monitor", {})
355    for i in range(screen.get_n_monitors()):
356        m_info[i] = get_monitor_info(display, screen, i)
357    fo = screen.get_font_options()
358    #win32 and osx return nothing here...
359    if fo:
360        fontoptions = info.setdefault("fontoptions", {})
361        fontoptions.update(get_font_info(fo))
362    vinfo = info.setdefault("visual", {})
363    def visual(name, v):
364        i = get_visual_info(v)
365        if i:
366            vinfo[name] = i
367    visual("rgba", screen.get_rgba_visual())
368    visual("system_visual", screen.get_system_visual())
369    if SHOW_ALL_VISUALS:
370        for i, v in enumerate(screen.list_visuals()):
371            visual(i, v)
372    #Gtk.settings
373    def get_setting(key, gtype):
374        v = GObject.Value()
375        v.init(gtype)
376        if screen.get_setting(key, v):
377            return v.get_value()
378        return None
379    sinfo = info.setdefault("settings", {})
380    for x, gtype in {
381        #NET:
382        "enable-event-sounds"   : GObject.TYPE_INT,
383        "icon-theme-name"       : GObject.TYPE_STRING,
384        "sound-theme-name"      : GObject.TYPE_STRING,
385        "theme-name"            : GObject.TYPE_STRING,
386        #Xft:
387        "xft-antialias" : GObject.TYPE_INT,
388        "xft-dpi"       : GObject.TYPE_INT,
389        "xft-hinting"   : GObject.TYPE_INT,
390        "xft-hintstyle" : GObject.TYPE_STRING,
391        "xft-rgba"      : GObject.TYPE_STRING,
392        }.items():
393        try:
394            v = get_setting("gtk-"+x, gtype)
395        except Exception:
396            log("failed to query screen '%s'", x, exc_info=True)
397            continue
398        if v is None:
399            v = ""
400        if x.startswith("xft-"):
401            x = x[4:]
402        sinfo[x] = v
403    return info
404
405def get_font_info(font_options):
406    #pylint: disable=no-member
407    font_info = {}
408    for x,vdict in {
409        "antialias" : {
410            cairo.ANTIALIAS_DEFAULT     : "default",
411            cairo.ANTIALIAS_NONE        : "none",
412            cairo.ANTIALIAS_GRAY        : "gray",
413            cairo.ANTIALIAS_SUBPIXEL    : "subpixel",
414            },
415        "hint_metrics" : {
416            cairo.HINT_METRICS_DEFAULT  : "default",
417            cairo.HINT_METRICS_OFF      : "off",
418            cairo.HINT_METRICS_ON       : "on",
419            },
420        "hint_style" : {
421            cairo.HINT_STYLE_DEFAULT    : "default",
422            cairo.HINT_STYLE_NONE       : "none",
423            cairo.HINT_STYLE_SLIGHT     : "slight",
424            cairo.HINT_STYLE_MEDIUM     : "medium",
425            cairo.HINT_STYLE_FULL       : "full",
426            },
427        "subpixel_order": {
428            cairo.SUBPIXEL_ORDER_DEFAULT    : "default",
429            cairo.SUBPIXEL_ORDER_RGB        : "RGB",
430            cairo.SUBPIXEL_ORDER_BGR        : "BGR",
431            cairo.SUBPIXEL_ORDER_VRGB       : "VRGB",
432            cairo.SUBPIXEL_ORDER_VBGR       : "VBGR",
433            },
434        }.items():
435        fn = getattr(font_options, "get_"+x)
436        val = fn()
437        font_info[x] = vdict.get(val, val)
438    return font_info
439
440def get_visual_info(v):
441    if not v:
442        return {}
443    vinfo = {}
444    for x, vdict in {
445        "bits_per_rgb"          : {},
446        "byte_order"            : BYTE_ORDER_NAMES,
447        "colormap_size"         : {},
448        "depth"                 : {},
449        "red_pixel_details"     : {},
450        "green_pixel_details"   : {},
451        "blue_pixel_details"    : {},
452        "visual_type"           : VISUAL_NAMES,
453        }.items():
454        val = None
455        try:
456            #ugly workaround for "visual_type" -> "type" for GTK2...
457            val = getattr(v, x.replace("visual_", ""))
458        except AttributeError:
459            try:
460                fn = getattr(v, "get_"+x)
461            except AttributeError:
462                pass
463            else:
464                val = fn()
465        if val is not None:
466            vinfo[x] = vdict.get(val, val)
467    return vinfo
468
469def get_monitor_info(_display, screen, i) -> dict:
470    info = {}
471    geom = screen.get_monitor_geometry(i)
472    for x in ("x", "y", "width", "height"):
473        info[x] = getattr(geom, x)
474    if hasattr(screen, "get_monitor_plug_name"):
475        info["plug_name"] = screen.get_monitor_plug_name(i) or ""
476    for x in ("scale_factor", "width_mm", "height_mm"):
477        fn = getattr(screen, "get_monitor_"+x, None)
478        if fn:
479            info[x] = int(fn(i))
480    rectangle = screen.get_monitor_workarea(i)
481    workarea_info = info.setdefault("workarea", {})
482    for x in ("x", "y", "width", "height"):
483        workarea_info[x] = getattr(rectangle, x)
484    return info
485
486
487def get_display_info() -> dict:
488    display = Gdk.Display.get_default()
489    info = {
490            "root-size"             : get_root_size(),
491            "screens"               : display.get_n_screens(),
492            "name"                  : display.get_name(),
493            "pointer"               : display.get_pointer()[-3:-1],
494            "devices"               : len(display.list_devices()),
495            "default_cursor_size"   : display.get_default_cursor_size(),
496            "maximal_cursor_size"   : display.get_maximal_cursor_size(),
497            "pointer_is_grabbed"    : display.pointer_is_grabbed(),
498            }
499    if not WIN32:
500        info["root"] = get_default_root_window().get_geometry()
501    sinfo = info.setdefault("supports", {})
502    for x in ("composite", "cursor_alpha", "cursor_color", "selection_notification", "clipboard_persistence", "shapes"):
503        f = "supports_"+x
504        if hasattr(display, f):
505            fn = getattr(display, f)
506            sinfo[x]  = fn()
507    info["screens"] = get_screens_info()
508    dm = display.get_device_manager()
509    for dt, name in {Gdk.DeviceType.MASTER  : "master",
510                     Gdk.DeviceType.SLAVE   : "slave",
511                     Gdk.DeviceType.FLOATING: "floating"}.items():
512        dinfo = info.setdefault("device", {})
513        dtinfo = dinfo.setdefault(name, {})
514        devices = dm.list_devices(dt)
515        for i, d in enumerate(devices):
516            dtinfo[i] = d.get_name()
517    return info
518
519
520def scaled_image(pixbuf, icon_size=None) -> Gtk.Image:
521    if not pixbuf:
522        return None
523    if icon_size:
524        pixbuf = pixbuf.scale_simple(icon_size, icon_size, GdkPixbuf.InterpType.BILINEAR)
525    return Gtk.Image.new_from_pixbuf(pixbuf)
526
527
528def get_icon_from_file(filename):
529    if not filename:
530        log("get_icon_from_file(%s)=None", filename)
531        return None
532    try:
533        if not os.path.exists(filename):
534            log.warn("Warning: cannot load icon, '%s' does not exist", filename)
535            return None
536        with open(filename, mode='rb') as f:
537            data = f.read()
538        loader = GdkPixbuf.PixbufLoader()
539        loader.write(data)
540        loader.close()
541    except Exception as e:
542        log("get_icon_from_file(%s)", filename, exc_info=True)
543        log.error("Error: failed to load '%s'", filename)
544        log.error(" %s", e)
545        return None
546    pixbuf = loader.get_pixbuf()
547    return pixbuf
548
549
550def get_icon_pixbuf(icon_name):
551    try:
552        if not icon_name:
553            log("get_icon_pixbuf(%s)=None", icon_name)
554            return None
555        from xpra.platform.paths import get_icon_filename
556        icon_filename = get_icon_filename(icon_name)
557        log("get_pixbuf(%s) icon_filename=%s", icon_name, icon_filename)
558        if icon_filename:
559            return GdkPixbuf.Pixbuf.new_from_file(icon_filename)
560    except Exception:
561        log.error("get_icon_pixbuf(%s)", icon_name, exc_info=True)
562    return None
563
564
565def imagebutton(title, icon=None, tooltip=None, clicked_callback=None, icon_size=32,
566                default=False, min_size=None, label_color=None, label_font=None) -> Gtk.Button:
567    button = Gtk.Button(title)
568    settings = button.get_settings()
569    settings.set_property('gtk-button-images', True)
570    if icon:
571        if icon_size:
572            icon = scaled_image(icon, icon_size)
573        button.set_image(icon)
574    if tooltip:
575        button.set_tooltip_text(tooltip)
576    if min_size:
577        button.set_size_request(min_size, min_size)
578    if clicked_callback:
579        button.connect("clicked", clicked_callback)
580    if default:
581        button.set_can_default(True)
582    if label_color or label_font:
583        l = button
584        try:
585            alignment = button.get_children()[0]
586            b_hbox = alignment.get_children()[0]
587            l = b_hbox.get_children()[1]
588        except (IndexError, AttributeError):
589            pass
590        if label_color and hasattr(l, "modify_fg"):
591            l.modify_fg(Gtk.StateType.NORMAL, label_color)
592        if label_font and hasattr(l, "modify_font"):
593            l.modify_font(label_font)
594    return button
595
596def menuitem(title, image=None, tooltip=None, cb=None) -> Gtk.ImageMenuItem:
597    """ Utility method for easily creating an ImageMenuItem """
598    menu_item = Gtk.ImageMenuItem()
599    menu_item.set_label(title)
600    if image:
601        menu_item.set_image(image)
602        #override gtk defaults: we *want* icons:
603        settings = menu_item.get_settings()
604        settings.set_property('gtk-menu-images', True)
605        if hasattr(menu_item, "set_always_show_image"):
606            menu_item.set_always_show_image(True)
607    if tooltip:
608        menu_item.set_tooltip_text(tooltip)
609    if cb:
610        menu_item.connect('activate', cb)
611    menu_item.show()
612    return menu_item
613
614
615def add_close_accel(window, callback):
616    accel_groups = []
617    def wa(s, cb):
618        accel_groups.append(add_window_accel(window, s, cb))
619    wa('<control>F4', callback)
620    wa('<Alt>F4', callback)
621    wa('Escape', callback)
622    return accel_groups
623
624def add_window_accel(window, accel, callback) -> Gtk.AccelGroup:
625    def connect(ag, *args):
626        ag.connect(*args)
627    accel_group = Gtk.AccelGroup()
628    key, mod = Gtk.accelerator_parse(accel)
629    connect(accel_group, key, mod, Gtk.AccelFlags.LOCKED, callback)
630    window.add_accel_group(accel_group)
631    return accel_group
632
633
634def label(text="", tooltip=None, font=None) -> Gtk.Label:
635    l = Gtk.Label(text)
636    if font:
637        fontdesc = Pango.FontDescription(font)
638        l.modify_font(fontdesc)
639    if tooltip:
640        l.set_tooltip_text(tooltip)
641    return l
642
643
644class TableBuilder:
645
646    def __init__(self, rows=1, columns=2, homogeneous=False, col_spacings=0, row_spacings=0):
647        self.table = Gtk.Table(rows, columns, homogeneous)
648        self.table.set_col_spacings(col_spacings)
649        self.table.set_row_spacings(row_spacings)
650        self.row = 0
651        self.widget_xalign = 0.0
652
653    def get_table(self):
654        return self.table
655
656    def add_row(self, widget, *widgets, **kwargs):
657        if widget:
658            l_al = Gtk.Alignment(xalign=1.0, yalign=0.5, xscale=0.0, yscale=0.0)
659            l_al.add(widget)
660            self.attach(l_al, 0)
661        if widgets:
662            i = 1
663            for w in widgets:
664                if w:
665                    w_al = Gtk.Alignment(xalign=self.widget_xalign, yalign=0.5, xscale=0.0, yscale=0.0)
666                    w_al.add(w)
667                    self.attach(w_al, i, **kwargs)
668                i += 1
669        self.inc()
670
671    def attach(self, widget, i=0, count=1,
672               xoptions=Gtk.AttachOptions.FILL, yoptions=Gtk.AttachOptions.FILL,
673               xpadding=10, ypadding=0):
674        self.table.attach(widget, i, i+count, self.row, self.row+1,
675                          xoptions=xoptions, yoptions=yoptions, xpadding=xpadding, ypadding=ypadding)
676
677    def inc(self):
678        self.row += 1
679
680    def new_row(self, row_label_str="", value1=None, value2=None, label_tooltip=None, **kwargs):
681        row_label = label(row_label_str, label_tooltip)
682        self.add_row(row_label, value1, value2, **kwargs)
683
684
685def choose_files(parent_window, title, action=Gtk.FileChooserAction.OPEN, action_button=Gtk.STOCK_OPEN, callback=None, file_filter=None, multiple=True):
686    log("choose_files%s", (parent_window, title, action, action_button, callback, file_filter))
687    chooser = Gtk.FileChooserDialog(title,
688                                parent=parent_window, action=action,
689                                buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, action_button, Gtk.ResponseType.OK))
690    chooser.set_select_multiple(multiple)
691    chooser.set_default_response(Gtk.ResponseType.OK)
692    if file_filter:
693        chooser.add_filter(file_filter)
694    response = chooser.run()
695    filenames = chooser.get_filenames()
696    chooser.hide()
697    chooser.destroy()
698    if response!=Gtk.ResponseType.OK:
699        return None
700    return filenames
701
702def choose_file(parent_window, title, action=Gtk.FileChooserAction.OPEN, action_button=Gtk.STOCK_OPEN, callback=None, file_filter=None):
703    filenames = choose_files(parent_window, title, action, action_button, callback, file_filter, False)
704    if not filenames or len(filenames)!=1:
705        return None
706    filename = filenames[0]
707    if callback:
708        callback(filename)
709    return filename
710
711
712def main():
713    from xpra.platform import program_context
714    from xpra.log import enable_color
715    with program_context("GTK-Version-Info", "GTK Version Info"):
716        enable_color()
717        print("%s" % get_gtk_version_info())
718        get_screen_sizes()
719
720
721if __name__ == "__main__":
722    main()
723