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