Coverage for /home/antoine/projects/xpra-git/dist/python3/lib64/python/xpra/client/gtk_base/gtk_client_window_base.py : 39%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of Xpra.
2# Copyright (C) 2011 Serviware (Arthur Huillet, <ahuillet@serviware.com>)
3# Copyright (C) 2010-2020 Antoine Martin <antoine@xpra.org>
4# Copyright (C) 2008, 2010 Nathaniel Smith <njs@pobox.com>
5# Xpra is released under the terms of the GNU GPL v2, or, at your option, any
6# later version. See the file COPYING for details.
8import math
9import os.path
10from urllib.parse import unquote
11from cairo import OPERATOR_OVER, LINE_CAP_ROUND #pylint: disable=no-name-in-module
12from gi.repository import Gtk, Gdk, Gio
14from xpra.os_util import bytestostr, strtobytes, is_X11, monotonic_time, WIN32, OSX, POSIX
15from xpra.util import (
16 AdHocStruct, typedict, envint, envbool, csv, first_time,
17 WORKSPACE_UNSET, WORKSPACE_ALL, WORKSPACE_NAMES, MOVERESIZE_DIRECTION_STRING, SOURCE_INDICATION_STRING,
18 MOVERESIZE_CANCEL,
19 MOVERESIZE_SIZE_TOPLEFT, MOVERESIZE_SIZE_TOP, MOVERESIZE_SIZE_TOPRIGHT,
20 MOVERESIZE_SIZE_RIGHT,
21 MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_BOTTOM, MOVERESIZE_SIZE_BOTTOMLEFT,
22 MOVERESIZE_SIZE_LEFT, MOVERESIZE_MOVE, MOVERESIZE_MOVE_KEYBOARD,
23 )
24from xpra.gtk_common.gobject_util import no_arg_signal, one_arg_signal
25from xpra.gtk_common.gtk_util import (
26 get_pixbuf_from_data, get_default_root_window,
27 set_visual,
28 BUTTON_MASK,
29 GRAB_STATUS_STRING,
30 WINDOW_EVENT_MASK,
31 )
32from xpra.gtk_common.keymap import KEY_TRANSLATIONS
33from xpra.client.client_window_base import ClientWindowBase
34from xpra.platform.gui import set_fullscreen_monitors, set_shaded
35from xpra.platform.gui import add_window_hooks, remove_window_hooks
36from xpra.log import Logger
38focuslog = Logger("focus", "grab")
39workspacelog = Logger("workspace")
40log = Logger("window")
41keylog = Logger("keyboard")
42keyeventlog = Logger("keyboard", "events")
43iconlog = Logger("icon")
44metalog = Logger("metadata")
45statelog = Logger("state")
46eventslog = Logger("events")
47shapelog = Logger("shape")
48mouselog = Logger("mouse")
49geomlog = Logger("geometry")
50grablog = Logger("grab")
51draglog = Logger("dragndrop")
52alphalog = Logger("alpha")
54CAN_SET_WORKSPACE = False
55HAS_X11_BINDINGS = False
56USE_X11_BINDINGS = POSIX and envbool("XPRA_USE_X11_BINDINGS", is_X11())
57prop_get, prop_set, prop_del = None, None, None
58NotifyInferior = None
59if USE_X11_BINDINGS:
60 try:
61 from xpra.gtk_common.error import xlog, verify_sync
62 from xpra.x11.gtk_x11.prop import prop_get, prop_set, prop_del
63 from xpra.x11.bindings.window_bindings import constants, X11WindowBindings, SHAPE_KIND #@UnresolvedImport
64 from xpra.x11.bindings.core_bindings import X11CoreBindings, set_context_check
65 from xpra.x11.gtk_x11.send_wm import send_wm_workspace
67 set_context_check(verify_sync)
68 X11Window = X11WindowBindings()
69 X11Core = X11CoreBindings()
70 NotifyInferior = constants["NotifyInferior"]
71 HAS_X11_BINDINGS = True
73 SubstructureNotifyMask = constants["SubstructureNotifyMask"]
74 SubstructureRedirectMask = constants["SubstructureRedirectMask"]
76 def can_set_workspace():
77 SET_WORKSPACE = envbool("XPRA_SET_WORKSPACE", True)
78 if not SET_WORKSPACE:
79 return False
80 try:
81 #TODO: in theory this is not a proper check, meh - that will do
82 root = get_default_root_window()
83 supported = prop_get(root, "_NET_SUPPORTED", ["atom"], ignore_errors=True)
84 return bool(supported) and "_NET_WM_DESKTOP" in supported
85 except Exception as e:
86 workspacelog("x11 workspace bindings error", exc_info=True)
87 workspacelog.error("Error: failed to setup workspace hooks:")
88 workspacelog.error(" %s", e)
89 CAN_SET_WORKSPACE = can_set_workspace()
90 except ImportError as e:
91 log("x11 bindings", exc_info=True)
92 log.error("Error: cannot import X11 bindings:")
93 log.error(" %s", e)
96AWT_DIALOG_WORKAROUND = envbool("XPRA_AWT_DIALOG_WORKAROUND", WIN32)
97BREAK_MOVERESIZE = os.environ.get("XPRA_BREAK_MOVERESIZE", "Escape").split(",")
98MOVERESIZE_X11 = envbool("XPRA_MOVERESIZE_X11", POSIX)
99MOVERESIZE_GDK = envbool("XPRA_MOVERESIZE_GDK", True)
100CURSOR_IDLE_TIMEOUT = envint("XPRA_CURSOR_IDLE_TIMEOUT", 6)
101DISPLAY_HAS_SCREEN_INDEX = POSIX and os.environ.get("DISPLAY", "").split(":")[-1].find(".")>=0
102DRAGNDROP = envbool("XPRA_DRAGNDROP", True)
103CLAMP_WINDOW_TO_SCREEN = envbool("XPRA_CLAMP_WINDOW_TO_SCREEN", True)
104FOCUS_RECHECK_DELAY = envint("XPRA_FOCUS_RECHECK_DELAY", 0)
105REPAINT_MAXIMIZED = envint("XPRA_REPAINT_MAXIMIZED", 0)
106REFRESH_MAXIMIZED = envbool("XPRA_REFRESH_MAXIMIZED", True)
108WINDOW_OVERFLOW_TOP = envbool("XPRA_WINDOW_OVERFLOW_TOP", False)
109AWT_RECENTER = envbool("XPRA_AWT_RECENTER", True)
110UNDECORATED_TRANSIENT_IS_OR = envint("XPRA_UNDECORATED_TRANSIENT_IS_OR", 1)
111XSHAPE = envbool("XPRA_XSHAPE", True)
112LAZY_SHAPE = envbool("XPRA_LAZY_SHAPE", True)
113def parse_padding_colors(colors_str):
114 padding_colors = 0, 0, 0
115 if colors_str:
116 try:
117 padding_colors = tuple(float(x.strip()) for x in colors_str.split(","))
118 assert len(padding_colors)==3, "you must specify 3 components"
119 except Exception as e:
120 log.warn("Warning: invalid padding colors specified,")
121 log.warn(" %s", e)
122 log.warn(" using black")
123 padding_colors = 0, 0, 0
124 log("parse_padding_colors(%s)=%s", colors_str, padding_colors)
125 return padding_colors
126PADDING_COLORS = parse_padding_colors(os.environ.get("XPRA_PADDING_COLORS"))
128#window types we map to POPUP rather than TOPLEVEL
129POPUP_TYPE_HINTS = set((
130 #"DIALOG",
131 #"MENU",
132 #"TOOLBAR",
133 #"SPLASH",
134 #"UTILITY",
135 #"DOCK",
136 #"DESKTOP",
137 "DROPDOWN_MENU",
138 "POPUP_MENU",
139 #"TOOLTIP",
140 #"NOTIFICATION",
141 #"COMBO",
142 #"DND"
143 ))
144#window types for which we skip window decorations (title bar)
145UNDECORATED_TYPE_HINTS = set((
146 #"DIALOG",
147 "MENU",
148 #"TOOLBAR",
149 "SPLASH",
150 "SPLASHSCREEN",
151 "UTILITY",
152 "DOCK",
153 "DESKTOP",
154 "DROPDOWN_MENU",
155 "POPUP_MENU",
156 "TOOLTIP",
157 "NOTIFICATION",
158 "COMBO",
159 "DND"))
161GDK_SCROLL_MAP = {
162 Gdk.ScrollDirection.UP : 4,
163 Gdk.ScrollDirection.DOWN : 5,
164 Gdk.ScrollDirection.LEFT : 6,
165 Gdk.ScrollDirection.RIGHT : 7,
166 }
168OR_TYPE_HINTS = (
169 Gdk.WindowTypeHint.DIALOG,
170 Gdk.WindowTypeHint.MENU,
171 Gdk.WindowTypeHint.TOOLBAR,
172 #Gdk.WindowTypeHint.SPLASHSCREEN,
173 #Gdk.WindowTypeHint.UTILITY,
174 #Gdk.WindowTypeHint.DOCK,
175 #Gdk.WindowTypeHint.DESKTOP,
176 Gdk.WindowTypeHint.DROPDOWN_MENU,
177 Gdk.WindowTypeHint.POPUP_MENU,
178 Gdk.WindowTypeHint.TOOLTIP,
179 #Gdk.WindowTypeHint.NOTIFICATION,
180 Gdk.WindowTypeHint.COMBO,
181 Gdk.WindowTypeHint.DND,
182 )
184WINDOW_NAME_TO_HINT = {
185 "NORMAL" : Gdk.WindowTypeHint.NORMAL,
186 "DIALOG" : Gdk.WindowTypeHint.DIALOG,
187 "MENU" : Gdk.WindowTypeHint.MENU,
188 "TOOLBAR" : Gdk.WindowTypeHint.TOOLBAR,
189 "SPLASH" : Gdk.WindowTypeHint.SPLASHSCREEN,
190 "UTILITY" : Gdk.WindowTypeHint.UTILITY,
191 "DOCK" : Gdk.WindowTypeHint.DOCK,
192 "DESKTOP" : Gdk.WindowTypeHint.DESKTOP,
193 "DROPDOWN_MENU" : Gdk.WindowTypeHint.DROPDOWN_MENU,
194 "POPUP_MENU" : Gdk.WindowTypeHint.POPUP_MENU,
195 "TOOLTIP" : Gdk.WindowTypeHint.TOOLTIP,
196 "NOTIFICATION" : Gdk.WindowTypeHint.NOTIFICATION,
197 "COMBO" : Gdk.WindowTypeHint.COMBO,
198 "DND" : Gdk.WindowTypeHint.DND
199 }
202def wn(w):
203 return WORKSPACE_NAMES.get(w, w)
206class GTKKeyEvent(AdHocStruct):
207 pass
210class GTKClientWindowBase(ClientWindowBase, Gtk.Window):
212 __common_gsignals__ = {
213 "state-updated" : no_arg_signal,
214 "xpra-focus-out-event" : one_arg_signal,
215 "xpra-focus-in-event" : one_arg_signal,
216 }
218 #maximum size of the actual window:
219 MAX_VIEWPORT_DIMS = 16*1024, 16*1024
220 #maximum size of the backing pixel buffer:
221 MAX_BACKING_DIMS = 16*1024, 16*1024
223 def init_window(self, metadata):
224 self.init_max_window_size()
225 if self._is_popup(metadata):
226 window_type = Gtk.WindowType.POPUP
227 else:
228 window_type = Gtk.WindowType.TOPLEVEL
229 self.on_realize_cb = {}
230 Gtk.Window.__init__(self, type = window_type)
231 self.set_app_paintable(True)
232 self.init_drawing_area()
233 self.set_decorated(self._is_decorated(metadata))
234 self._window_state = {}
235 self._resize_counter = 0
236 self._can_set_workspace = HAS_X11_BINDINGS and CAN_SET_WORKSPACE
237 self._current_frame_extents = None
238 self._screen = -1
239 self._frozen = False
240 self.window_state_timer = None
241 self.send_iconify_timer = None
242 self.remove_pointer_overlay_timer = None
243 self.show_pointer_overlay_timer = None
244 self.moveresize_timer = None
245 self.moveresize_event = None
246 #add platform hooks
247 self.connect_after("realize", self.on_realize)
248 self.connect('unrealize', self.on_unrealize)
249 self.add_events(WINDOW_EVENT_MASK)
250 if DRAGNDROP and not self._client.readonly:
251 self.init_dragndrop()
252 self.init_focus()
253 ClientWindowBase.init_window(self, metadata)
255 def init_drawing_area(self):
256 widget = Gtk.DrawingArea()
257 widget.set_app_paintable(True)
258 widget.set_size_request(*self._size)
259 widget.show()
260 self.drawing_area = widget
261 self.init_widget_events(widget)
262 self.add(widget)
264 def repaint(self, x, y, w, h):
265 #self.queue_draw_area(0, 0, *self._size)
266 widget = self.drawing_area
267 if widget:
268 widget.queue_draw_area(x, y, w, h)
271 def init_widget_events(self, widget):
272 widget.add_events(WINDOW_EVENT_MASK)
273 def motion(_w, event):
274 self._do_motion_notify_event(event)
275 return True
276 widget.connect("motion-notify-event", motion)
277 def press(_w, event):
278 self._do_button_press_event(event)
279 return True
280 widget.connect("button-press-event", press)
281 def release(_w, event):
282 self._do_button_release_event(event)
283 return True
284 widget.connect("button-release-event", release)
285 def scroll(_w, event):
286 self._do_scroll_event(event)
287 return True
288 widget.connect("scroll-event", scroll)
289 widget.connect("draw", self.draw_widget)
291 def draw_widget(self, widget, context):
292 raise NotImplementedError()
294 def get_drawing_area_geometry(self):
295 raise NotImplementedError()
298 ######################################################################
299 # drag and drop:
300 def init_dragndrop(self):
301 targets = [
302 Gtk.TargetEntry.new("text/uri-list", 0, 80),
303 ]
304 flags = Gtk.DestDefaults.MOTION | Gtk.DestDefaults.HIGHLIGHT
305 actions = Gdk.DragAction.COPY # | Gdk.ACTION_LINK
306 self.drag_dest_set(flags, targets, actions)
307 self.connect('drag_drop', self.drag_drop_cb)
308 self.connect('drag_motion', self.drag_motion_cb)
309 self.connect('drag_data_received', self.drag_got_data_cb)
311 def drag_drop_cb(self, widget, context, x, y, time):
312 targets = list(x.name() for x in context.list_targets())
313 draglog("drag_drop_cb%s targets=%s", (widget, context, x, y, time), targets)
314 if not targets:
315 #this happens on macos, but we can still get the data..
316 draglog("Warning: no targets provided, continuing anyway")
317 elif "text/uri-list" not in targets:
318 draglog("Warning: cannot handle targets:")
319 draglog(" %s", csv(targets))
320 return
321 atom = Gdk.Atom.intern("text/uri-list", False)
322 widget.drag_get_data(context, atom, time)
324 def drag_motion_cb(self, wid, context, x, y, time):
325 draglog("drag_motion_cb%s", (wid, context, x, y, time))
326 Gdk.drag_status(context, Gdk.DragAction.COPY, time)
327 return True #accept this data
329 def drag_got_data_cb(self, wid, context, x, y, selection, info, time):
330 draglog("drag_got_data_cb%s", (wid, context, x, y, selection, info, time))
331 #draglog("%s: %s", type(selection), dir(selection))
332 #draglog("%s: %s", type(context), dir(context))
333 targets = list(x.name() for x in context.list_targets())
334 actions = context.get_actions()
335 def xid(w):
336 #TODO: use a generic window handle function
337 #this only used for debugging for now
338 if w and POSIX:
339 return w.get_xid()
340 return 0
341 dest_window = xid(context.get_dest_window())
342 source_window = xid(context.get_source_window())
343 suggested_action = context.get_suggested_action()
344 draglog("drag_got_data_cb context: source_window=%#x, dest_window=%#x",
345 source_window, dest_window)
346 draglog("drag_got_data_cb context: suggested_action=%s, actions=%s, targets=%s",
347 suggested_action, actions, targets)
348 dtype = selection.get_data_type()
349 fmt = selection.get_format()
350 l = selection.get_length()
351 target = selection.get_target()
352 text = selection.get_text()
353 uris = selection.get_uris()
354 draglog("drag_got_data_cb selection: data type=%s, format=%s, length=%s, target=%s, text=%s, uris=%s",
355 dtype, fmt, l, target, text, uris)
356 if not uris:
357 return
358 filelist = []
359 for uri in uris:
360 if not uri:
361 continue
362 if not uri.startswith("file://"):
363 draglog.warn("Warning: cannot handle drag-n-drop URI '%s'", uri)
364 continue
365 filename = unquote(uri[len("file://"):].rstrip("\n\r"))
366 if WIN32:
367 filename = filename.lstrip("/")
368 abspath = os.path.abspath(filename)
369 if not os.path.isfile(abspath):
370 draglog.warn("Warning: '%s' is not a file", abspath)
371 continue
372 filelist.append(abspath)
373 draglog("drag_got_data_cb: will try to upload: %s", csv(filelist))
374 pending = set(filelist)
375 #when all the files have been loaded / failed,
376 #finish the drag and drop context so the source knows we're done with them:
377 def file_done(filename):
378 if not pending:
379 return
380 try:
381 pending.remove(filename)
382 except KeyError:
383 pass
384 if not pending:
385 context.finish(True, False, time)
386 for filename in filelist:
387 def got_file_info(gfile, result, arg=None):
388 draglog("got_file_info(%s, %s, %s)", gfile, result, arg)
389 file_info = gfile.query_info_finish(result)
390 basename = gfile.get_basename()
391 ctype = file_info.get_content_type()
392 size = file_info.get_size()
393 draglog("file_info(%s)=%s ctype=%s, size=%s", filename, file_info, ctype, size)
394 def got_file_data(gfile, result, user_data=None):
395 _, data, entity = gfile.load_contents_finish(result)
396 filesize = len(data)
397 draglog("got_file_data(%s, %s, %s) entity=%s", gfile, result, user_data, entity)
398 file_done(filename)
399 openit = self._client.remote_open_files
400 draglog.info("sending file %s (%i bytes)", basename, filesize)
401 self._client.send_file(filename, "", data, filesize=filesize, openit=openit)
402 cancellable = None
403 user_data = (filename, True)
404 gfile.load_contents_async(cancellable, got_file_data, user_data)
405 try:
406 gfile = Gio.File.new_for_path(filename)
407 #basename = gf.get_basename()
408 FILE_QUERY_INFO_NONE = 0
409 G_PRIORITY_DEFAULT = 0
410 cancellable = None
411 gfile.query_info_async("standard::*", FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable, got_file_info, None)
412 except Exception as e:
413 draglog("file upload for %s:", filename, exc_info=True)
414 draglog.error("Error: cannot upload '%s':", filename)
415 draglog.error(" %s", e)
416 del e
417 file_done(filename)
419 ######################################################################
420 # focus:
421 def init_focus(self):
422 self.recheck_focus_timer = 0
423 self.when_realized("init-focus", self.do_init_focus)
425 def do_init_focus(self):
426 #hook up the X11 gdk event notifications so we can get focus-out when grabs are active:
427 if POSIX and not OSX:
428 try:
429 from xpra.x11.gtk_x11.gdk_bindings import add_event_receiver
430 except ImportError as e:
431 log("do_init_focus()", exc_info=True)
432 log.warn("Warning: missing gdk bindings:")
433 log.warn(" %s", e)
434 else:
435 self._focus_latest = None
436 grablog("adding event receiver so we can get FocusIn and FocusOut events whilst grabbing the keyboard")
437 add_event_receiver(self.get_window(), self)
438 #other platforms should bet getting regular focus events instead:
439 def focus_in(_window, event):
440 focuslog("focus-in-event for wid=%s", self._id)
441 self.do_xpra_focus_in_event(event)
442 def focus_out(_window, event):
443 focuslog("focus-out-event for wid=%s", self._id)
444 self.do_xpra_focus_out_event(event)
445 self.connect("focus-in-event", focus_in)
446 self.connect("focus-out-event", focus_out)
447 if not self._override_redirect:
448 self.connect("notify::has-toplevel-focus", self._focus_change)
450 def _focus_change(self, *args):
451 assert not self._override_redirect
452 htf = self.has_toplevel_focus()
453 focuslog("%s focus_change%s has-toplevel-focus=%s, _been_mapped=%s", self, args, htf, self._been_mapped)
454 if self._been_mapped:
455 self._client.update_focus(self._id, htf)
457 def recheck_focus(self):
458 self.recheck_focus_timer = 0
459 #we receive pairs of FocusOut + FocusIn following a keyboard grab,
460 #so we recheck the focus status via this timer to skip unnecessary churn
461 focused = self._client._focused
462 focuslog("recheck_focus() wid=%i, focused=%s, latest=%s", self._id, focused, self._focus_latest)
463 hasfocus = focused==self._id
464 if hasfocus==self._focus_latest:
465 #we're already up to date
466 return
467 if not self._focus_latest:
468 self._client.window_ungrab()
469 self._client.update_focus(self._id, False)
470 else:
471 self._client.update_focus(self._id, True)
473 def cancel_focus_timer(self):
474 rft = self.recheck_focus_timer
475 if rft:
476 self.recheck_focus_timer = 0
477 self.source_remove(rft)
479 def schedule_recheck_focus(self):
480 if FOCUS_RECHECK_DELAY<0:
481 self.recheck_focus()
482 return
483 if self.recheck_focus_timer==0:
484 self.recheck_focus_timer = self.timeout_add(FOCUS_RECHECK_DELAY, self.recheck_focus)
485 return True
487 def do_xpra_focus_out_event(self, event):
488 focuslog("do_xpra_focus_out_event(%s)", event)
489 if NotifyInferior is not None:
490 detail = getattr(event, "detail", None)
491 if detail==NotifyInferior:
492 focuslog("dropped NotifyInferior focus event")
493 return True
494 self._focus_latest = False
495 return self.schedule_recheck_focus()
497 def do_xpra_focus_in_event(self, event):
498 focuslog("do_xpra_focus_in_event(%s) been_mapped=%s", event, self._been_mapped)
499 if self._been_mapped:
500 self._focus_latest = True
501 return self.schedule_recheck_focus()
504 def init_max_window_size(self):
505 """ used by GL windows to enforce a hard limit on window sizes """
506 saved_mws = self.max_window_size
507 def clamp_to(maxw, maxh):
508 #don't bother if the new limit is greater than 16k:
509 if maxw>=16*1024 and maxh>=16*1024:
510 return
511 #only take into account the current max-window-size if non zero:
512 mww, mwh = self.max_window_size
513 if mww>0:
514 maxw = min(mww, maxw)
515 if mwh>0:
516 maxh = min(mwh, maxh)
517 self.max_window_size = maxw, maxh
518 #viewport is easy, measured in window pixels:
519 clamp_to(*self.MAX_VIEWPORT_DIMS)
520 #backing dimensions are harder,
521 #we have to take scaling into account (if any):
522 clamp_to(*self._client.sp(*self.MAX_BACKING_DIMS))
523 if self.max_window_size!=saved_mws:
524 log("init_max_window_size(..) max-window-size changed from %s to %s",
525 saved_mws, self.max_window_size)
526 log(" because of max viewport dims %s and max backing dims %s",
527 self.MAX_VIEWPORT_DIMS, self.MAX_BACKING_DIMS)
530 def is_awt(self, metadata) -> bool:
531 wm_class = metadata.get("class-instance")
532 return wm_class and len(wm_class)==2 and wm_class[0].startswith("sun-awt-X11")
534 def _is_popup(self, metadata) -> bool:
535 #decide if the window type is POPUP or NORMAL
536 if self._override_redirect:
537 return True
538 if UNDECORATED_TRANSIENT_IS_OR>0:
539 transient_for = metadata.get("transient-for", -1)
540 decorations = metadata.get("decorations", 0)
541 if transient_for>0 and decorations<=0:
542 if UNDECORATED_TRANSIENT_IS_OR>1:
543 metalog("forcing POPUP type for window transient-for=%s", transient_for)
544 return True
545 if metadata.get("skip-taskbar") and self.is_awt(metadata):
546 metalog("forcing POPUP type for Java AWT skip-taskbar window, transient-for=%s", transient_for)
547 return True
548 window_types = metadata.strtupleget("window-type")
549 popup_types = tuple(POPUP_TYPE_HINTS.intersection(window_types))
550 metalog("popup_types(%s)=%s", window_types, popup_types)
551 if popup_types:
552 metalog("forcing POPUP window type for %s", popup_types)
553 return True
554 return False
556 def _is_decorated(self, metadata) -> bool:
557 #decide if the window type is POPUP or NORMAL
558 #(show window decorations or not)
559 if self._override_redirect:
560 return False
561 return metadata.boolget("decorations", True)
563 def set_decorated(self, decorated : bool):
564 was_decorated = self.get_decorated()
565 if self._fullscreen and was_decorated and not decorated:
566 #fullscreen windows aren't decorated anyway!
567 #calling set_decorated(False) would cause it to get unmapped! (why?)
568 pass
569 else:
570 Gtk.Window.set_decorated(self, decorated)
571 if WIN32:
572 #workaround for new window offsets:
573 #keep the window contents where they were and adjust the frame
574 #this generates a configure event which ensures the server has the correct window position
575 wfs = self._client.get_window_frame_sizes()
576 if wfs and decorated and not was_decorated:
577 geomlog("set_decorated(%s) re-adjusting window location using %s", decorated, wfs)
578 normal = wfs.get("normal")
579 fixed = wfs.get("fixed")
580 if normal and fixed:
581 nx, ny = normal
582 fx, fy = fixed
583 x, y = self.get_position()
584 Gtk.Window.move(self, max(0, x-nx+fx), max(0, y-ny+fy))
587 def setup_window(self, *args):
588 log("setup_window%s", args)
589 self.set_alpha()
591 if self._override_redirect:
592 transient_for = self.get_transient_for()
593 type_hint = self.get_type_hint()
594 if transient_for is not None and type_hint in self.OR_TYPE_HINTS:
595 transient_for._override_redirect_windows.append(self)
597 self.connect("property-notify-event", self.property_changed)
598 self.connect("window-state-event", self.window_state_updated)
600 #this will create the backing:
601 ClientWindowBase.setup_window(self, *args)
603 #try to honour the initial position
604 geomlog("setup_window() position=%s, set_initial_position=%s, OR=%s, decorated=%s",
605 self._pos, self._set_initial_position, self.is_OR(), self.get_decorated())
606 if self._pos!=(0, 0) or self._set_initial_position or self.is_OR():
607 x, y = self.adjusted_position(*self._pos)
608 if self.is_OR():
609 #make sure OR windows are mapped on screen
610 if self._client._current_screen_sizes:
611 w, h = self._size
612 self.window_offset = self.calculate_window_offset(x, y, w, h)
613 geomlog("OR offsets=%s", self.window_offset)
614 if self.window_offset:
615 x += self.window_offset[0]
616 y += self.window_offset[1]
617 elif self.get_decorated():
618 #try to adjust for window frame size if we can figure it out:
619 #Note: we cannot just call self.get_window_frame_size() here because
620 #the window is not realized yet, and it may take a while for the window manager
621 #to set the frame-extents property anyway
622 wfs = self._client.get_window_frame_sizes()
623 dx, dy = 0, 0
624 if wfs:
625 geomlog("setup_window() window frame sizes=%s", wfs)
626 v = wfs.get("offset")
627 if v:
628 dx, dy = v
629 x = max(0, x-dx)
630 y = max(0, y-dy)
631 self._pos = x, y
632 geomlog("setup_window() adjusted initial position=%s", self._pos)
633 self.move(x, y)
634 self.set_default_size(*self._size)
636 def new_backing(self, bw, bh):
637 b = ClientWindowBase.new_backing(self, bw, bh)
638 #call via idle_add so that the backing has time to be realized too:
639 self.when_realized("cursor", self.idle_add, self._backing.set_cursor_data, self.cursor_data)
640 return b
642 def set_cursor_data(self, cursor_data):
643 self.cursor_data = cursor_data
644 b = self._backing
645 if b:
646 self.when_realized("cursor", b.set_cursor_data, cursor_data)
648 def adjusted_position(self, ox, oy):
649 if AWT_RECENTER and self.is_awt(self._metadata):
650 ss = self._client._current_screen_sizes
651 if ss and len(ss)==1:
652 screen0 = ss[0]
653 monitors = screen0[5]
654 if monitors and len(monitors)>1:
655 monitor = monitors[0]
656 mw = monitor[3]
657 mh = monitor[4]
658 w, h = self._size
659 #adjust for window centering on monitor instead of screen java
660 screen = self.get_screen()
661 sw = screen.get_width()
662 sh = screen.get_height()
663 #re-center on first monitor if the window is within
664 #$tolerance of the center of the screen:
665 tolerance = 10
666 #center of the window:
667 cx = ox + w//2
668 cy = oy + h//2
669 if abs(sw//2 - cx) <= tolerance:
670 x = mw//2 - w//2
671 else:
672 x = ox
673 if abs(sh//2 - cy) <= tolerance:
674 y = mh//2 - h//2
675 else:
676 y = oy
677 geomlog("adjusted_position(%i, %i)=%i, %i", ox, oy, x, y)
678 return x, y
679 return ox, oy
682 def calculate_window_offset(self, wx, wy, ww, wh):
683 ss = self._client._current_screen_sizes
684 if not ss:
685 return None
686 if len(ss)!=1:
687 geomlog("cannot handle more than one screen for OR offset")
688 return None
689 screen0 = ss[0]
690 monitors = screen0[5]
691 if not monitors:
692 geomlog("screen %s lacks monitors information: %s", screen0)
693 return None
694 from xpra.rectangle import rectangle #@UnresolvedImport
695 wrect = rectangle(wx, wy, ww, wh)
696 rects = [wrect]
697 pixels_in_monitor = {}
698 for i, monitor in enumerate(monitors):
699 plug_name, x, y, w, h = monitor[:5]
700 new_rects = []
701 for rect in rects:
702 new_rects += rect.substract(x, y, w, h)
703 geomlog("after removing areas visible on %s from %s: %s", plug_name, rects, new_rects)
704 rects = new_rects
705 if not rects:
706 #the whole window is visible
707 return None
708 #keep track of how many pixels would be on this monitor:
709 inter = wrect.intersection(x, y, w, h)
710 if inter:
711 pixels_in_monitor[inter.width*inter.height] = i
712 #if we're here, then some of the window would land on an area
713 #not show on any monitors
714 #choose the monitor that had most of the pixels and make it fit:
715 geomlog("pixels in monitor=%s", pixels_in_monitor)
716 if not pixels_in_monitor:
717 i = 0
718 else:
719 best = max(pixels_in_monitor.keys())
720 i = pixels_in_monitor[best]
721 monitor = monitors[i]
722 plug_name, x, y, w, h = monitor[:5]
723 geomlog("calculating OR offset for monitor %i: %s", i, plug_name)
724 if ww>w or wh>=h:
725 geomlog("window %ix%i is bigger than the monitor %i: %s %ix%i, not adjusting it",
726 ww, wh, i, plug_name, w, h)
727 return None
728 dx = 0
729 dy = 0
730 if wx<x:
731 dx = x-wx
732 elif wx+ww>x+w:
733 dx = (x+w) - (wx+ww)
734 if wy<y:
735 dy = y-wy
736 elif wy+wh>y+h:
737 dy = (y+h) - (wy+wh)
738 assert dx!=0 or dy!=0
739 geomlog("calculate_window_offset%s=%s", (wx, wy, ww, wh), (dx, dy))
740 return dx, dy
742 def when_realized(self, identifier, callback, *args):
743 if self.get_realized():
744 callback(*args)
745 else:
746 self.on_realize_cb[identifier] = callback, args
748 def on_realize(self, widget):
749 eventslog("on_realize(%s) gdk window=%s", widget, self.get_window())
750 add_window_hooks(self)
751 cb = self.on_realize_cb
752 self.on_realize_cb = {}
753 for x, args in cb.values():
754 try:
755 x(*args)
756 except Exception:
757 log.error("Error on realize callback %s for window %i", x, self._id, exc_info=True)
758 if HAS_X11_BINDINGS:
759 #request frame extents if the window manager supports it
760 self._client.request_frame_extents(self)
761 if self.watcher_pid:
762 log("using watcher pid=%i for wid=%i", self.watcher_pid, self._id)
763 prop_set(self.get_window(), "_NET_WM_PID", "u32", self.watcher_pid)
764 if self.group_leader:
765 self.get_window().set_group(self.group_leader)
767 def on_unrealize(self, widget):
768 eventslog("on_unrealize(%s)", widget)
769 remove_window_hooks(self)
772 def set_alpha(self):
773 #try to enable alpha on this window if needed,
774 #and if the backing class can support it:
775 bc = self.get_backing_class()
776 alphalog("set_alpha() has_alpha=%s, %s.HAS_ALPHA=%s, realized=%s",
777 self._has_alpha, bc, bc.HAS_ALPHA, self.get_realized())
778 #by default, only RGB (no transparency):
779 #rgb_formats = tuple(BACKING_CLASS.RGB_MODES)
780 self._client_properties["encodings.rgb_formats"] = ["RGB", "RGBX"]
781 #only set the visual if we need to enable alpha:
782 #(breaks the headerbar otherwise!)
783 if not self.get_realized() and self._has_alpha:
784 if set_visual(self, True):
785 if self._has_alpha:
786 self._client_properties["encodings.rgb_formats"] = ["RGBA", "RGB", "RGBX"]
787 self._window_alpha = self._has_alpha
788 else:
789 alphalog("failed to set RGBA visual")
790 self._has_alpha = False
791 self._client_properties["encoding.transparency"] = False
792 if not self._has_alpha or not bc.HAS_ALPHA:
793 self._client_properties["encoding.transparency"] = False
796 def freeze(self):
797 #the OpenGL subclasses override this method to also free their GL context
798 self._frozen = True
799 self.iconify()
801 def unfreeze(self):
802 if not self._frozen or not self._iconified:
803 return
804 log("unfreeze() wid=%i, frozen=%s, iconified=%s", self._id, self._frozen, self._iconified)
805 if not self._frozen or not self._iconified:
806 #has been deiconified already
807 return
808 self._frozen = False
809 self.deiconify()
812 def window_state_updated(self, widget, event):
813 statelog("%s.window_state_updated(%s, %s) changed_mask=%s, new_window_state=%s",
814 self, widget, repr(event), event.changed_mask, event.new_window_state)
815 state_updates = {}
816 for flag in ("fullscreen", "above", "below", "sticky", "iconified", "maximized", "focused"):
817 wstate = getattr(Gdk.WindowState, flag.upper()) #ie: Gdk.WindowState.FULLSCREEN
818 if event.changed_mask & wstate:
819 state_updates[flag] = bool(event.new_window_state & wstate)
820 self.update_window_state(state_updates)
822 def update_window_state(self, state_updates):
823 if self._client.readonly:
824 log("update_window_state(%s) ignored in readonly mode", state_updates)
825 return
826 if state_updates.get("maximized") is False or state_updates.get("fullscreen") is False:
827 #if we unfullscreen or unmaximize, re-calculate offsets if we have any:
828 w, h = self._backing.render_size
829 ww, wh = self.get_size()
830 log("update_window_state(%s) unmax or unfullscreen", state_updates)
831 log("window_offset=%s, backing render_size=%s, window size=%s",
832 self.window_offset, (w, h), (ww, wh))
833 if self._backing.offsets!=(0, 0, 0, 0):
834 self.center_backing(w, h)
835 self.repaint(0, 0, ww, wh)
836 #decide if this is really an update by comparing with our local state vars:
837 #(could just be a notification of a state change we already know about)
838 actual_updates = {}
839 for state,value in state_updates.items():
840 var = "_" + state.replace("-", "_") #ie: "skip-pager" -> "_skip_pager"
841 cur = getattr(self, var) #ie: self._maximized
842 if cur!=value:
843 setattr(self, var, value) #ie: self._maximized = True
844 actual_updates[state] = value
845 statelog("%s=%s (was %s)", var, value, cur)
846 server_updates = dict((k,v) for k,v in actual_updates.items() if k in self._client.server_window_states)
847 #iconification is handled a bit differently...
848 iconified = server_updates.pop("iconified", None)
849 if iconified is not None:
850 statelog("iconified=%s", iconified)
851 #handle iconification as map events:
852 if iconified:
853 #usually means it is unmapped
854 self._unfocus()
855 if not self._override_redirect and not self.send_iconify_timer:
856 #tell server, but wait a bit to try to prevent races:
857 self.schedule_send_iconify()
858 else:
859 self.cancel_send_iconifiy_timer()
860 self._frozen = False
861 self.process_map_event()
862 statelog("window_state_updated(..) state updates: %s, actual updates: %s, server updates: %s",
863 state_updates, actual_updates, server_updates)
864 if "maximized" in state_updates:
865 if REPAINT_MAXIMIZED>0:
866 def repaint_maximized():
867 if not self._backing:
868 return
869 ww, wh = self.get_size()
870 self.repaint(0, 0, ww, wh)
871 self.timeout_add(REPAINT_MAXIMIZED, repaint_maximized)
872 if REFRESH_MAXIMIZED:
873 self._client.send_refresh(self._id)
875 self._window_state.update(server_updates)
876 self.emit("state-updated")
877 #if we have state updates, send them back to the server using a configure window packet:
878 if self._window_state and not self.window_state_timer:
879 self.window_state_timer = self.timeout_add(25, self.send_updated_window_state)
881 def send_updated_window_state(self):
882 self.window_state_timer = None
883 if self._window_state and self.get_window():
884 self.send_configure_event(True)
886 def cancel_window_state_timer(self):
887 wst = self.window_state_timer
888 if wst:
889 self.window_state_timer = None
890 self.source_remove(wst)
893 def schedule_send_iconify(self):
894 #calculate a good delay to prevent races causing minimize/unminimize loops:
895 if self._client.readonly:
896 return
897 delay = 150
898 spl = tuple(self._client.server_ping_latency)
899 if spl:
900 worst = max(x[1] for x in self._client.server_ping_latency)
901 delay += int(1000*worst)
902 delay = min(1000, delay)
903 statelog("telling server about iconification with %sms delay", delay)
904 self.send_iconify_timer = self.timeout_add(delay, self.send_iconify)
906 def send_iconify(self):
907 self.send_iconify_timer = None
908 if self._iconified:
909 self.send("unmap-window", self._id, True, self._window_state)
910 #we have sent the window-state already:
911 self._window_state = {}
912 self.cancel_window_state_timer()
914 def cancel_send_iconifiy_timer(self):
915 sit = self.send_iconify_timer
916 if sit:
917 self.send_iconify_timer = None
918 self.source_remove(sit)
921 def set_command(self, command):
922 if not HAS_X11_BINDINGS:
923 return
924 v = command
925 if not isinstance(command, str):
926 try:
927 v = v.decode("utf8")
928 except UnicodeDecodeError:
929 v = bytestostr(command)
930 def do_set_command():
931 metalog("do_set_command() str(%s)='%r' (type=%s)", command, v, type(command))
932 prop_set(self.get_window(), "WM_COMMAND", "latin1", v)
933 self.when_realized("command", do_set_command)
936 def set_x11_property(self, prop_name, dtype, dformat, value):
937 metalog("set_x11_property%s", (prop_name, dtype, dformat, value))
938 dtype = bytestostr(dtype)
939 if dtype=="latin1":
940 value = bytestostr(value)
941 if isinstance(value, (list, tuple)):
942 dtype = (dtype, )
943 def do_set_prop():
944 gdk_window = self.get_window()
945 if not dtype and not dformat:
946 #remove prop
947 prop_del(gdk_window, prop_name)
948 else:
949 prop_set(gdk_window, prop_name, dtype, value)
950 self.when_realized("x11-prop-%s" % prop_name, do_set_prop)
952 def set_class_instance(self, wmclass_name, wmclass_class):
953 if not self.get_realized():
954 #Warning: window managers may ignore the icons we try to set
955 #if the wm_class value is set and matches something somewhere undocumented
956 #(if the default is used, you cannot override the window icon)
957 self.set_wmclass(wmclass_name, wmclass_class)
958 elif HAS_X11_BINDINGS:
959 xid = self.get_window().get_xid()
960 with xlog:
961 X11Window.setClassHint(xid, strtobytes(wmclass_class), strtobytes(wmclass_name))
962 log("XSetClassHint(%s, %s) done", wmclass_class, wmclass_name)
964 def set_shape(self, shape):
965 shapelog("set_shape(%s)", shape)
966 if not HAS_X11_BINDINGS or not XSHAPE:
967 return
968 def do_set_shape():
969 xid = self.get_window().get_xid()
970 x_off, y_off = shape.get("x", 0), shape.get("y", 0)
971 for kind, name in SHAPE_KIND.items(): #@UndefinedVariable
972 rectangles = shape.get("%s.rectangles" % name) #ie: Bounding.rectangles = [(0, 0, 150, 100)]
973 if rectangles:
974 #adjust for scaling:
975 if self._client.xscale!=1 or self._client.yscale!=1:
976 x_off, y_off = self._client.sp(x_off, y_off)
977 rectangles = self.scale_shape_rectangles(name, rectangles)
978 #too expensive to log with actual rectangles:
979 shapelog("XShapeCombineRectangles(%#x, %s, %i, %i, %i rects)",
980 xid, name, x_off, y_off, len(rectangles))
981 with xlog:
982 X11Window.XShapeCombineRectangles(xid, kind, x_off, y_off, rectangles)
983 self.when_realized("shape", do_set_shape)
985 def scale_shape_rectangles(self, kind_name, rectangles):
986 if LAZY_SHAPE or len(rectangles)<2:
987 #scale the rectangles without a bitmap...
988 #results aren't so good! (but better than nothing?)
989 srect = self._client.srect
990 return [srect(*x) for x in rectangles]
991 from PIL import Image, ImageDraw #@UnresolvedImport
992 ww, wh = self._size
993 sw, sh = self._client.cp(ww, wh)
994 img = Image.new('1', (sw, sh), color=0)
995 shapelog("drawing %s on bitmap(%s,%s)=%s", kind_name, sw, sh, img)
996 d = ImageDraw.Draw(img)
997 for x,y,w,h in rectangles:
998 d.rectangle([x, y, x+w, y+h], fill=1)
999 img = img.resize((ww, wh))
1000 shapelog("resized %s bitmap to window size %sx%s: %s", kind_name, ww, wh, img)
1001 #now convert back to rectangles...
1002 rectangles = []
1003 for y in range(wh):
1004 #for debugging, this is very useful, but costly!
1005 #shapelog("pixels[%3i]=%s", y, "".join([str(img.getpixel((x, y))) for x in range(ww)]))
1006 x = 0
1007 start = None
1008 while x<ww:
1009 #find first white pixel:
1010 while x<ww and img.getpixel((x, y))==0:
1011 x += 1
1012 start = x
1013 #find next black pixel:
1014 while x<ww and img.getpixel((x, y))!=0:
1015 x += 1
1016 end = x
1017 if start<end:
1018 rectangles.append((start, y, end-start, 1))
1019 return rectangles
1021 def set_bypass_compositor(self, v):
1022 if not HAS_X11_BINDINGS:
1023 return
1024 if v not in (0, 1, 2):
1025 v = 0
1026 def do_set_bypass_compositor():
1027 prop_set(self.get_window(), "_NET_WM_BYPASS_COMPOSITOR", "u32", v)
1028 self.when_realized("bypass-compositor", do_set_bypass_compositor)
1031 def set_strut(self, strut):
1032 if not HAS_X11_BINDINGS:
1033 return
1034 log("strut=%s", strut)
1035 d = typedict(strut)
1036 values = []
1037 for x in ("left", "right", "top", "bottom"):
1038 v = d.intget(x, 0)
1039 #handle scaling:
1040 if x in ("left", "right"):
1041 v = self._client.sx(v)
1042 else:
1043 v = self._client.sy(v)
1044 values.append(v)
1045 has_partial = False
1046 for x in ("left_start_y", "left_end_y",
1047 "right_start_y", "right_end_y",
1048 "top_start_x", "top_end_x",
1049 "bottom_start_x", "bottom_end_x"):
1050 if x in d:
1051 has_partial = True
1052 v = d.intget(x, 0)
1053 if x.find("_x"):
1054 v = self._client.sx(v)
1055 elif x.find("_y"):
1056 v = self._client.sy(v)
1057 values.append(v)
1058 log("setting strut=%s, has partial=%s", values, has_partial)
1059 def do_set_strut():
1060 if has_partial:
1061 prop_set(self.get_window(), "_NET_WM_STRUT_PARTIAL", ["u32"], values)
1062 prop_set(self.get_window(), "_NET_WM_STRUT", ["u32"], values[:4])
1063 self.when_realized("strut", do_set_strut)
1066 def set_window_type(self, window_types):
1067 pass
1068 hints = 0
1069 for window_type in window_types:
1070 #win32 workaround:
1071 if AWT_DIALOG_WORKAROUND and window_type=="DIALOG" and self._metadata.boolget("skip-taskbar"):
1072 wm_class = self._metadata.strtupleget("class-instance", (None, None), 2, 2)
1073 if wm_class and len(wm_class)==2 and wm_class[0] and wm_class[0].startswith("sun-awt-X11"):
1074 #replace "DIALOG" with "NORMAL":
1075 if "NORMAL" in window_types:
1076 continue
1077 window_type = "NORMAL"
1078 hint = WINDOW_NAME_TO_HINT.get(window_type, None)
1079 if hint is not None:
1080 hints |= hint
1081 else:
1082 log("ignoring unknown window type hint: %s", window_type)
1083 log("set_window_type(%s) hints=%s", window_types, hints)
1084 if hints:
1085 self.set_type_hint(hints)
1087 def set_modal(self, modal):
1088 #with gtk2 setting the window as modal would prevent
1089 #all other windows we manage from receiving input
1090 #including other unrelated applications
1091 #what we want is "window-modal"
1092 #so we can turn this off using the "modal_windows" feature,
1093 #from the command line and the system tray:
1094 mw = self._client.modal_windows
1095 log("set_modal(%s) modal_windows=%s", modal, mw)
1096 Gtk.Window.set_modal(self, modal and mw)
1099 def set_fullscreen_monitors(self, fsm):
1100 #platform specific code:
1101 log("set_fullscreen_monitors(%s)", fsm)
1102 def do_set_fullscreen_monitors():
1103 set_fullscreen_monitors(self.get_window(), fsm)
1104 self.when_realized("fullscreen-monitors", do_set_fullscreen_monitors)
1107 def set_shaded(self, shaded):
1108 #platform specific code:
1109 log("set_shaded(%s)", shaded)
1110 def do_set_shaded():
1111 set_shaded(self.get_window(), shaded)
1112 self.when_realized("shaded", do_set_shaded)
1115 def set_fullscreen(self, fullscreen):
1116 statelog("%s.set_fullscreen(%s)", self, fullscreen)
1117 def do_set_fullscreen():
1118 if fullscreen:
1119 #we may need to temporarily remove the max-window-size restrictions
1120 #to be able to honour the fullscreen request:
1121 w, h = self.max_window_size
1122 if w>0 and h>0:
1123 self.set_size_constraints(self.size_constraints, (0, 0))
1124 self.fullscreen()
1125 else:
1126 self.unfullscreen()
1127 #re-apply size restrictions:
1128 w, h = self.max_window_size
1129 if w>0 and h>0:
1130 self.set_size_constraints(self.size_constraints, self.max_window_size)
1131 self.when_realized("fullscreen", do_set_fullscreen)
1133 def set_xid(self, xid):
1134 if not HAS_X11_BINDINGS:
1135 return
1136 if xid.startswith("0x") and xid.endswith("L"):
1137 xid = xid[:-1]
1138 try:
1139 iid = int(xid, 16)
1140 except Exception as e:
1141 log("%s.set_xid(%s) error parsing/setting xid: %s", self, xid, e)
1142 return
1143 def do_set_xid():
1144 self.xset_u32_property(self.get_window(), "XID", iid)
1145 self.when_realized("xid", do_set_xid)
1147 def xget_u32_property(self, target, name):
1148 if prop_get:
1149 v = prop_get(target, name, "u32", ignore_errors=True)
1150 log("%s.xget_u32_property(%s, %s)=%s", self, target, name, v)
1151 if isinstance(v, int):
1152 return v
1153 return None
1155 def xset_u32_property(self, target, name, value):
1156 prop_set(target, name, "u32", value)
1159 def property_changed(self, widget, event):
1160 atom = str(event.atom)
1161 statelog("property_changed(%s, %s) : %s", widget, event, atom)
1162 if atom=="_NET_WM_DESKTOP":
1163 if self._been_mapped and not self._override_redirect and self._can_set_workspace:
1164 self.do_workspace_changed(event)
1165 elif atom=="_NET_FRAME_EXTENTS":
1166 if prop_get:
1167 v = prop_get(self.get_window(), "_NET_FRAME_EXTENTS", ["u32"], ignore_errors=False)
1168 statelog("_NET_FRAME_EXTENTS: %s", v)
1169 if v:
1170 if v==self._current_frame_extents:
1171 #unchanged
1172 return
1173 if not self._been_mapped:
1174 #map event will take care of sending it
1175 return
1176 if self.is_OR() or self.is_tray():
1177 #we can't do it: the server can't handle configure packets for OR windows!
1178 return
1179 if not self._client.server_window_frame_extents:
1180 #can't send cheap "skip-geometry" packets or frame-extents feature not supported:
1181 return
1182 #tell server about new value:
1183 self._current_frame_extents = v
1184 statelog("sending configure event to update _NET_FRAME_EXTENTS to %s", v)
1185 self._window_state["frame"] = self._client.crect(*v)
1186 self.send_configure_event(True)
1187 elif atom=="XKLAVIER_STATE":
1188 if prop_get:
1189 #unused for now, but log it:
1190 xklavier_state = prop_get(self.get_window(), "XKLAVIER_STATE", ["integer"], ignore_errors=False)
1191 keylog("XKLAVIER_STATE=%s", [hex(x) for x in (xklavier_state or [])])
1192 elif atom=="_NET_WM_STATE":
1193 if prop_get:
1194 wm_state_atoms = prop_get(self.get_window(), "_NET_WM_STATE", ["atom"], ignore_errors=False)
1195 #code mostly duplicated from gtk_x11/window.py:
1196 WM_STATE_NAME = {
1197 "fullscreen" : ("_NET_WM_STATE_FULLSCREEN", ),
1198 "maximized" : ("_NET_WM_STATE_MAXIMIZED_VERT", "_NET_WM_STATE_MAXIMIZED_HORZ"),
1199 "shaded" : ("_NET_WM_STATE_SHADED", ),
1200 "sticky" : ("_NET_WM_STATE_STICKY", ),
1201 "skip-pager" : ("_NET_WM_STATE_SKIP_PAGER", ),
1202 "skip-taskbar" : ("_NET_WM_STATE_SKIP_TASKBAR", ),
1203 "above" : ("_NET_WM_STATE_ABOVE", ),
1204 "below" : ("_NET_WM_STATE_BELOW", ),
1205 "focused" : ("_NET_WM_STATE_FOCUSED", ),
1206 }
1207 state_atoms = set(wm_state_atoms or [])
1208 state_updates = {}
1209 for state, atoms in WM_STATE_NAME.items():
1210 var = "_" + state.replace("-", "_") #ie: "skip-pager" -> "_skip_pager"
1211 cur_state = getattr(self, var)
1212 wm_state_is_set = set(atoms).issubset(state_atoms)
1213 if wm_state_is_set and not cur_state:
1214 state_updates[state] = True
1215 elif cur_state and not wm_state_is_set:
1216 state_updates[state] = False
1217 log("_NET_WM_STATE=%s, state_updates=%s", wm_state_atoms, state_updates)
1218 if state_updates:
1219 self.update_window_state(state_updates)
1222 ######################################################################
1223 # workspace
1224 def workspace_changed(self):
1225 #on X11 clients, this fires from the root window property watcher
1226 ClientWindowBase.workspace_changed(self)
1227 if self._can_set_workspace:
1228 self.do_workspace_changed("desktop workspace changed")
1230 def do_workspace_changed(self, info):
1231 #call this method whenever something workspace related may have changed
1232 window_workspace = self.get_window_workspace()
1233 desktop_workspace = self.get_desktop_workspace()
1234 workspacelog("do_workspace_changed(%s) for window %i (window, desktop): from %s to %s",
1235 info, self._id,
1236 (wn(self._window_workspace), wn(self._desktop_workspace)),
1237 (wn(window_workspace), wn(desktop_workspace)))
1238 if self._window_workspace==window_workspace and self._desktop_workspace==desktop_workspace:
1239 #no change
1240 return
1241 suspend_resume = None
1242 if desktop_workspace<0 or window_workspace is None:
1243 #maybe the property has been cleared? maybe the window is being scrubbed?
1244 workspacelog("not sure if the window is shown or not: %s vs %s, resuming to be safe",
1245 wn(desktop_workspace), wn(window_workspace))
1246 suspend_resume = False
1247 elif window_workspace==WORKSPACE_UNSET:
1248 workspacelog("workspace unset: assume current")
1249 suspend_resume = False
1250 elif window_workspace==WORKSPACE_ALL:
1251 workspacelog("window is on all workspaces")
1252 suspend_resume = False
1253 elif desktop_workspace!=window_workspace:
1254 workspacelog("window is on a different workspace, increasing its batch delay")
1255 workspacelog(" desktop: %s, window: %s", wn(desktop_workspace), wn(window_workspace))
1256 suspend_resume = True
1257 elif self._window_workspace!=self._desktop_workspace:
1258 assert desktop_workspace==window_workspace
1259 workspacelog("window was on a different workspace, resetting its batch delay")
1260 workspacelog(" (was desktop: %s, window: %s, now both on %s)",
1261 wn(self._window_workspace), wn(self._desktop_workspace), wn(desktop_workspace))
1262 suspend_resume = False
1263 self._window_workspace = window_workspace
1264 self._desktop_workspace = desktop_workspace
1265 client_properties = {}
1266 if window_workspace is not None:
1267 client_properties["workspace"] = window_workspace
1268 self.send_control_refresh(suspend_resume, client_properties)
1270 def send_control_refresh(self, suspend_resume, client_properties=None, refresh=False):
1271 statelog("send_control_refresh%s", (suspend_resume, client_properties, refresh))
1272 #we can tell the server using a "buffer-refresh" packet instead
1273 #and also take care of tweaking the batch config
1274 options = {"refresh-now" : refresh} #no need to refresh it
1275 self._client.control_refresh(self._id, suspend_resume, refresh=refresh, options=options, client_properties=client_properties)
1277 def get_workspace_count(self):
1278 if not self._can_set_workspace:
1279 return None
1280 root = get_default_root_window()
1281 return self.xget_u32_property(root, "_NET_NUMBER_OF_DESKTOPS")
1283 def set_workspace(self, workspace):
1284 workspacelog("set_workspace(%s)", workspace)
1285 if not self._can_set_workspace:
1286 return
1287 if not self._been_mapped:
1288 #will be dealt with in the map event handler
1289 #which will look at the window metadata again
1290 workspacelog("workspace=%s will be set when the window is mapped", wn(workspace))
1291 return
1292 workspace = workspace
1293 if workspace is not None:
1294 workspace = workspace & 0xffffffff
1295 desktop = self.get_desktop_workspace()
1296 ndesktops = self.get_workspace_count()
1297 current = self.get_window_workspace()
1298 workspacelog("set_workspace(%s) realized=%s", wn(workspace), self.get_realized())
1299 workspacelog(" current workspace=%s, detected=%s, desktop workspace=%s, ndesktops=%s",
1300 wn(self._window_workspace), wn(current), wn(desktop), ndesktops)
1301 if not self._can_set_workspace or ndesktops is None:
1302 return
1303 if workspace==desktop or workspace==WORKSPACE_ALL or desktop is None:
1304 #window is back in view
1305 self._client.control_refresh(self._id, False, False)
1306 if (workspace<0 or workspace>=ndesktops) and workspace not in(WORKSPACE_UNSET, WORKSPACE_ALL):
1307 #this should not happen, workspace is unsigned (CARDINAL)
1308 #and the server should have the same list of desktops that we have here
1309 workspacelog.warn("Warning: invalid workspace number: %s", wn(workspace))
1310 workspace = WORKSPACE_UNSET
1311 if workspace==WORKSPACE_UNSET:
1312 #we cannot unset via send_wm_workspace, so we have to choose one:
1313 workspace = self.get_desktop_workspace()
1314 if workspace in (None, WORKSPACE_UNSET):
1315 workspacelog.warn("workspace=%s (doing nothing)", wn(workspace))
1316 return
1317 #we will need the gdk window:
1318 if current==workspace:
1319 workspacelog("window workspace unchanged: %s", wn(workspace))
1320 return
1321 gdkwin = self.get_window()
1322 workspacelog("do_set_workspace: gdkwindow: %#x, mapped=%s, visible=%s",
1323 gdkwin.get_xid(), self.get_mapped(), gdkwin.is_visible())
1324 root = get_default_root_window()
1325 with xlog:
1326 send_wm_workspace(root, gdkwin, workspace)
1328 def get_desktop_workspace(self):
1329 window = self.get_window()
1330 if window:
1331 root = window.get_screen().get_root_window()
1332 else:
1333 #if we are called during init.. we don't have a window
1334 root = get_default_root_window()
1335 return self.do_get_workspace(root, "_NET_CURRENT_DESKTOP")
1337 def get_window_workspace(self):
1338 return self.do_get_workspace(self.get_window(), "_NET_WM_DESKTOP", WORKSPACE_UNSET)
1340 def do_get_workspace(self, target, prop, default_value=None):
1341 if not self._can_set_workspace:
1342 workspacelog("do_get_workspace: not supported, returning %s", wn(default_value))
1343 return default_value #windows and OSX do not have workspaces
1344 if target is None:
1345 workspacelog("do_get_workspace: target is None, returning %s", wn(default_value))
1346 return default_value #window is not realized yet
1347 value = self.xget_u32_property(target, prop)
1348 if value is not None:
1349 workspacelog("do_get_workspace %s=%s on window %i: %#x",
1350 prop, wn(value), self._id, target.get_xid())
1351 return value & 0xffffffff
1352 workspacelog("do_get_workspace %s unset on window %i: %#x, returning default value=%s",
1353 prop, self._id, target.get_xid(), wn(default_value))
1354 return default_value
1357 def keyboard_ungrab(self, *args):
1358 grablog("keyboard_ungrab%s", args)
1359 self._client.keyboard_grabbed = False
1360 gdkwin = self.get_window()
1361 if gdkwin:
1362 d = gdkwin.get_display()
1363 if d:
1364 d.keyboard_ungrab(0)
1365 return True
1367 def keyboard_grab(self, *args):
1368 grablog("keyboard_grab%s", args)
1369 r = Gdk.keyboard_grab(self.get_window(), True, 0)
1370 self._client.keyboard_grabbed = r==Gdk.GrabStatus.SUCCESS
1371 grablog("keyboard_grab%s Gdk.keyboard_grab(%s, True)=%s, keyboard_grabbed=%s",
1372 args, self.get_window(), GRAB_STATUS_STRING.get(r), self._client.keyboard_grabbed)
1374 def toggle_keyboard_grab(self):
1375 grabbed = self._client.keyboard_grabbed
1376 grablog("toggle_keyboard_grab() grabbed=%s", grabbed)
1377 if grabbed:
1378 self.keyboard_ungrab()
1379 else:
1380 self.keyboard_grab()
1382 def pointer_grab(self, *args):
1383 gdkwin = self.get_window()
1384 em = Gdk.EventMask
1385 event_mask = (em.BUTTON_PRESS_MASK |
1386 em.BUTTON_RELEASE_MASK |
1387 em.POINTER_MOTION_MASK |
1388 em.POINTER_MOTION_HINT_MASK |
1389 em.ENTER_NOTIFY_MASK |
1390 em.LEAVE_NOTIFY_MASK)
1391 r = Gdk.pointer_grab(gdkwin, True, event_mask, gdkwin, None, 0)
1392 self._client.pointer_grabbed = r==Gdk.GrabStatus.SUCCESS
1393 grablog("pointer_grab%s Gdk.pointer_grab(%s, True)=%s, pointer_grabbed=%s",
1394 args, self.get_window(), GRAB_STATUS_STRING.get(r), self._client.pointer_grabbed)
1396 def pointer_ungrab(self, *args):
1397 grablog("pointer_ungrab%s pointer_grabbed=%s",
1398 args, self._client.pointer_grabbed)
1399 self._client.pointer_grabbed = False
1400 gdkwin = self.get_window()
1401 if gdkwin:
1402 d = gdkwin.get_display()
1403 if d:
1404 d.pointer_ungrab(0)
1405 return True
1407 def toggle_pointer_grab(self):
1408 pg = self._client.pointer_grabbed
1409 grablog("toggle_pointer_grab() pointer_grabbed=%s", pg)
1410 if pg:
1411 self.pointer_ungrab()
1412 else:
1413 self.pointer_grab()
1416 def toggle_fullscreen(self):
1417 geomlog("toggle_fullscreen()")
1418 if self._fullscreen:
1419 self.unfullscreen()
1420 else:
1421 self.fullscreen()
1424 ######################################################################
1425 # pointer overlay handling
1426 def cancel_remove_pointer_overlay_timer(self):
1427 rpot = self.remove_pointer_overlay_timer
1428 if rpot:
1429 self.remove_pointer_overlay_timer = None
1430 self.source_remove(rpot)
1432 def cancel_show_pointer_overlay_timer(self):
1433 rsot = self.show_pointer_overlay_timer
1434 if rsot:
1435 self.show_pointer_overlay_timer = None
1436 self.source_remove(rsot)
1438 def show_pointer_overlay(self, pos):
1439 #schedule do_show_pointer_overlay if needed
1440 b = self._backing
1441 if not b:
1442 return
1443 prev = b.pointer_overlay
1444 if pos is None:
1445 if prev is None:
1446 return
1447 value = None
1448 else:
1449 if prev and prev[:2]==pos[:2]:
1450 return
1451 #store both scaled and unscaled value:
1452 #(the opengl client uses the raw value)
1453 value = pos[:2]+self._client.sp(*pos[:2])+pos[2:]
1454 mouselog("show_pointer_overlay(%s) previous value=%s, new value=%s", pos, prev, value)
1455 b.pointer_overlay = value
1456 if not self.show_pointer_overlay_timer:
1457 self.show_pointer_overlay_timer = self.timeout_add(10, self.do_show_pointer_overlay, prev)
1459 def do_show_pointer_overlay(self, prev):
1460 #queue a draw event at the previous and current position of the pointer
1461 #(so the backend will repaint / overlay the cursor image there)
1462 self.show_pointer_overlay_timer = None
1463 b = self._backing
1464 if not b:
1465 return
1466 cursor_data = b.cursor_data
1467 def abs_coords(x, y, size):
1468 if self.window_offset:
1469 x += self.window_offset[0]
1470 y += self.window_offset[1]
1471 w, h = size, size
1472 if cursor_data:
1473 w = cursor_data[3]
1474 h = cursor_data[4]
1475 xhot = cursor_data[5]
1476 yhot = cursor_data[6]
1477 x = x-xhot
1478 y = y-yhot
1479 return x, y, w, h
1480 value = b.pointer_overlay
1481 if value:
1482 #repaint the scale value (in window coordinates):
1483 x, y, w, h = abs_coords(*value[2:5])
1484 self.repaint(x, y, w, h)
1485 #clear it shortly after:
1486 self.cancel_remove_pointer_overlay_timer()
1487 def remove_pointer_overlay():
1488 self.remove_pointer_overlay_timer = None
1489 self.show_pointer_overlay(None)
1490 self.remove_pointer_overlay_timer = self.timeout_add(CURSOR_IDLE_TIMEOUT*1000, remove_pointer_overlay)
1491 if prev:
1492 x, y, w, h = abs_coords(*prev[2:5])
1493 self.repaint(x, y, w, h)
1496 def _do_button_press_event(self, event):
1497 #Gtk.Window.do_button_press_event(self, event)
1498 self._button_action(event.button, event, True)
1500 def _do_button_release_event(self, event):
1501 #Gtk.Window.do_button_release_event(self, event)
1502 self._button_action(event.button, event, False)
1504 ######################################################################
1505 # pointer motion
1507 def _do_motion_notify_event(self, event):
1508 #Gtk.Window.do_motion_notify_event(self, event)
1509 if self.moveresize_event:
1510 self.motion_moveresize(event)
1511 ClientWindowBase._do_motion_notify_event(self, event)
1513 def motion_moveresize(self, event):
1514 x_root, y_root, direction, button, start_buttons, wx, wy, ww, wh = self.moveresize_event
1515 dirstr = MOVERESIZE_DIRECTION_STRING.get(direction, direction)
1516 buttons = self._event_buttons(event)
1517 geomlog("motion_moveresize(%s) direction=%s, buttons=%s", event, dirstr, buttons)
1518 if start_buttons is None:
1519 #first time around, store the buttons
1520 start_buttons = buttons
1521 self.moveresize_event[4] = buttons
1522 if (button>0 and button not in buttons) or (button==0 and start_buttons!=buttons):
1523 geomlog("%s for window button %i is no longer pressed (buttons=%s) cancelling moveresize",
1524 dirstr, button, buttons)
1525 self.moveresize_event = None
1526 self.cancel_moveresize_timer()
1527 else:
1528 x = event.x_root
1529 y = event.y_root
1530 dx = x-x_root
1531 dy = y-y_root
1532 #clamp resizing using size hints,
1533 #or sane defaults: minimum of (1x1) and maximum of (2*15x2*25)
1534 minw = self.geometry_hints.get("min_width", 1)
1535 minh = self.geometry_hints.get("min_height", 1)
1536 maxw = self.geometry_hints.get("max_width", 2**15)
1537 maxh = self.geometry_hints.get("max_height", 2**15)
1538 geomlog("%s: min=%ix%i, max=%ix%i, window=%ix%i, delta=%ix%i",
1539 dirstr, minw, minh, maxw, maxh, ww, wh, dx, dy)
1540 if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_BOTTOM, MOVERESIZE_SIZE_BOTTOMLEFT):
1541 #height will be set to: wh+dy
1542 dy = max(minh-wh, dy)
1543 dy = min(maxh-wh, dy)
1544 elif direction in (MOVERESIZE_SIZE_TOPRIGHT, MOVERESIZE_SIZE_TOP, MOVERESIZE_SIZE_TOPLEFT):
1545 #height will be set to: wh-dy
1546 dy = min(wh-minh, dy)
1547 dy = max(wh-maxh, dy)
1548 if direction in (MOVERESIZE_SIZE_BOTTOMRIGHT, MOVERESIZE_SIZE_RIGHT, MOVERESIZE_SIZE_TOPRIGHT):
1549 #width will be set to: ww+dx
1550 dx = max(minw-ww, dx)
1551 dx = min(maxw-ww, dx)
1552 elif direction in (MOVERESIZE_SIZE_BOTTOMLEFT, MOVERESIZE_SIZE_LEFT, MOVERESIZE_SIZE_TOPLEFT):
1553 #width will be set to: ww-dx
1554 dx = min(ww-minw, dx)
1555 dx = max(ww-maxw, dx)
1556 #calculate move + resize:
1557 if direction==MOVERESIZE_MOVE:
1558 data = (wx+dx, wy+dy), None
1559 elif direction==MOVERESIZE_SIZE_BOTTOMRIGHT:
1560 data = None, (ww+dx, wh+dy)
1561 elif direction==MOVERESIZE_SIZE_BOTTOM:
1562 data = None, (ww, wh+dy)
1563 elif direction==MOVERESIZE_SIZE_BOTTOMLEFT:
1564 data = (wx+dx, wy), (ww-dx, wh+dy)
1565 elif direction==MOVERESIZE_SIZE_RIGHT:
1566 data = None, (ww+dx, wh)
1567 elif direction==MOVERESIZE_SIZE_LEFT:
1568 data = (wx+dx, wy), (ww-dx, wh)
1569 elif direction==MOVERESIZE_SIZE_TOPRIGHT:
1570 data = (wx, wy+dy), (ww+dx, wh-dy)
1571 elif direction==MOVERESIZE_SIZE_TOP:
1572 data = (wx, wy+dy), (ww, wh-dy)
1573 elif direction==MOVERESIZE_SIZE_TOPLEFT:
1574 data = (wx+dx, wy+dy), (ww-dx, wh-dy)
1575 else:
1576 #not handled yet!
1577 data = None
1578 geomlog("%s for window %ix%i: started at %s, now at %s, delta=%s, button=%s, buttons=%s, data=%s",
1579 dirstr, ww, wh, (x_root, y_root), (x, y), (dx, dy), button, buttons, data)
1580 if data:
1581 #modifying the window is slower than moving the pointer,
1582 #do it via a timer to batch things together
1583 self.moveresize_data = data
1584 if self.moveresize_timer is None:
1585 self.moveresize_timer = self.timeout_add(20, self.do_moveresize)
1587 def cancel_moveresize_timer(self):
1588 mrt = self.moveresize_timer
1589 if mrt:
1590 self.moveresize_timer = None
1591 self.source_remove(mrt)
1593 def do_moveresize(self):
1594 self.moveresize_timer = None
1595 mrd = self.moveresize_data
1596 geomlog("do_moveresize() data=%s", mrd)
1597 if not mrd:
1598 return
1599 move, resize = mrd
1600 if move:
1601 x, y = int(move[0]), int(move[1])
1602 if resize:
1603 w, h = int(resize[0]), int(resize[1])
1604 if self._client.readonly:
1605 #change size-constraints first,
1606 #so the resize can be honoured:
1607 sc = self._force_size_constraint(w, h)
1608 self._metadata.update(sc)
1609 self.set_metadata(sc)
1610 if move and resize:
1611 self.get_window().move_resize(x, y, w, h)
1612 elif move:
1613 self.get_window().move(x, y)
1614 elif resize:
1615 self.get_window().resize(w, h)
1618 def initiate_moveresize(self, x_root, y_root, direction, button, source_indication):
1619 geomlog("initiate_moveresize%s",
1620 (x_root, y_root, MOVERESIZE_DIRECTION_STRING.get(direction, direction),
1621 button, SOURCE_INDICATION_STRING.get(source_indication, source_indication)))
1622 #the values we get are bogus!
1623 #x, y = x_root, y_root
1624 #use the current position instead:
1625 p = self.get_root_window().get_pointer()[-3:-1]
1626 x, y = p[0], p[1]
1627 if MOVERESIZE_X11 and HAS_X11_BINDINGS:
1628 self.initiate_moveresize_X11(x, y, direction, button, source_indication)
1629 return
1630 if direction==MOVERESIZE_CANCEL:
1631 self.moveresize_event = None
1632 self.moveresize_data = None
1633 self.cancel_moveresize_timer()
1634 elif MOVERESIZE_GDK:
1635 if direction in (MOVERESIZE_MOVE, MOVERESIZE_MOVE_KEYBOARD):
1636 self.begin_move_drag(button, x, y, 0)
1637 else:
1638 edge = {
1639 MOVERESIZE_SIZE_TOPLEFT : Gdk.WindowEdge.NORTH_WEST,
1640 MOVERESIZE_SIZE_TOP : Gdk.WindowEdge.NORTH,
1641 MOVERESIZE_SIZE_TOPRIGHT : Gdk.WindowEdge.NORTH_EAST,
1642 MOVERESIZE_SIZE_RIGHT : Gdk.WindowEdge.EAST,
1643 MOVERESIZE_SIZE_BOTTOMRIGHT : Gdk.WindowEdge.SOUTH_EAST,
1644 MOVERESIZE_SIZE_BOTTOM : Gdk.WindowEdge.SOUTH,
1645 MOVERESIZE_SIZE_BOTTOMLEFT : Gdk.WindowEdge.SOUTH_WEST,
1646 MOVERESIZE_SIZE_LEFT : Gdk.WindowEdge.WEST,
1647 #MOVERESIZE_SIZE_KEYBOARD,
1648 }.get(direction)
1649 geomlog("edge(%s)=%s", MOVERESIZE_DIRECTION_STRING.get(direction), edge)
1650 if direction is not None:
1651 self.begin_resize_drag(edge, button, x, y, 0)
1652 else:
1653 #handle it ourselves:
1654 #use window coordinates (which include decorations)
1655 wx, wy = self.get_window().get_root_origin()
1656 ww, wh = self.get_size()
1657 self.moveresize_event = [x_root, y_root, direction, button, None, wx, wy, ww, wh]
1659 def initiate_moveresize_X11(self, x_root, y_root, direction, button, source_indication):
1660 statelog("initiate_moveresize_X11%s",
1661 (x_root, y_root, MOVERESIZE_DIRECTION_STRING.get(direction, direction),
1662 button, SOURCE_INDICATION_STRING.get(source_indication, source_indication)))
1663 event_mask = SubstructureNotifyMask | SubstructureRedirectMask
1664 root = self.get_window().get_screen().get_root_window()
1665 root_xid = root.get_xid()
1666 xwin = self.get_window().get_xid()
1667 with xlog:
1668 X11Core.UngrabPointer()
1669 X11Window.sendClientMessage(root_xid, xwin, False, event_mask, "_NET_WM_MOVERESIZE",
1670 x_root, y_root, direction, button, source_indication)
1673 def apply_transient_for(self, wid):
1674 if wid==-1:
1675 def set_root_transient():
1676 #root is a gdk window, so we need to ensure we have one
1677 #backing our gtk window to be able to call set_transient_for on it
1678 log("%s.apply_transient_for(%s) gdkwindow=%s, mapped=%s",
1679 self, wid, self.get_window(), self.get_mapped())
1680 self.get_window().set_transient_for(get_default_root_window())
1681 self.when_realized("transient-for-root", set_root_transient)
1682 else:
1683 #gtk window is easier:
1684 window = self._client._id_to_window.get(wid)
1685 log("%s.apply_transient_for(%s) window=%s", self, wid, window)
1686 if window:
1687 self.set_transient_for(window)
1689 def cairo_paint_border(self, context, clip_area=None):
1690 log("cairo_paint_border(%s, %s)", context, clip_area)
1691 b = self.border
1692 if b is None or not b.shown:
1693 return
1694 s = b.size
1695 ww, wh = self.get_size()
1696 borders = []
1697 #window is wide enough, add borders on the side:
1698 borders.append((0, 0, s, wh)) #left
1699 borders.append((ww-s, 0, s, wh)) #right
1700 #window is tall enough, add borders on top and bottom:
1701 borders.append((0, 0, ww, s)) #top
1702 borders.append((0, wh-s, ww, s)) #bottom
1703 for x, y, w, h in borders:
1704 if w<=0 or h<=0:
1705 continue
1706 r = Gdk.Rectangle()
1707 r.x = x
1708 r.y = y
1709 r.width = w
1710 r.height = h
1711 rect = r
1712 if clip_area:
1713 rect = clip_area.intersect(r)
1714 if rect.width==0 or rect.height==0:
1715 continue
1716 context.save()
1717 context.rectangle(x, y, w, h)
1718 context.clip()
1719 context.set_source_rgba(self.border.red, self.border.green, self.border.blue, self.border.alpha)
1720 context.fill()
1721 context.paint()
1722 context.restore()
1725 def paint_spinner(self, context, area=None):
1726 log("%s.paint_spinner(%s, %s)", self, context, area)
1727 c = self._client
1728 if not c:
1729 return
1730 ww, wh = self.get_size()
1731 w = c.cx(ww)
1732 h = c.cy(wh)
1733 #add grey semi-opaque layer on top:
1734 context.set_operator(OPERATOR_OVER)
1735 context.set_source_rgba(0.2, 0.2, 0.2, 0.4)
1736 #we can't use the area as rectangle with:
1737 #context.rectangle(area)
1738 #because those would be unscaled dimensions
1739 #it's easier and safer to repaint the whole window:
1740 context.rectangle(0, 0, w, h)
1741 context.fill()
1742 #add spinner:
1743 dim = min(w/3.0, h/3.0, 100.0)
1744 context.set_line_width(dim/10.0)
1745 context.set_line_cap(LINE_CAP_ROUND)
1746 context.translate(w/2, h/2)
1747 from xpra.client.spinner import cv
1748 count = int(monotonic_time()*4.0)
1749 for i in range(8): #8 lines
1750 context.set_source_rgba(0, 0, 0, cv.trs[count%8][i])
1751 context.move_to(0.0, -dim/4.0)
1752 context.line_to(0.0, -dim)
1753 context.rotate(math.pi/4)
1754 context.stroke()
1756 def spinner(self, _ok):
1757 c = self._client
1758 if not self.can_have_spinner() or not c:
1759 return
1760 #with normal windows, we just queue a draw request
1761 #and let the expose event paint the spinner
1762 w, h = self.get_size()
1763 self.repaint(0, 0, w, h)
1766 def do_map_event(self, event):
1767 log("%s.do_map_event(%s) OR=%s", self, event, self._override_redirect)
1768 Gtk.Window.do_map_event(self, event)
1769 if not self._override_redirect:
1770 #we can get a map event for an iconified window on win32:
1771 if self._iconified:
1772 self.deiconify()
1773 self.process_map_event()
1774 #use the drawing area to enforce the minimum size:
1775 #(as this also honoured correctly with CSD,
1776 # whereas set_geometry_hints is not..)
1777 minw, minh = self.size_constraints.intpair("minimum-size", (0, 0))
1778 self.drawing_area.set_size_request(minw, minh)
1780 def process_map_event(self):
1781 x, y, w, h = self.get_drawing_area_geometry()
1782 state = self._window_state
1783 props = self._client_properties
1784 self._client_properties = {}
1785 self._window_state = {}
1786 self.cancel_window_state_timer()
1787 workspace = self.get_window_workspace()
1788 if self._been_mapped:
1789 if workspace is None:
1790 #not set, so assume it is on the current workspace:
1791 workspace = self.get_desktop_workspace()
1792 else:
1793 self._been_mapped = True
1794 workspace = self._metadata.intget("workspace", WORKSPACE_UNSET)
1795 if workspace!=WORKSPACE_UNSET:
1796 log("map event set workspace %s", wn(workspace))
1797 self.set_workspace(workspace)
1798 if self._window_workspace!=workspace and workspace is not None:
1799 workspacelog("map event: been_mapped=%s, changed workspace from %s to %s",
1800 self._been_mapped, wn(self._window_workspace), wn(workspace))
1801 self._window_workspace = workspace
1802 if workspace is not None:
1803 props["workspace"] = workspace
1804 if self._client.server_window_frame_extents and "frame" not in state:
1805 wfs = self.get_window_frame_size()
1806 if wfs and len(wfs)==4:
1807 state["frame"] = self._client.crect(*wfs)
1808 self._current_frame_extents = wfs
1809 geomlog("map-window wid=%s, geometry=%s, client props=%s, state=%s", self._id, (x, y, w, h), props, state)
1810 cx = self._client.cx
1811 cy = self._client.cy
1812 sx, sy, sw, sh = cx(x), cy(y), cx(w), cy(h)
1813 packet = ["map-window", self._id, sx, sy, sw, sh, props, state]
1814 self.send(*packet)
1815 self._pos = (x, y)
1816 self._size = (w, h)
1817 if self._backing is None:
1818 #we may have cleared the backing, so we must re-create one:
1819 self._set_backing_size(w, h)
1820 if not self._override_redirect:
1821 htf = self.has_toplevel_focus()
1822 focuslog("mapped: has-toplevel-focus=%s", htf)
1823 if htf:
1824 self._client.update_focus(self._id, htf)
1826 def get_window_frame_size(self):
1827 frame = self._client.get_frame_extents(self)
1828 if not frame:
1829 #default to global value we may have:
1830 wfs = self._client.get_window_frame_sizes()
1831 if wfs:
1832 frame = wfs.get("frame")
1833 return frame
1836 def send_configure(self):
1837 self.send_configure_event()
1839 def do_configure_event(self, event):
1840 eventslog("%s.do_configure_event(%s) OR=%s, iconified=%s",
1841 self, event, self._override_redirect, self._iconified)
1842 Gtk.Window.do_configure_event(self, event)
1843 if not self._override_redirect and not self._iconified:
1844 self.process_configure_event()
1846 def process_configure_event(self, skip_geometry=False):
1847 assert skip_geometry or not self.is_OR()
1848 x, y, w, h = self.get_drawing_area_geometry()
1849 w = max(1, w)
1850 h = max(1, h)
1851 ox, oy = self._pos
1852 dx, dy = x-ox, y-oy
1853 self._pos = (x, y)
1854 self.send_configure_event(skip_geometry)
1855 if dx!=0 or dy!=0:
1856 #window has moved, also move any child OR window:
1857 for window in self._override_redirect_windows:
1858 x, y = window.get_position()
1859 window.move(x+dx, y+dy)
1860 geomlog("configure event: current size=%s, new size=%s, backing=%s, iconified=%s",
1861 self._size, (w, h), self._backing, self._iconified)
1862 self._size = (w, h)
1863 self._set_backing_size(w, h)
1864 if self._backing and not self._iconified:
1865 geomlog("configure event: size unchanged, queueing redraw")
1866 self.repaint(0, 0, w, h)
1868 def send_configure_event(self, skip_geometry=False):
1869 assert skip_geometry or not self.is_OR()
1870 x, y, w, h = self.get_drawing_area_geometry()
1871 w = max(1, w)
1872 h = max(1, h)
1873 state = self._window_state
1874 props = self._client_properties
1875 self._client_properties = {}
1876 self._window_state = {}
1877 self.cancel_window_state_timer()
1878 if self._been_mapped:
1879 #if the window has been mapped already, the workspace should be set:
1880 workspace = self.get_window_workspace()
1881 if self._window_workspace!=workspace and workspace is not None:
1882 workspacelog("send_configure_event: changed workspace from %s to %s",
1883 wn(self._window_workspace), wn(workspace))
1884 self._window_workspace = workspace
1885 props["workspace"] = workspace
1886 cx = self._client.cx
1887 cy = self._client.cy
1888 sx, sy, sw, sh = cx(x), cy(y), cx(w), cy(h)
1889 packet = ["configure-window", self._id, sx, sy, sw, sh, props, self._resize_counter, state, skip_geometry]
1890 pwid = self._id
1891 if self.is_OR():
1892 pwid = -1
1893 packet.append(pwid)
1894 packet.append(self._client.get_mouse_position())
1895 packet.append(self._client.get_current_modifiers())
1896 geomlog("%s", packet)
1897 self.send(*packet)
1899 def _set_backing_size(self, ww, wh):
1900 b = self._backing
1901 bw = self._client.cx(ww)
1902 bh = self._client.cy(wh)
1903 if max(ww, wh)>=32000 or min(ww, wh)<0:
1904 raise Exception("invalid window size %ix%i" % (ww, wh))
1905 if max(bw, bh)>=32000:
1906 raise Exception("invalid window backing size %ix%i" % (bw, bh))
1907 if b:
1908 b.init(ww, wh, bw, bh)
1909 else:
1910 self.new_backing(bw, bh)
1911 self._client_properties["encoding.render-size"] = b.render_size
1913 def resize(self, w, h, resize_counter=0):
1914 ww, wh = self.get_size()
1915 geomlog("resize(%s, %s, %s) current size=%s, fullscreen=%s, maximized=%s",
1916 w, h, resize_counter, (ww, wh), self._fullscreen, self._maximized)
1917 self._resize_counter = resize_counter
1918 if (w, h)==(ww, wh):
1919 self._backing.offsets = 0, 0, 0, 0
1920 self.repaint(0, 0, w, h)
1921 return
1922 if not self._fullscreen and not self._maximized:
1923 Gtk.Window.resize(self, w, h)
1924 ww, wh = w, h
1925 self._backing.offsets = 0, 0, 0, 0
1926 else:
1927 self.center_backing(w, h)
1928 geomlog("backing offsets=%s, window offset=%s", self._backing.offsets, self.window_offset)
1929 self._set_backing_size(w, h)
1930 self.repaint(0, 0, ww, wh)
1932 def center_backing(self, w, h):
1933 ww, wh = self.get_size()
1934 #align in the middle:
1935 dw = max(0, ww-w)
1936 dh = max(0, wh-h)
1937 ox = dw//2
1938 oy = dh//2
1939 geomlog("using window offset values %i,%i", ox, oy)
1940 #some backings use top,left values,
1941 #(opengl uses left and botton since the viewport starts at the bottom)
1942 self._backing.offsets = ox, oy, ox+(dw&0x1), oy+(dh&0x1)
1943 geomlog("center_backing(%i, %i) window size=%ix%i, backing offsets=%s", w, h, ww, wh, self._backing.offsets)
1944 #adjust pointer coordinates:
1945 self.window_offset = ox, oy
1947 def paint_backing_offset_border(self, backing, context):
1948 w,h = self.get_size()
1949 left, top, right, bottom = backing.offsets
1950 if left!=0 or top!=0 or right!=0 or bottom!=0:
1951 context.save()
1952 context.set_source_rgb(*PADDING_COLORS)
1953 coords = (
1954 (0, 0, left, h), #left hand side padding
1955 (0, 0, w, top), #top padding
1956 (w-right, 0, right, h), #RHS
1957 (0, h-bottom, w, bottom), #bottom
1958 )
1959 geomlog("paint_backing_offset_border(%s, %s) offsets=%s, size=%s, rgb=%s, coords=%s",
1960 backing, context, backing.offsets, (w,h), PADDING_COLORS, coords)
1961 for rx, ry, rw, rh in coords:
1962 if rw>0 and rh>0:
1963 context.rectangle(rx, ry, rw, rh)
1964 context.fill()
1965 context.restore()
1967 def clip_to_backing(self, backing, context):
1968 w,h = self.get_size()
1969 left, top, right, bottom = backing.offsets
1970 clip_rect = (left, top, w-left-right, h-top-bottom)
1971 context.rectangle(*clip_rect)
1972 geomlog("clip_to_backing%s rectangle=%s", (backing, context), clip_rect)
1973 context.clip()
1975 def move_resize(self, x, y, w, h, resize_counter=0):
1976 geomlog("window %i move_resize%s", self._id, (x, y, w, h, resize_counter))
1977 x, y = self.adjusted_position(x, y)
1978 w = max(1, w)
1979 h = max(1, h)
1980 if self.window_offset:
1981 x += self.window_offset[0]
1982 y += self.window_offset[1]
1983 #TODO: check this doesn't move it off-screen!
1984 self._resize_counter = resize_counter
1985 wx, wy = self.get_drawing_area_geometry()[:2]
1986 if (wx, wy)==(x, y):
1987 #same location, just resize:
1988 if self._size==(w, h):
1989 geomlog("window unchanged")
1990 else:
1991 geomlog("unchanged position %ix%i, using resize(%i, %i)", x, y, w, h)
1992 self.resize(w, h)
1993 return
1994 #we have to move:
1995 if not self.get_realized():
1996 geomlog("window was not realized yet")
1997 self.realize()
1998 #adjust for window frame:
1999 window = self.get_window()
2000 ox, oy = window.get_origin()[-2:]
2001 rx, ry = window.get_root_origin()
2002 ax = x - (ox - rx)
2003 ay = y - (oy - ry)
2004 geomlog("window origin=%ix%i, root origin=%ix%i, actual position=%ix%i", ox, oy, rx, ry, ax, ay)
2005 #validate against edge of screen (ensure window is shown):
2006 if CLAMP_WINDOW_TO_SCREEN:
2007 mw, mh = self._client.get_root_size()
2008 if (ax + w)<=0:
2009 ax = -w + 1
2010 elif ax >= mw:
2011 ax = mw - 1
2012 if not WINDOW_OVERFLOW_TOP and ay<=0:
2013 ay = 0
2014 elif (ay + h)<=0:
2015 ay = -y + 1
2016 elif ay >= mh:
2017 ay = mh -1
2018 geomlog("validated window position for total screen area %ix%i : %ix%i", mw, mh, ax, ay)
2019 if self._size==(w, h):
2020 #just move:
2021 geomlog("window size unchanged: %ix%i, using move(%i, %i)", w, h, ax, ay)
2022 window.move(ax, ay)
2023 return
2024 #resize:
2025 self._size = (w, h)
2026 geomlog("%s.move_resize%s", window, (ax, ay, w, h))
2027 window.move_resize(ax, ay, w, h)
2028 #re-init the backing with the new size
2029 self._set_backing_size(w, h)
2032 def noop_destroy(self):
2033 log.warn("Warning: window destroy called twice!")
2035 def destroy(self): #pylint: disable=method-hidden
2036 self.cancel_window_state_timer()
2037 self.cancel_send_iconifiy_timer()
2038 self.cancel_show_pointer_overlay_timer()
2039 self.cancel_remove_pointer_overlay_timer()
2040 self.cancel_focus_timer()
2041 self.cancel_moveresize_timer()
2042 self.on_realize_cb = {}
2043 ClientWindowBase.destroy(self)
2044 Gtk.Window.destroy(self)
2045 self._unfocus()
2046 self.destroy = self.noop_destroy
2049 def do_unmap_event(self, event):
2050 eventslog("do_unmap_event(%s)", event)
2051 self._unfocus()
2052 if not self._override_redirect:
2053 self.send("unmap-window", self._id, False)
2055 def do_delete_event(self, event):
2056 #Gtk.Window.do_delete_event(self, event)
2057 eventslog("do_delete_event(%s)", event)
2058 self._client.window_close_event(self._id)
2059 return True
2062 def _offset_pointer(self, x, y):
2063 if self.window_offset:
2064 x -= self.window_offset[0]
2065 y -= self.window_offset[1]
2066 return self._client.cp(x, y)
2068 def _get_pointer(self, event):
2069 return event.x_root, event.y_root
2071 def _get_relative_pointer(self, event):
2072 return event.x, event.y
2074 def _pointer_modifiers(self, event):
2075 x, y = self._get_pointer(event)
2076 rx, ry = self._get_relative_pointer(event)
2077 #adjust for window offset:
2078 pointer = self._offset_pointer(x, y)
2079 relative_pointer = self._client.cp(rx, ry)
2080 #FIXME: state is used for both mods and buttons??
2081 modifiers = self._client.mask_to_names(event.state)
2082 buttons = self._event_buttons(event)
2083 v = pointer, relative_pointer, modifiers, buttons
2084 mouselog("pointer_modifiers(%s)=%s (x_root=%s, y_root=%s, window_offset=%s)",
2085 event, v, event.x_root, event.y_root, self.window_offset)
2086 return v
2088 def _event_buttons(self, event):
2089 return [button for mask, button in BUTTON_MASK.items() if event.state & mask]
2091 def parse_key_event(self, event, pressed):
2092 keyval = event.keyval
2093 keycode = event.hardware_keycode
2094 keyname = Gdk.keyval_name(keyval)
2095 keyname = KEY_TRANSLATIONS.get((keyname, keyval, keycode), keyname)
2096 key_event = GTKKeyEvent()
2097 key_event.modifiers = self._client.mask_to_names(event.state)
2098 key_event.keyname = keyname or ""
2099 key_event.keyval = keyval or 0
2100 key_event.keycode = keycode
2101 key_event.group = event.group
2102 try:
2103 key_event.string = event.string or ""
2104 except UnicodeDecodeError as e:
2105 keylog("parse_key_event(%s, %s)", event, pressed, exc_info=True)
2106 if first_time("key-%s-%s" % (keycode, keyname)):
2107 keylog.warn("Warning: failed to parse string for key")
2108 keylog.warn(" keyname=%s, keycode=%s", keyname, keycode)
2109 keylog.warn(" %s", e)
2110 key_event.string = ""
2111 key_event.pressed = pressed
2112 keyeventlog("parse_key_event(%s, %s)=%s", event, pressed, key_event)
2113 return key_event
2115 def do_key_press_event(self, event):
2116 key_event = self.parse_key_event(event, True)
2117 if self.moveresize_event and key_event.keyname in BREAK_MOVERESIZE:
2118 #cancel move resize if there is one:
2119 self.moveresize_event = None
2120 self.cancel_moveresize_timer()
2121 return self._client.handle_key_action(self, key_event)
2123 def do_key_release_event(self, event):
2124 key_event = self.parse_key_event(event, False)
2125 return self._client.handle_key_action(self, key_event)
2128 def _do_scroll_event(self, event):
2129 if self._client.readonly:
2130 return
2131 button_mapping = GDK_SCROLL_MAP.get(event.direction, -1)
2132 mouselog("do_scroll_event device=%s, direction=%s, button_mapping=%s",
2133 self._device_info(event), event.direction, button_mapping)
2134 if button_mapping>=0:
2135 self._button_action(button_mapping, event, True)
2136 self._button_action(button_mapping, event, False)
2139 def update_icon(self, img):
2140 self._current_icon = img
2141 has_alpha = img.mode=="RGBA"
2142 width, height = img.size
2143 rowstride = width * (3+int(has_alpha))
2144 pixbuf = get_pixbuf_from_data(img.tobytes(), has_alpha, width, height, rowstride)
2145 iconlog("%s.set_icon(%s)", self, pixbuf)
2146 self.set_icon(pixbuf)