/
workspace.py
2272 lines (1831 loc) · 84.8 KB
/
workspace.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This file is part of MyPaint.
# Copyright (C) 2013 by Andrew Chadwick <a.t.chadwick@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
"""Workspaces with a central canvas, sidebars and saved layouts"""
## Imports
import os
from warnings import warn
import math
import logging
logger = logging.getLogger(__name__)
from gettext import gettext as _
import cairo
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from lib.observable import event
from lib.helpers import escape
import objfactory
from widgets import borderless_button
## Tool widget size constants
# Tool widgets should use GTK3-style sizing, and the lollowing layout
# constants.
#: Minimum width for a "sidebar-dockable" tool widget.
TOOL_WIDGET_MIN_WIDTH = 220
#: Minimum height for a "sidebar-dockable" tool widget.
TOOL_WIDGET_MIN_HEIGHT = 25
# Tool widgets should declare natural heights that result in nice ratios: not
# too short and not too tall. The GNOME HIG recommends that the longer
# dimension of a window not be more than 50% longer than the shorter dimension.
# The layout code will respect widgets' natural sizes vertically. For the look
# of the UI as a whole, it's best to use one of the sizing constants below for
# the natural height in most cases.
#: Natural height for shorter tool widgets
TOOL_WIDGET_NATURAL_HEIGHT_SHORT = TOOL_WIDGET_MIN_WIDTH
#: Natural height for taller tool widget
TOOL_WIDGET_NATURAL_HEIGHT_TALL = 1.25 * TOOL_WIDGET_MIN_WIDTH
## Class defs
class Workspace (Gtk.VBox, Gtk.Buildable):
"""Widget housing a central canvas flanked by two sidebar toolstacks
Workspaces also manage zero or more floating ToolStacks, and can set the
initial size and position of their own toplevel window.
Instances of tool widget classes can be constructed, then shown and hdden
by the workspace programatically using their GType name and an optional
sequence of construction parameters as a key. They should support the
following Python properties:
* ``tool_widget_icon_name``: the name of the icon to use.
* ``tool_widget_title``: the title to display in the tooltip, and in
floating window titles.
* ``tool_widget_description``: the description string to show in the
tooltip.
and the following methods:
* ``tool_widget_properties()``: show the properties dialog.
* ``tool_widget_get_icon_pixbuf(size)``: returns a pixbuf icon for a
particular pixel size. This is used in preference to the icon name.
Defaults will be used if these properties and methods aren't defined, but
the defaults are unlikely to be useful.
The entire layout of a Workspace, including the toplevel window of the
Workspace itself, can be dumped to and built from structures containing
only simple Python types. Widgets can be hidden by the user by clicking on
a tab group close button within a ToolStack widget, moved around between
stacks, or snapped out of stacks into new floating windows.
Workspaces observe their toplevel window, and automatically hide their
sidebars and floating windows in fullscreen. Auto-hidden elements are
revealed temporarily when the pointer moves near them. Autohide can be
toggled off or on, and the setting is retained in the layout definition.
Workspaces can also manage the visibility of a header and a footer bar
widget in the same manner: these bars are assumed to be packed above or
below the Workspace respectively.
"""
## Class vars
#: How near the pointer needs to be to a window edge or a hidden window to
#: automatically reveal it when autohide is enabled in fullscreen.
AUTOHIDE_REVEAL_BORDER = 12
#: Time in milliseconds to wait before hiding UI elements when autohide is
#: enabled in fullscreen.
AUTOHIDE_TIMEOUT = 800
#: Mask for all buttons.
#: Used to prevent autohide reveals when a pointer button is pressed
#: down. Prevents a possible source of distraction when the user is
#: drawing.
_ALL_BUTTONS_MASK = (
Gdk.ModifierType.BUTTON1_MASK | Gdk.ModifierType.BUTTON2_MASK |
Gdk.ModifierType.BUTTON3_MASK | Gdk.ModifierType.BUTTON4_MASK |
Gdk.ModifierType.BUTTON5_MASK )
# Edges the pointer can bump: used for autohide reveals
_EDGE_NONE = 0x00
_EDGE_LEFT = 0x01
_EDGE_RIGHT = 0x02
_EDGE_TOP = 0x04
_EDGE_BOTTOM = 0x08
#: Set keep-above in fullscreen mode.
#: Experimental hack to work around some annoying WM issues (Unity, Xfce)
_FULLSCREEN_KEEP_ABOVE_HACK = True
## GObject integration (type name, properties)
__gtype_name__ = 'MyPaintWorkspace'
#: Title suffix property for floating windows.
floating_window_title_suffix = GObject.property(
type=str, flags=GObject.PARAM_READWRITE,
nick='Floating window title suffix',
blurb='The suffix to append to floating windows: typically a '
'hyphen followed by the application name.',
default=None)
#: Title separator property for floating windows.
floating_window_title_separator = GObject.property(
type=str, flags=GObject.PARAM_READWRITE,
nick='Floating window title separator',
blurb='String used to separate the names of tools in a '
'floating window. By default, a comma is used.',
default=", ")
#: Header bar widget, to be hidden when entering fullscreen mode. This
#: widget should be packed externally to the workspace, and to its top.
header_bar = GObject.property(
type=Gtk.Widget, flags=GObject.PARAM_READWRITE,
nick='Header bar widget',
blurb="External Menubar/toolbar widget to be hidden when "
"entering fullscreen mode, and re-shown when leaving "
"it. The pointer position is also used for reveals and "
"hides in fullscreen.",
default=None)
#: Footer bar widget, to be hidden when entering fullscreen mode. This
#: widget should be packed externally to the workspace, and to its bottom.
footer_bar = GObject.property(
type=Gtk.Widget, flags=GObject.PARAM_READWRITE,
nick='Footer bar widget',
blurb="External footer bar widget to be hidden when entering "
"fullscreen mode, and re-shown when leaving it. The "
"pointer position is also used for reveals and hides "
"in fullscreen.",
default=None)
def __init__(self):
"""Initializes, with a placeholder canvas widget and no tool widgets"""
Gtk.VBox.__init__(self)
# Sidebar stacks
self._lstack = lstack = ToolStack()
self._rstack = rstack = ToolStack()
lscrolls = Gtk.ScrolledWindow()
rscrolls = Gtk.ScrolledWindow()
self._lscrolls = lscrolls
self._rscrolls = rscrolls
lscrolls.add_with_viewport(lstack)
rscrolls.add_with_viewport(rstack)
lscrolls.set_shadow_type(Gtk.ShadowType.IN)
rscrolls.set_shadow_type(Gtk.ShadowType.IN)
lscrolls.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
rscrolls.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self._lpaned = lpaned = Gtk.HPaned()
self._rpaned = rpaned = Gtk.HPaned()
for stack, paned in [(lstack, lpaned), (rstack, rpaned)]:
stack.workspace = self
stack.connect("hide", self._sidebar_stack_hide_cb, paned)
# Canvas scrolls. The canvas isn't scrollable yet, but use the same
# class as the right and left sidebars so that the shadows match
# in all themes.
cscrolls = Gtk.ScrolledWindow()
cscrolls.set_shadow_type(Gtk.ShadowType.IN)
cscrolls.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER)
self._canvas_scrolls = cscrolls
# Sidebar packing
lpaned.pack1(lscrolls, resize=False, shrink=False)
lpaned.pack2(rpaned, resize=True, shrink=False)
rpaned.pack2(rscrolls, resize=False, shrink=False)
self.pack_start(lpaned, True, True, 0)
# Autohide
self._autohide_enabled = True
self._autohide_timeout = None
# Window tracking
self._floating = set()
self._toplevel_pos = dict()
self._save_toplevel_pos_timeout = None
self._is_fullscreen = False
self._is_maximized = False
self._fs_event_handlers = []
# Initial layout happens in several phases
self._initial_layout = None
self.connect("realize", self._realize_cb)
self.connect("map", self._map_cb)
# Tool widget cache and factory
self._tool_widgets = objfactory.ObjFactory(gtype=Gtk.Widget)
self._tool_widgets.object_rebadged += self._tool_widget_rebadged
## GtkBuildable implementation (pre-realize)
def do_add_child(self, builder, child, type_):
"""Adds a child as the canvas: gtk_buildable_add_child() impl."""
self.set_canvas(child)
## Setup from layout descriptions (pre-realize)
def build_from_layout(self, layout):
"""Builds the workspace from a definition dict.
:param layout: a layout definition
:type layout: dict
In order to have any effect, this must be called before the workspace
widget is realized, but after it has been packed into its toplevel
window. Keys and values in the dict are as follows:
* position: an initial window position dict for the toplevel
window. See `set_initial_window_position()`.
* left_sidebar, right_sidebar: `ToolStack` definition lists.
See `Toolstack.build_from_layout()`.
* floating: a list of floating window definitions. Each element
is a dict with the following keys:
- contents: a `ToolStack` definition dict: see above.
- position: an initial window position dict: see above.
* autohide: whether autohide is enabled when fullscreening.
* fullsceen: whether to start in fullscreen mode.
* maximized: whether to start maximized.
See also `get_layout()`.
"""
toplevel_win = self.get_toplevel()
assert toplevel_win is not None
assert toplevel_win is not self
assert not toplevel_win.get_visible()
# Set initial position and fullscreen state
toplevel_pos = layout.get("position", None)
if toplevel_pos:
set_initial_window_position(toplevel_win, toplevel_pos)
if layout.get("fullscreen", False):
toplevel_win.fullscreen()
GObject.idle_add(lambda *a: toplevel_win.fullscreen())
elif layout.get("maximized", False):
toplevel_win.maximize()
GObject.idle_add(lambda *a: toplevel_win.maximize())
toplevel_win.connect("window-state-event",
self._toplevel_window_state_event_cb)
self.autohide_enabled = layout.get("autohide", True)
self._initial_layout = layout
def get_layout(self):
"""Returns a layout definition dict for the workspace
This should be called before the toplevel window is fully destroyed,
or the dicts representing the tool stacks will be empty.
"""
llayout = self._lstack.get_layout()
rlayout = self._rstack.get_layout()
float_layouts = [w.get_layout() for w in self._floating]
return dict(left_sidebar=llayout, right_sidebar=rlayout,
floating=float_layouts, position=self._toplevel_pos,
autohide=self._autohide_enabled,
fullscreen=self._is_fullscreen,
maximized=self._is_maximized)
## Initial layout (pre/post-realize)
def _realize_cb(self, widget):
"""Kick off the deferred layout code when the widget is realized"""
# Set up monitoring of the toplevel's size changes.
toplevel = self.get_toplevel()
toplevel.connect("configure-event", self._toplevel_configure_cb)
# Do the initial layout
layout = self._initial_layout
if layout is None:
return
llayout = layout.get("left_sidebar", {})
rlayout = layout.get("right_sidebar", {})
self._lstack.build_from_layout(llayout)
self._rstack.build_from_layout(rlayout)
# Floating windows
for flayout in layout.get("floating", []):
win = ToolStackWindow()
self.floating_window_created(win)
win.stack.workspace = self
win.build_from_layout(flayout)
self._floating.add(win)
# Reveal floating windows only after floating_window_created handlers
# have had a chance to run.
for win in self._floating:
GObject.idle_add(win.show_all)
def _map_cb(self, widget):
assert self.get_realized()
logger.debug("Completing layout (mapped)")
GObject.idle_add(self._complete_initial_layout)
def _complete_initial_layout(self):
"""Finish initial layout; called after toplevel win is positioned"""
# Restore saved widths for the sidebar
layout = self._initial_layout
if layout is not None:
left_width = layout.get("left_sidebar", {}).get("w", None)
if left_width is not None:
self.set_left_sidebar_width(left_width)
right_width = layout.get("right_sidebar", {}).get("w", None)
if right_width is not None:
self.set_right_sidebar_width(right_width)
# Sidebar stacks are initially shown, but their contents are not
# because building from layout was deferred. Hide empties and issue
# show_all()s.
if self._lstack.is_empty():
self._lstack.hide()
else:
self._lstack.show_all()
if self._rstack.is_empty():
self._rstack.hide()
else:
self._rstack.show_all()
# Toolstacks are responsible for their groups' positions
for stack in self._get_tool_stacks():
stack._complete_initial_layout()
## Canvas widget
def set_canvas(self, widget):
"""Canvas widget (setter)"""
assert self.get_canvas() is None
self._rpaned.pack1(widget, resize=True, shrink=False)
self._update_canvas_scrolledwindow()
def get_canvas(self):
"""Canvas widget (getter)"""
widget = self._rpaned.get_child1()
if widget is self._canvas_scrolls:
widget = widget.get_child()
return widget
def _update_canvas_scrolledwindow(self):
"""Update whether the canvas has a surrounding ScrolledWindow
In fullscreen mode, the ScrolledWindow is removed from the widget
hierarchy so that the canvas widget can occupy the full size of the
screen. In nonfullscreen mode, the scrollers provide a pretty frame.
"""
canvas = self.get_canvas()
parent = canvas.get_parent()
if not self._is_fullscreen:
if parent is self._canvas_scrolls:
return
logger.debug("Adding GtkScrolledWindow around canvas")
assert parent is self._rpaned
self._rpaned.remove(canvas)
self._rpaned.pack1(self._canvas_scrolls, resize=True, shrink=False)
self._canvas_scrolls.add_with_viewport(canvas)
self._canvas_scrolls.show_all()
else:
if parent is self._rpaned:
return
logger.debug("Removing GtkScrolledWindow around canvas")
assert parent is self._canvas_scrolls
self._canvas_scrolls.remove(canvas)
self._rpaned.remove(self._canvas_scrolls)
self._rpaned.pack1(canvas, resize=True, shrink=False)
self._canvas_scrolls.hide()
## Tool widgets
def show_tool_widget(self, tool_gtypename, tool_params):
"""Shows a tool widget identified by GType name and construct params
:param tool_gtypename: GType system name for the new widget's class
:param tool_params: parameters for the class's Python constructor
The widget will be created if it doesn't already exist. It will be
added to the first stack available. Existing floating windows will be
favoured over the sidebars; if there are no stacks visible, a sidebar
will be made visible to receive the new widget.
"""
# Attempt to get the widget, potentially creating it here
try:
widget = self._tool_widgets.get(tool_gtypename, *tool_params)
except objfactory.ConstructError as ex:
logger.error("show_tool_widget: %s", ex.message)
return
# Inject it into a suitable ToolStack
stack = None
if widget.get_parent() is not None:
logger.debug("Existing %r is already visible", widget)
stack = widget.get_parent()
while stack and not isinstance(stack, ToolStack):
stack = stack.get_parent()
else:
logger.debug("Showing %r, which is currently hidden", widget)
maxpages = 1
added = False
stack = None
while maxpages < 100 and not added:
for stack in self._get_tool_stacks():
if stack.add_tool_widget(widget, maxnotebooks=3,
maxpages=maxpages):
added = True
break
maxpages += 1
if not added:
logger.error("Cant find space for %r in any stack", widget)
return
# Reveal the widget's ToolStack
assert stack and isinstance(stack, ToolStack)
stack.reveal_tool_widget(widget)
def hide_tool_widget(self, tool_gtypename, tool_params):
"""Hides a tool widget by typename+params
This hides the widget and orphans it from the widget hierarchy, but a
reference to it is kept in the Workspace's internal cache. Further
calls to show_tool_widget() will use the cached object.
:param tool_gtypename: GType system name for the widget's class
:param tool_params: construct params further identifying the widget
:returns: whether the widget was found and hidden
:rtype: bool
"""
# First, does it even exist?
if not self._tool_widgets.cache_has(tool_gtypename, *tool_params):
return False
# Can't hide anything that's already hidden
widget = self._tool_widgets.get(tool_gtypename, *tool_params)
if widget.get_parent() is None:
return False
# The widget should exist in a known stack; find and remove
for stack in self._get_tool_stacks():
if stack.remove_tool_widget(widget):
return True
# Should never haappen...
warn("Asked to hide a visible widget, but it wasn't in any stack",
RuntimeWarning)
return False
def get_tool_widget_showing(self, gtype_name, params):
"""Returns whether a tool widget is currently parented and showing"""
# Nonexistent objects are not parented or showing
if not self._tool_widgets.cache_has(gtype_name, *params):
return False
# Otherwise, just test whether it's in a widget tree
widget = self._tool_widgets.get(gtype_name, *params)
return widget.get_parent() is not None
def update_tool_widget_params(self, tool_gtypename,
old_params, new_params):
"""Update the construction params of a tool widget
:param tool_gtypename: GType system name for the widget's class
:param old_params: old parameters for the class's Python constructor
:param new_params: new parameters for the class's Python constructor
If an object has changed so that its construction parameters must be
update, this method should be called to keep track of its identity
within the workspace. This method will not show or hide it: its
current state remains the same.
See also `update_tool_widget_ui()`.
"""
# If it doesn't exist yet, updating what is effectively a cache key used
# for accesing it makes no sense.
if not self._tool_widgets.cache_has(tool_gtypename, *old_params):
return
# Update the params of an existing object.
widget = self._tool_widgets.get(tool_gtypename, *old_params)
if old_params == new_params:
logger.devug("No construct params update needed for %r", widget)
else:
logger.debug("Updating construct params for %r", widget)
self._tool_widgets.rebadge(widget, new_params)
def update_tool_widget_ui(self, gtype_name, params):
"""Updates tooltips and tab labels for a specified tool widget
Calling this method causes the workspace to re-read the tool widget's
tab label, window title, and tooltip properties, and update the
display. Use it when things like the icon pixbuf have changed. It's
not necessary to call this after `update_tool_widget_params()` has been
used: that's handled with an event.
"""
if not self.get_tool_widget_showing(gtype_name, params):
return
widget = self._tool_widgets.get(gtype_name, *params)
logger.debug("Updating workspace UI widgets for %r", widget)
self._update_tool_widget_ui(widget)
## Tool widget events
@event
def tool_widget_shown(self, widget):
"""Event: tool widget shown"""
@event
def tool_widget_hidden(self, widget):
"""Event: tool widget hidden, either by the user or programatically"""
@event
def floating_window_created(self, toplevel):
"""Event: a floating window was created to house a toolstack."""
@event
def floating_window_destroyed(self, toplevel):
"""Event: a floating window was just `destroy()`ed."""
## Sidebar toolstack width
def set_right_sidebar_width(self, width):
"""Sets the width of the right sidebar toolstack
"""
if self._rstack.is_empty():
return
width = max(width, 100)
handle_size = GObject.Value()
handle_size.init(int)
self._rpaned.style_get_property("handle-size", handle_size)
position = self._rpaned.get_allocated_width()
position -= width
position -= handle_size.get_int()
self._rpaned.set_position(position)
def set_left_sidebar_width(self, width):
"""Sets the width of the left sidebar toolstack
"""
if self._lstack.is_empty():
return
width = max(width, 100)
self._lpaned.set_position(width)
## Position saving (toplevel window)
def _toplevel_configure_cb(self, toplevel, event):
"""Record the toplevel window's position ("configure-event" callback)
"""
# Avoid saving fullscreen positions. The timeout is a bit of hack, but
# it's necessary because the state change event and the configure event
# when fullscreening don't have a sensible order.
w, h = event.width, event.height
srcid = self._save_toplevel_pos_timeout
if srcid:
GObject.source_remove(srcid)
srcid = GObject.timeout_add(250, self._save_toplevel_pos_timeout_cb,
w, h)
self._save_toplevel_pos_timeout = srcid
def _save_toplevel_pos_timeout_cb(self, w, h):
"""Toplevel window position recording (post-"configure-event" oneshot)
Saves the (x,y) from the frame and the (w,h) from the configure event:
the combination can be used as an initial size and position next time.
"""
self._save_toplevel_pos_timeout = None
if self._is_fullscreen or self._is_maximized:
return False
toplevel = self.get_toplevel()
gdk_win = toplevel.get_window()
extents = gdk_win.get_frame_extents()
x = max(0, extents.x)
y = max(0, extents.y)
pos = dict(x=x, y=y, w=w, h=h)
self._toplevel_pos = pos
return False
## Toolstack order for searching, tool insertion etc.
def _get_tool_stacks(self):
"""Yields all known ToolStacks, in floating-first order.
"""
for win in self._floating:
yield win.stack
yield self._rstack
yield self._lstack
## Tool widget tab dragging (event callbacks)
def _tool_tab_drag_begin_cb(self):
"""Shows all possible drag targets at the start of a tool tab drag
Ensures that all the known toolstacks are visible to receive the drag,
even those which are empty or hidden due to fullscreening. Called by
stack notebooks in this workspace when tab drags start.
"""
# First cancel any pending hides.
if self._is_fullscreen:
self._cancel_autohide_timeout()
# Ensure the left and right stacks are visible at the beginning of
# a tab drag even if empty so that the user can drop a tab there.
for stack in (self._lstack, self._rstack):
scrolls = stack.get_parent().get_parent()
empty = stack.is_empty()
visible = stack.get_visible() and scrolls.get_visible()
if (empty and not visible) or (not empty and not visible):
scrolls = stack.get_parent().get_parent()
scrolls.show_all()
def _tool_tab_drag_end_cb(self):
"""Hides empty toolstacks at the end of a tool tab drag
Called by stack notebooks in this workspace when tab drags finish.
"""
for stack in (self._lstack, self._rstack):
scrolls = stack.get_parent().get_parent()
empty = stack.is_empty()
visible = stack.get_visible() and scrolls.get_visible()
if empty and visible:
stack.hide()
def _sidebar_stack_hide_cb(self, stack, paned):
"""Resets sidebar sizes when they're emptied (sidebar "hide" callback)
If the hide is due to the sidebar stack having been emptied out,
resetting the size means that it'll show at its placeholder's size when
it's next shown by the drag-start handler: this makes a narrower but
less intrusive target for the drag.
"""
if stack.is_empty():
paned.set_position(-1)
# Hide the parent GtkScrolledWindow too, allowing the paned to not
# show the sidebar pane at all - GtkScrolledWindows have an allocation
# even if they're empty, assuming they have scrollbars.
scrolls = stack.get_parent().get_parent()
scrolls.hide()
## Fullscreen (event callbacks)
def _toplevel_window_state_event_cb(self, toplevel, event):
"""Handle transitions between fullscreen and windowed."""
if event.changed_mask & Gdk.WindowState.FULLSCREEN:
fullscreen = event.new_window_state & Gdk.WindowState.FULLSCREEN
if fullscreen:
if self._FULLSCREEN_KEEP_ABOVE_HACK:
toplevel.set_keep_above(True)
# Setting keep-above in fullscreen mode results in fewer
# interruptions from the DE (in Xfce4.10, anyway). Floating
# windows and dialogs all use set_transient_for(), and will
# hopefully work.
if self.autohide_enabled:
self._connect_autohide_events()
self._start_autohide_timeout()
# Showing the floating windows makes an initial fullscreen
# look a little nicer.
for floating in self._floating:
floating.show_all()
else:
if self._FULLSCREEN_KEEP_ABOVE_HACK:
toplevel.set_keep_above(False)
self._disconnect_autohide_events()
self._show_autohide_widgets()
self._is_fullscreen = bool(fullscreen)
self._update_canvas_scrolledwindow()
if event.changed_mask & Gdk.WindowState.MAXIMIZED:
maximized = event.new_window_state & Gdk.WindowState.MAXIMIZED
self._is_maximized = bool(maximized)
## Autohide flag
def get_autohide_enabled(self):
"""Auto-hide is enabled in fullscreen (getter)"""
return self._autohide_enabled
def set_autohide_enabled(self, autohide_enabled):
"""Auto-hide is enabled in fullscreen (setter)"""
if self._is_fullscreen:
if autohide_enabled:
self._connect_autohide_events()
self._hide_autohide_widgets()
else:
self._disconnect_autohide_events()
self._show_autohide_widgets()
self._autohide_enabled = bool(autohide_enabled)
autohide_enabled = property(get_autohide_enabled, set_autohide_enabled)
def _hide_autohide_widgets(self):
"""Hides all auto-hiding widgets immediately"""
if not self._is_fullscreen:
return
self._cancel_autohide_timeout()
display = self.get_display()
if display.pointer_is_grabbed():
logger.warning("Pointer grabbed: not auto-hiding")
return
ah_widgets = self._get_autohide_widgets()
logger.debug("Hiding %d autohide widget(s)", len(ah_widgets))
for widget in ah_widgets:
if widget.get_visible():
widget.hide()
if self._FULLSCREEN_KEEP_ABOVE_HACK:
toplevel = self.get_toplevel()
toplevel.set_keep_above(False)
GObject.idle_add(toplevel.present)
GObject.idle_add(toplevel.set_keep_above, True)
def _show_autohide_widgets(self):
"""Shows all auto-hiding widgets immediately"""
self._cancel_autohide_timeout()
ah_widgets = self._get_autohide_widgets()
logger.debug("Hiding %d autohide widget(s)", len(ah_widgets))
for widget in ah_widgets:
widget.show_all()
def _get_autohide_widgets(self):
"""List of autohide widgets
Returns a list of the widgets which should be revealed by edge bumping,
rollovers, or hidden by the timeout.
"""
widgets = []
for stack in [self._lstack, self._rstack]:
if not stack.is_empty():
scrolls = stack.get_parent().get_parent()
widgets.append(scrolls)
widgets.extend(list(self._floating))
for bar in [self.header_bar, self.footer_bar]:
if bar:
widgets.append(bar)
return widgets
## Autohide mode: auto-hide timer
def _start_autohide_timeout(self):
"""Start a timer to hide the UI after a brief period of inactivity"""
if not self._autohide_timeout:
logger.debug("Starting autohide timeout (%d milliseconds)",
self.AUTOHIDE_TIMEOUT)
else:
self._cancel_autohide_timeout()
srcid = GObject.timeout_add(self.AUTOHIDE_TIMEOUT,
self._autohide_timeout_cb)
self._autohide_timeout = srcid
def _cancel_autohide_timeout(self):
"""Cancels any pending auto-hide"""
if not self._autohide_timeout:
return
GObject.source_remove(self._autohide_timeout)
self._autohide_timeout = None
def _autohide_timeout_cb(self):
"""Hide auto-hide widgets when the auto-hide timer finishes"""
self._hide_autohide_widgets()
return False
## Autohide mode: event handling on the canvas widget
def _connect_autohide_events(self):
"""Start listening for autohide events"""
if self._fs_event_handlers:
return
evwidget = self.get_canvas()
if not evwidget:
return
mask = (Gdk.EventMask.POINTER_MOTION_HINT_MASK |
Gdk.EventMask.POINTER_MOTION_MASK |
Gdk.EventMask.LEAVE_NOTIFY_MASK |
Gdk.EventMask.ENTER_NOTIFY_MASK )
evwidget.add_events(mask)
handlers = [("motion-notify-event", self._fs_motion_cb),
("leave-notify-event", self._fs_leave_cb),
("enter-notify-event", self._fs_enter_cb)]
for event_name, handler_callback in handlers:
handler_id = evwidget.connect(event_name, handler_callback)
self._fs_event_handlers.append((evwidget, handler_id))
def _disconnect_autohide_events(self):
"""Stop listening for autohide events"""
for evwidget, handler_id in self._fs_event_handlers:
evwidget.disconnect(handler_id)
self._fs_event_handlers = []
def _fs_leave_cb(self, widget, event):
"""Handles leaving the canvas in fullscreen"""
assert self._is_fullscreen
# if event.state & self._ALL_BUTTONS_MASK:
# # "Starting painting", except not quite.
# # Can't use this: there's no way of distinguishing it from
# # resizing a floating window. Hiding the window being resized
# # breaks window management badly! (Xfce 4.10)
# self._cancel_autohide_timeout()
# self._hide_autohide_widgets()
if event.mode == Gdk.CrossingMode.UNGRAB:
# Finished painting. To appear more consistent with a mouse,
# restart the hide timer now rather than waiting for a motion
# event.
self._start_autohide_timeout()
elif event.mode == Gdk.CrossingMode.NORMAL:
# User may be using a sidebar. Leave it open.
self._cancel_autohide_timeout()
return False
def _fs_enter_cb(self, widget, event):
"""Handles entering the canvas in fullscreen"""
assert self._is_fullscreen
# If we're safely in the middle, the autohide timer can begin now.
if not self._get_bumped_edges(widget, event):
self._start_autohide_timeout()
return False
def _fs_motion_cb(self, widget, event):
"""Handles edge bumping and other rollovers in fullscreen mode"""
assert self._is_fullscreen
# Firstly, if the user appears to be drawing, be as stable as we can.
if event.state & self._ALL_BUTTONS_MASK:
self._cancel_autohide_timeout()
return False
# Floating window rollovers
show_floating = False
for win in self._floating:
if win.get_visible():
continue
x, y = event.x_root, event.y_root
b = self.AUTOHIDE_REVEAL_BORDER
if win.contains_point(x, y, b=b):
show_floating = True
if show_floating:
for win in self._floating:
win.show_all()
self._cancel_autohide_timeout()
return False
# Edge bumping
# Bump the mouse into the edge of the screen to get back the stuff
# that was hidden there, similar to media players etc.
edges = self._get_bumped_edges(widget, event)
if not edges:
self._start_autohide_timeout()
return False
if edges & self._EDGE_TOP and self.header_bar:
self.header_bar.show_all()
if edges & self._EDGE_BOTTOM and self.footer_bar:
self.footer_bar.show_all()
if edges & self._EDGE_LEFT and not self._lstack.is_empty():
self._lscrolls.show_all()
if edges & self._EDGE_RIGHT and not self._rstack.is_empty():
self._rscrolls.show_all()
@classmethod
def _get_bumped_edges(cls, widget, event):
# Returns a bitmask of the edges bumped by the pointer.
alloc = widget.get_allocation()
w, h = alloc.width, alloc.height
x, y = event.x, event.y
b = cls.AUTOHIDE_REVEAL_BORDER
if not (x < b or x > w-b or y < b or y > h-b):
return cls._EDGE_NONE
edges = cls._EDGE_NONE
if y < b or (y < 5*b and (x < b or x > w-b)):
edges |= cls._EDGE_TOP
if y > h - b:
edges |= cls._EDGE_BOTTOM
if x < b:
edges |= cls._EDGE_LEFT
if x > w - b:
edges |= cls._EDGE_RIGHT
return edges
## Tool widget tab & title updates
def _tool_widget_rebadged(self, factory, product, old_params, new_params):
"""Internal: update UI elements when the ID of a tool widget changes
For parameterized ones like the brush group tool widget, the tooltip
and titlebar are dependent on the identity strings and must be update
when the tab is renamed.
"""
self._update_tool_widget_ui(product)
def _update_tool_widget_ui(self, widget):
"""Internal: update UI elements for a known descendent tool widget"""
page = widget.get_parent()
notebook = page.get_parent()
notebook.update_tool_widget_ui(widget)
class ToolStack (Gtk.EventBox):
"""Vertical stack of tool widget groups
The layout has movable dividers between groups of tool widgets, and an
empty group on the end which accepts tabs dragged to it. The groups are
implmented as `Gtk.Notebook`s, but that interface is not exposed.
ToolStacks are built up from layout definitions represented by simple
types: see `Workspace` and `build_from_layout()` for details.
"""
## Behavioural constants
RESIZE_STICKINESS = 20
## GObject integration (type name, properties)
__gtype_name__ = 'MyPaintToolStack'