/
widget.py
3260 lines (2659 loc) · 111 KB
/
widget.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
"""
pygame-menu
https://github.com/ppizarror/pygame-menu
WIDGET
Base class for widgets.
"""
__all__ = [
# Main class
'AbstractWidgetManager',
'Widget',
# Utils
'check_widget_mouseleave',
# Types
'WidgetBorderPositionType',
# Global widget mouseover list
'WIDGET_MOUSEOVER',
# Others
'WIDGET_BORDER_POSITION_FULL',
'WIDGET_BORDER_POSITION_NONE',
'WIDGET_FULL_BORDER',
'WIDGET_SHADOW_TYPE_ELLIPSE',
'WIDGET_SHADOW_TYPE_RECTANGULAR',
'WidgetTransformationNotImplemented'
]
import random
import time
import pygame
import pygame_menu
from pygame_menu._base import Base
from pygame_menu._decorator import Decorator
from pygame_menu.controls import Controller
from pygame_menu.font import FontType
from pygame_menu.locals import POSITION_NORTHWEST, POSITION_SOUTHWEST, POSITION_WEST, \
POSITION_EAST, POSITION_NORTHEAST, POSITION_CENTER, POSITION_NORTH, POSITION_SOUTH, \
POSITION_SOUTHEAST, ALIGN_CENTER
from pygame_menu.sound import Sound
from pygame_menu.utils import make_surface, assert_alignment, assert_color, \
assert_position, assert_vector, parse_padding, uuid4, \
mouse_motion_current_mouse_position, PYGAME_V2, set_pygame_cursor, warn, \
get_cursor, ShadowGenerator
from pygame_menu.widgets.core.selection import Selection
from pygame_menu._types import Optional, ColorType, Tuple2IntType, NumberType, \
PaddingType, Union, List, Tuple, Any, CallbackType, Dict, Callable, Tuple4IntType, \
Tuple2BoolType, Tuple3IntType, NumberInstance, ColorInputType, EventType, \
EventVectorType, EventListType, CursorInputType, CursorType, VectorInstance, \
Tuple2NumberType, CallableNoArgsType
# This list stores the current widget which requested the mouseover status, and
# the previous widget list which requested the mouseover. Each time the widget
# changes the over status, if leaves all previous widgets that are not hovered
# trigger mouseleave. The format of each item of the list is
# [..., [widget, previous_cursor, [previous_widget, previous_cursor2, [....]
WIDGET_MOUSEOVER: List[Any] = [None, []]
# Stores the top cursor for validation
WIDGET_TOP_CURSOR: List[Any] = [None]
WIDGET_TOP_CURSOR_WARNING = False
WIDGET_BORDER_POSITION_NONE = 'border-none'
WIDGET_BORDER_POSITION_FULL = 'border-position-border-full'
WIDGET_FULL_BORDER = (POSITION_NORTH, POSITION_SOUTH, POSITION_EAST, POSITION_WEST)
# Creates the shadow generator
WIDGET_SHADOW_GENERATOR = ShadowGenerator()
WIDGET_SHADOW_TYPE_ELLIPSE = 'ellipse'
WIDGET_SHADOW_TYPE_RECTANGULAR = 'rectangular'
def check_widget_mouseleave(event: Optional[EventType] = None, force: bool = False) -> None:
"""
Check if the active widget (WIDGET_MOUSEOVER[0]) is still over, else, execute
previous list (WIDGET_MOUSEOVER[1]).
:param event: Mouse motion event. If ``None`` this method creates the event
:param force: If ``True`` calls all mouse leave without checking if the mouse is still over
"""
return _check_widget_mouseleave(event, force)
# noinspection PyProtectedMember
def _check_widget_mouseleave(
event: Optional[EventType] = None,
force: bool = False,
recursive: bool = False
) -> None:
"""
Check if the active widget (WIDGET_MOUSEOVER[0]) is still over, else, execute
previous list (WIDGET_MOUSEOVER[1]).
:param event: Mouse motion event. If ``None`` this method creates the event
:param force: If ``True`` calls all mouse leave without checking if the mouse is still over
:param recursive: If ``True`` the call is recursive
"""
# If no widget is over, return
if WIDGET_MOUSEOVER[0] is None:
assert len(WIDGET_MOUSEOVER[1]) == 0, 'widget leave sublist must be empty'
assert WIDGET_TOP_CURSOR[0] is None, 'widget top cursor must be None'
return
if event is None:
event = mouse_motion_current_mouse_position()
# Check widget is still over
current: 'Widget' = WIDGET_MOUSEOVER[0]
current._check_mouseover(event, check_all_widget_mouseleave=False) # This may change WIDGET_MOUSEOVER
# If mouse is not visible, forces
if PYGAME_V2:
force = force or not pygame.mouse.get_visible()
# The active widget is not over
if (not current._mouseover or force) and WIDGET_MOUSEOVER[0] is not None:
assert len(WIDGET_MOUSEOVER[1]) == 3, 'invalid widget leave sublist length'
# Unpack list
prev_widget: 'Widget' = WIDGET_MOUSEOVER[1][0]
prev_cursor = WIDGET_MOUSEOVER[1][1]
prev_list: List[Any] = WIDGET_MOUSEOVER[1][2]
assert WIDGET_MOUSEOVER[0] == prev_widget, \
'inconsistent widget leave sublist'
# Set previous cursor
set_pygame_cursor(prev_cursor)
# Unpack list
if len(prev_list) == 0:
WIDGET_MOUSEOVER[0] = None
WIDGET_MOUSEOVER[1] = []
if prev_cursor != WIDGET_TOP_CURSOR[0]:
if WIDGET_TOP_CURSOR_WARNING and current._verbose:
warn(
f'expected {WIDGET_TOP_CURSOR[0]} to be the top cursor '
f'(WIDGET_TOP_CURSOR), but {prev_cursor} is the current '
f'previous cursor from WIDGET_MOUSEOVER recursive list. '
f'The top cursor {WIDGET_TOP_CURSOR[0]} will be established '
f'as the pygame default mouse cursor'
)
set_pygame_cursor(WIDGET_TOP_CURSOR[0])
WIDGET_TOP_CURSOR[0] = None
else:
assert len(prev_list) == 3, 'invalid widget leave sublist length'
WIDGET_MOUSEOVER[0] = prev_list[0]
WIDGET_MOUSEOVER[1] = prev_list
# Call leave
prev_widget.mouseleave(event, check_all_widget_mouseleave=False)
# Recursive call
_check_widget_mouseleave(event, force, recursive=True)
# Check sublist
if len(WIDGET_MOUSEOVER[1]) == 3 and len(WIDGET_MOUSEOVER[1][2]) > 0 and not recursive and not force:
prev: List[Any] = WIDGET_MOUSEOVER[1][2] # [widget, cursor, [widget, cursor, [...]]]
while True:
if len(prev) == 0:
break
widget: 'Widget' = prev[0]
cursor = prev[1]
# Check widget is still over
widget._check_mouseover(event, check_all_widget_mouseleave=False)
# If not active
if not widget._mouseover:
# Update the array
if len(prev[2]) == 3:
prev[0] = prev[2][0]
prev[1] = prev[2][1]
prev[2] = prev[2][2]
# Set previous cursor
set_pygame_cursor(cursor)
else:
for _ in range(len(prev)):
prev.pop()
break
else:
prev = prev[2] # Recursive call
# Types
BackgroundSurfaceType = Optional[List[Union['pygame.Rect', 'pygame.Surface', Optional[Union[ColorType, 'pygame_menu.BaseImage']]]]]
CallbackMouseType = Optional[Union[Callable[['Widget', EventType], Any], CallableNoArgsType]]
CallbackSelectType = Optional[Union[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any], CallableNoArgsType]]
WidgetBorderPositionType = Union[str, List[str], Tuple[str, ...]]
WidgetBorderType = Tuple[ColorType, int, WidgetBorderPositionType, Tuple2IntType]
WidgetShadowType = Dict[str, Union[Optional['pygame.Surface'], Optional['pygame.Rect'], bool, Tuple[str, int, int, int, Tuple3IntType]]]
# noinspection PyProtectedMember
class Widget(Base):
"""
Widget abstract class.
.. note::
Widget cannot be copied or deep-copied.
:param title: Widget title
:param widget_id: Widget identifier
:param onchange: Callback when updating the status of the widget, executed in :py:meth:`pygame_menu.widgets.core.widget.Widget.change`
:param onreturn: Callback when applying on the widget (return), executed in :py:meth:`pygame_menu.widgets.core.widget.Widget.apply`
:param onselect: Callback when selecting the widget, executed in :py:meth:`pygame_menu.widgets.core.widget.Widget.set_selected`
:param args: Optional arguments for callbacks
:param kwargs: Optional keyword arguments
"""
_accept_events: bool
_alignment: str
_angle: NumberType
_args: List[Any]
_background_color: Optional[Union[ColorType, 'pygame_menu.BaseImage']]
_background_inflate: Tuple2IntType
_background_surface: BackgroundSurfaceType
_border_color: ColorType
_border_inflate: Tuple2IntType
_border_position: WidgetBorderPositionType
_border_width: int
_check_mouseleave_call_render: bool
_col_row_index: Tuple3IntType
_ctrl: 'Controller'
_cursor: CursorType
_decorator: 'Decorator'
_default_value: Any
_draw_callbacks: Dict[str, Callable[['Widget', Optional['pygame_menu.Menu']], Any]]
_events: EventListType
_flip: Tuple2BoolType
_floating: bool
_floating_origin_position: bool
_font: Optional['pygame.font.Font']
_font_antialias: bool
_font_background_color: Optional[ColorType]
_font_color: ColorType
_font_name: FontType
_font_readonly_color: ColorType
_font_readonly_selected_color: ColorType
_font_selected_color: ColorType
_font_shadow: bool
_font_shadow_color: ColorType
_font_shadow_offset: NumberType
_font_shadow_position: str
_font_shadow_tuple: Tuple2IntType
_font_size: int
_frame: Optional['pygame_menu.widgets.Frame']
_joystick_enabled: bool
_keyboard_enabled: bool
_keyboard_ignore_nonphysical: bool
_kwargs: Dict[Any, Any]
_last_render_hash: int
_margin: Tuple2IntType
_max_height: List[Optional[bool]]
_max_width: List[Optional[bool]]
_menu: Optional['pygame_menu.Menu'] # Menu which contains the Widget
_menu_hook: Optional['pygame_menu.Menu'] # Menu the Widget points to (submenu)
_mouse_enabled: bool
_mouseleave_called: bool
_mouseover: bool # Check if mouse is over
_mouseover_called: Optional[bool] # Check if the mouseover/mouseleave callbacks were called
_mouseover_check_rect: Callable[[], 'pygame.Rect']
_onchange: CallbackType
_onmouseleave: CallbackMouseType
_onmouseover: CallbackMouseType
_onreturn: CallbackType
_onselect: CallbackSelectType
_padding: Tuple4IntType
_padding_transform: Tuple4IntType
_position: Tuple2IntType
_rect: 'pygame.Rect'
_rect_size_delta: Tuple2IntType
_scale: List[Union[bool, NumberType]]
_scrollarea: Optional['pygame_menu._scrollarea.ScrollArea'] # Parent scrollarea
_selected: bool
_selection_effect: 'Selection'
_selection_effect_draw_post: bool
_selection_time: NumberType
_shadow: WidgetShadowType
_sound: 'Sound'
_surface: Optional['pygame.Surface']
_tab_size: int
_title: str
_touchscreen_enabled: bool
_translate: Tuple2IntType # Translation made by user
_translate_virtual: Tuple2IntType # Virtual translation applied by api
_update_callbacks: Dict[str, Callable[[EventListType, 'Widget', 'pygame_menu.Menu'], Any]]
_visible: bool
active: bool
configured: bool
force_menu_draw_focus: bool
is_scrollable: bool
is_selectable: bool
last_surface: Optional['pygame.Surface']
lock_position: bool
readonly: bool
selection_expand_background: bool
def __init__(
self,
title: Any = '',
widget_id: str = '',
onchange: CallbackType = None,
onmouseleave: Optional[Callable[['Widget', EventType], Any]] = None,
onmouseover: Optional[Callable[['Widget', EventType], Any]] = None,
onreturn: CallbackType = None,
onselect: Optional[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any]] = None,
args=None,
kwargs=None
) -> None:
super(Widget, self).__init__(object_id=widget_id)
self._accept_events = False # Indicate the widget receives events (info)
self._alignment = ALIGN_CENTER # Widget alignment
self._background_color = None
self._background_inflate = (0, 0)
self._background_surface = None
self._check_mouseleave_call_render = False
self._col_row_index = (-1, -1, -1)
self._cursor = None
self._decorator = Decorator(self)
self._default_value = _WidgetNoValue()
self._events = []
self._frame = None
self._margin = (0, 0)
self._max_height = [None, False, True] # size, width_scale, smooth
self._max_width = [None, False, True] # size, height_scale, smooth
self._mouseleave_called = False
self._mouseover = False
self._mouseover_called = None
self._padding = (0, 0, 0, 0) # top, right, bottom, left
self._padding_transform = (0, 0, 0, 0)
self._position = (0, 0)
self._scrollarea = None # Widget scrollarea container
self._selected = False # Use select() to modify this status
self._selection_time = 0
self._sound = Sound()
self._tab_size = 0 # Tab spaces
self._title = str(title)
self._visible = True # Use show() or hide() to modify this status
# If True, the widget don't contribute width/height to the Menu widget
# positioning computation. Use .set_float() to modify this status
self._floating = False
self._floating_origin_position = False
# Which function is used to get the rect which checks if the widget is
# active or not
self._mouseover_check_rect = lambda: self.get_rect(to_real_position=True)
# Widget transforms
self._angle = 0 # Rotation angle (degrees)
self._flip = (False, False) # x, y
self._scale = [False, 1, 1, False] # do_scale, x, y, smooth
self._scale_factor = (1, 1) # Transformed/Original in x, y
self._translate = (0, 0)
self._translate_virtual = (0, 0) # Translation virtual used by scrollarea's
# Widget rect. This object does not contain padding. For getting the
# widget+padding use .get_rect() widget method instead. Widget subclass
# should ONLY modify width/height, in rendering and READ position (rect.x,
# rect.y) in drawing. Position during rendering is not the same as it will
# have in menu (menu rendering changes widget position). Some widgets like
# MenuBar are the exception, as its position never changes during menu
# execution (unless user triggers a change), then widgets like these may
# access without problems
self._rect = pygame.Rect(0, 0, 0, 0)
self._rect_size_delta = (0, 0) # Size added to rect width/height
# Callbacks
self._draw_callbacks = {}
self._update_callbacks = {}
self.set_onchange(onchange)
self.set_onmouseleave(onmouseleave)
self.set_onmouseover(onmouseover)
self.set_onreturn(onreturn)
self.set_onselect(onselect)
self._args = args or []
self._kwargs = kwargs or {}
# Surface of the widget
self._surface = None
# Menu reference
self._menu = None # Menu which contains the widget
self._menu_hook = None # Menu the widget points to. Modified by WidgetManager._add_submenu
# Modified in set_font() method
self._font = None
self._font_antialias = True
self._font_background_color = None
self._font_color = (0, 0, 0)
self._font_name = ''
self._font_readonly_color = (0, 0, 0)
self._font_readonly_selected_color = (255, 255, 255)
self._font_selected_color = (255, 255, 255)
self._font_size = 0
# Font shadow
self._font_shadow = False
self._font_shadow_color = (0, 0, 0)
self._font_shadow_offset = 2.0
self._font_shadow_position = POSITION_NORTHWEST
self._font_shadow_tuple = (0, 0) # (x px offset, y px offset)
# Widget shadow
self._shadow = {
'enabled': False,
'properties': (),
'rect': None,
'surface': None
}
# Border
self._border_color = (0, 0, 0)
self._border_inflate = (0, 0)
self._border_position = 'none'
self._border_width = 0
# Rendering, this variable may be used by render() method. If the hash
# of the variables change respect to the last render hash (hash computed
# using self._hash_variables() method) then the widget should render and
# update the hash
self._last_render_hash = 0
# Selection effect, for avoiding exception while getting object rect,
# NullSelection was created. Initially it was None
self._selection_effect = pygame_menu.widgets.NoneSelection()
# If False, the selection effect is drawn previous the widget surface
self._selection_effect_draw_post = True
# Inputs
self._ctrl = Controller()
self._keyboard_enabled = True
self._keyboard_ignore_nonphysical = True # Ignores non-physical keyboard buttons pressed
self._joystick_enabled = True
self._mouse_enabled = True # Accept mouse interaction
self._touchscreen_enabled = True
# Public statutes. These values can be changed without calling for
# methods (safe to update)
self.active = False # Widget requests focus if selected
self.configured = False # Widget has been configured
self.force_menu_draw_focus = False # If True Menu draw focus if widget is selected, don't consider the previous requisites
self.is_scrollable = False # Some widgets can be scrolled, such as the Frame
self.is_selectable = True # Some widgets cannot be selected like labels
self.last_surface = None # Stores the last surface the widget has been drawn
self.lock_position = False # If True, the widget don't update the position if .set_position() is executed
self.readonly = False # If True, widget ignores all input
self.receive_menu_update_events = True # If False, the widget does not receive events from Menu.update(events)
self.selection_expand_background = False # If True, the widget background will inflate to match selection margin if selected
def _ignores_keyboard_nonphysical(self) -> bool:
"""
Ignores the keyboard non-physical button events.
:return: True if ignored
"""
if self._menu is None:
return self._keyboard_ignore_nonphysical
return self._keyboard_ignore_nonphysical and self._menu._keyboard_ignore_nonphysical
def set_onchange(self, onchange: CallbackType) -> 'Widget':
"""
Set ``onchange`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.change` method. The
callback function receives the following arguments:
.. code-block:: python
onchange(value, *args, *widget._args, **widget._kwargs)
:param onchange: Callback executed if the Widget changes its value; it can be a function or None
:return: Self reference
"""
if onchange:
assert callable(onchange), \
'onchange must be callable (function-type) or None'
self._onchange = onchange
return self
def set_onreturn(self, onreturn: CallbackType) -> 'Widget':
"""
Set ``onreturn`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.apply` method. The
callback function receives the following arguments:
.. code-block:: python
onreturn(value, *args, *widget._args, **widget._kwargs)
:param onreturn: Callback executed if user applies on Widget; it can be a function or None
:return: Self reference
"""
if onreturn:
assert callable(onreturn), \
'onreturn must be callable (function-type) or None'
self._onreturn = onreturn
return self
def set_onselect(self, onselect: CallbackSelectType) -> 'Widget':
"""
Set ``onselect`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.select` method. The
callback function receives the following arguments:
.. code-block:: python
onselect(selected, widget, menu) <or> onselect()
:param onselect: Callback executed if user selects the Widget; it can be a function or None
:return: Self reference
"""
if onselect:
assert callable(onselect), \
'onselect must be callable (function-type) or None'
self._onselect = onselect
return self
def set_onmouseover(self, onmouseover: CallbackMouseType) -> 'Widget':
"""
Set ``onmouseover`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.mouseover` method. The
callback function receives the following arguments:
.. code-block:: python
onmouseover(widget, event) <or> onmouseover()
:param onmouseover: Callback executed if user enters the Widget with the mouse; it can be a function or None
:return: Self reference
"""
if onmouseover:
assert callable(onmouseover), \
'onmouseover must be callable (function-type) or None'
self._onmouseover = onmouseover
return self
def set_onmouseleave(self, onmouseleave: CallbackMouseType) -> 'Widget':
"""
Set ``onmouseleave`` callback. This method is executed in
:py:meth:`pygame_menu.widgets.core.widget.Widget.mouseleave` method. The
callback function receives the following arguments:
.. code-block:: python
onmouseleave(widget, event) <or> onmouseleave()
:param onmouseleave: Callback executed if user leaves the Widget with the mouse; it can be a function or None
:return: Self reference
"""
if onmouseleave:
assert callable(onmouseleave), \
'onmouseleave must be callable (function-type) or None'
self._onmouseleave = onmouseleave
return self
def mouseover(
self,
event: EventType,
check_all_widget_mouseleave: bool = True
) -> 'Widget':
"""
Run the ``onmouseover`` if the mouse is placed over the Widget. The
callback receive the Widget object reference and the mouse event:
.. code-block:: python
onmouseover(widget, event) <or> onmouseover()
.. warning::
This method does not evaluate if the mouse is placed over the Widget.
Only executes the callback and updates the cursor if enabled.
:param event: ``MOUSEMOVE`` pygame event
:param check_all_widget_mouseleave: Check widget leave statutes
:return: Self reference
"""
# Check if within frame, and the previous frame has not been called, call it
if check_all_widget_mouseleave:
if self._frame is not None and WIDGET_MOUSEOVER[0] != self._frame:
in_prev = False
# Check frame not in previous
prev = WIDGET_MOUSEOVER[1]
if len(prev) != 0:
while True:
if len(prev) == 0:
break
if prev[0] == self._frame:
in_prev = True
break
prev = prev[2]
if not in_prev:
self._frame.mouseover(event, check_all_widget_mouseleave)
if self._onmouseover is not None:
if self._mouseover_called is None or not self._mouseover_called:
try:
self._onmouseover(self, event)
except TypeError:
self._onmouseover()
self._mouseover_called = True
self._mouseleave_called = False
# Check previous state
if check_all_widget_mouseleave:
check_widget_mouseleave(event)
# Change cursor
previous_cursor = get_cursor() # Previous cursor
set_pygame_cursor(self._cursor)
# Update previous state
if check_all_widget_mouseleave:
if WIDGET_MOUSEOVER[0] is None:
WIDGET_TOP_CURSOR[0] = previous_cursor
WIDGET_MOUSEOVER[0] = self
WIDGET_MOUSEOVER[1] = [self, previous_cursor, WIDGET_MOUSEOVER[1]]
return self
def mouseleave(
self,
event: EventType,
check_all_widget_mouseleave: bool = True
) -> 'Widget':
"""
Run the ``onmouseleave`` callback if the mouse is placed outside the Widget.
The callback receive the Widget object reference and the mouse event:
.. code-block:: python
onmouseleave(widget, event) <or> onmouseleave()
.. warning::
This method does not evaluate if the mouse is placed over the Widget.
Only executes the callback and updates the cursor if enabled.
:param event: ``MOUSEMOVE`` pygame event
:param check_all_widget_mouseleave: Check widget leave statutes
:return: Self reference
"""
# Check for consistency
if WIDGET_MOUSEOVER[0] != self or not check_all_widget_mouseleave:
if self._onmouseleave is not None and self._mouseover_called or self._onmouseover is None:
if self._onmouseleave and not self._mouseleave_called:
try:
self._onmouseleave(self, event)
except TypeError:
self._onmouseleave()
self._mouseover_called = False
self._mouseleave_called = True
if check_all_widget_mouseleave:
check_widget_mouseleave(event)
return self
def _check_mouseover(
self,
event: EventType,
rect: Optional['pygame.Rect'] = None,
check_all_widget_mouseleave: bool = True
) -> bool:
"""
Check the mouse is over the widget. If so, execute the methods.
:param event: Mouse event (``MOUSEMOTION`` or ``ACTIVEEVENT``)
:param rect: Rect object. If ``None`` uses the widget rect in real position
:param check_all_widget_mouseleave: Check widget leave statutes
:return: ``True`` if the mouseover status changed
"""
if not hasattr(event, 'type') or event.type not in (pygame.MOUSEMOTION, pygame.ACTIVEEVENT):
return False
# If mouse out from window
if event.type == pygame.ACTIVEEVENT and hasattr(event, 'gain'):
if event.gain == 1:
return False
else: # Mouse out from window
check_widget_mouseleave(force=True)
return True
if rect is None:
rect = self._mouseover_check_rect()
updated = False
# Check if menu is active
menu_enabled = True if self._menu is None else self._menu.is_enabled()
# Check if mouse is over the widget, the widget must be visible
if (
self.is_visible() and
self._mouse_enabled and
hasattr(event, 'pos') and
rect.collidepoint(*event.pos) and
menu_enabled
):
if not self._mouseover:
self._mouseover = True
self.mouseover(event, check_all_widget_mouseleave)
updated = True
else:
if self._mouseover:
self._mouseover = False
self.mouseleave(event, check_all_widget_mouseleave)
updated = True
if updated and self._check_mouseleave_call_render:
self._render()
return updated
def set_cursor(self, cursor: CursorInputType) -> 'Widget':
"""
Set the Widget cursor if user places the mouse over the Widget.
:param cursor: Pygame cursor
:return: Self reference
"""
self._cursor = cursor
return self
def get_sound(self) -> 'Sound':
"""
Return the Widget sound engine.
:return: Sound API
"""
return self._sound
def is_selected(self) -> bool:
"""
Return ``True`` if the Widget is selected.
:return: Selected status
"""
return self._selected
def on_remove_from_menu(self) -> 'Widget':
"""
Function executed if the Widget is removed from the Menu.
:return: Self reference
"""
return self
def is_visible(self, check_frame: bool = True) -> bool:
"""
Return ``True`` if the Widget is visible.
:param check_frame: If ``True`` check frame and sub-frames if they're opened as well
:return: Visible status
"""
if not check_frame:
return self._visible
if not self._visible:
return False
frame = self._frame
if frame is not None:
while True:
if frame is None:
break
if not frame._visible:
return False
frame = frame._frame
return True
def is_floating(self) -> bool:
"""
Return ``True`` if the Widget is floating.
:return: Float status
"""
return self._floating
def __copy__(self) -> 'pygame_menu.Menu':
"""
Copy method.
:return: Raises copy exception
"""
raise _WidgetCopyException('Widget class cannot be copied')
def __deepcopy__(self, memodict: Dict) -> 'pygame_menu.Menu':
"""
Deep-copy method.
:param memodict: Memo dict
:return: Raises copy exception
"""
raise _WidgetCopyException('Widget class cannot be deep-copied')
def _force_render(self) -> Optional[bool]:
"""
Forces Widget render.
.. note::
If this method is used it's not necessary to call Widget methods
:py:meth:`pygame_menu.widgets.core.widget.Widget.force_menu_surface_update` and
:py:meth:`pygame_menu.widgets.core.widget.Widget.force_menu_surface_cache_update`.
As `render` should force Menu render, updating both surface and cache.
:return: Render return value
"""
self._last_render_hash = 0
return self._render()
def force_menu_surface_update(self) -> 'Widget':
"""
Forces menu surface update after next rendering call.
This method automatically updates widget decoration cache as Menu render
forces it to re-render.
This method also should be aclled by each widget after render.
.. note::
This method is expensive, as menu surface update forces re-rendering
of all widgets (because them can change in size, position, etc...).
:return: Self reference
"""
if self._menu is not None:
# Don't set _menu._widgets_surface to None because if so
# in the drawing process it may destroy the surface and raising
# an Error. The usage of _widgets_surface_need_update is only on
# Menu _render()
self._menu._widgets_surface_need_update = True
self._shadow['surface'] = None
return self
def force_menu_surface_cache_update(self) -> 'Widget':
"""
Forces menu surface cache to update after next drawing call. This also
updates widget decoration.
.. note::
This method only updates the surface cache, without forcing re-rendering
of all Menu widgets as
:py:meth:`pygame_menu.widgets.core.widget.Widget.force_menu_surface_update`
does.
:return: Self reference
"""
if self._menu is not None:
# Menu _widget_surface_cache_need_update property is only accessed on
# draw method. This does not set _menu._widgets_surface to None
self._menu._widget_surface_cache_need_update = True
self._decorator.force_cache_update()
return self
def render(self) -> Optional[bool]:
"""
Public rendering method.
.. note::
Unlike private ``_render`` method, public method forces widget rendering
(calling :py:meth:`pygame_menu.widgets.core.widget.Widget._force_render`).
Use this method only if the widget has changed the state. Running this
function many times may affect the performance.
.. note::
Before rendering, check out if the widget font/title/values are
set. If not, it is probable that a zero-size surface is set.
:return: ``True`` if widget has rendered a new state, ``None`` if the widget has not changed, so render used a cache
"""
return self._force_render()
def _render(self) -> Optional[bool]:
"""
Render the Widget surface.
This method shall update the attribute ``_surface`` with a :py:class:`pygame.Surface`
object representing the outer borders of the widget.
.. note::
Before rendering, check out if the widget font/title/values are
set. If not, it is probable that a zero-size surface is set.
.. note::
Render methods should call
:py:meth:`pygame_menu.widgets.core.widget.Widget.force_menu_surface_update`
to force Menu to update the drawing surface.
:return: ``True`` if widget has rendered a new state, ``None`` if the widget has not changed, so render used a cache
"""
raise NotImplementedError('override is mandatory')
@staticmethod
def _hash_variables(*args) -> int:
"""
Compute hash from a series of variables.
:param args: Variables to compute hash
:return: Hash data
"""
h = hash(args)
if h == 0: # Menu considers 0 as un-rendered status
h = random.randrange(-100000, 100000)
return h
def _render_hash_changed(self, *args) -> bool:
"""
This method checks if the widget must render because the inner variables
changed. This method should include all the variables used by the render
method, for example, visibility, selected, etc.
:param args: Variables to check the hash
:return: ``True`` if render has changed the widget
"""
_hash = self._hash_variables(*args)
if _hash != self._last_render_hash or self._last_render_hash == 0:
self._last_render_hash = _hash
return True
return False
def set_title(self, title: str) -> 'Widget':
"""
Update the Widget title.
.. note::
Not all widgets implements this method, for example, images don't
accept a title.
:param title: New title
:return: Self reference
"""
self._title = str(title)
self._apply_font()
self._force_render()
return self
def get_title(self) -> str:
"""
Return the Widget title.
.. note::
Not all widgets implements this method, for example, images don't
accept a title, and such widget would return an empty string if this
method is called.
:return: Widget title
"""
return self._title
def set_background_color(
self,
color: Optional[Union[ColorInputType, 'pygame_menu.BaseImage']],
inflate: Optional[Tuple2IntType] = (0, 0)
) -> 'Widget':
"""
Set the Widget background color.
:param color: Widget background color
:param inflate: Inflate background on x-axis and y-axis (x, y). If ``None``, the widget value is not updated
:return: Self reference
"""
if color is not None:
if isinstance(color, pygame_menu.BaseImage):
assert color.get_drawing_mode() == pygame_menu.baseimage.IMAGE_MODE_FILL, \
'currently widget only supports IMAGE_MODE_FILL drawing mode'
else:
color = assert_color(color)
if inflate is None:
inflate = self._background_inflate
assert_vector(inflate, 2, int)
assert inflate[0] >= 0 and inflate[1] >= 0, \
'widget background inflate must be equal or greater than zero in both axis'
self._background_color = color
self._background_inflate = tuple(inflate)
self._background_surface = None
self._force_render()
return self
def background_inflate_to_selection_effect(self) -> 'Widget':