codekingpro/portable-devtools
114k
1# Urwid Window-Icon-Menu-Pointer-style widget classes2# Copyright (C) 2004-2011 Ian Ward3#4# This library is free software; you can redistribute it and/or5# modify it under the terms of the GNU Lesser General Public6# License as published by the Free Software Foundation; either7# version 2.1 of the License, or (at your option) any later version.8#9# This library is distributed in the hope that it will be useful,10# but WITHOUT ANY WARRANTY; without even the implied warranty of11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU12# Lesser General Public License for more details.13#14# You should have received a copy of the GNU Lesser General Public15# License along with this library; if not, write to the Free Software16# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA17#18# Urwid web site: https://urwid.org/19 20 21from __future__ import annotations22 23import typing24 25from urwid.canvas import CompositeCanvas26from urwid.command_map import Command27from urwid.signals import connect_signal28from urwid.text_layout import calc_coords29from urwid.util import is_mouse_press30 31from .columns import Columns32from .constants import Align, WrapMode33from .text import Text34from .widget import WidgetError, WidgetWrap35 36if typing.TYPE_CHECKING:37 from collections.abc import Callable, Hashable, MutableSequence38 39 from typing_extensions import Literal, Self40 41 from urwid.canvas import TextCanvas42 from urwid.text_layout import TextLayout43 44 _T = typing.TypeVar("_T")45 46 47class SelectableIcon(Text):48 ignore_focus = False49 _selectable = True50 51 def __init__(52 self,53 text: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],54 cursor_position: int = 0,55 align: Literal["left", "center", "right"] | Align = Align.LEFT,56 wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,57 layout: TextLayout | None = None,58 ) -> None:59 """60 :param text: markup for this widget; see :class:`Text` for61 description of text markup62 :param cursor_position: position the cursor will appear in the63 text when this widget is in focus64 :param align: typically ``'left'``, ``'center'`` or ``'right'``65 :type align: text alignment mode66 :param wrap: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``67 :type wrap: text wrapping mode68 :param layout: defaults to a shared :class:`StandardTextLayout` instance69 :type layout: text layout instance70 71 This is a text widget that is selectable. A cursor72 displayed at a fixed location in the text when in focus.73 This widget has no special handling of keyboard or mouse input.74 """75 super().__init__(text, align=align, wrap=wrap, layout=layout)76 self._cursor_position = cursor_position77 78 def render( # type: ignore[override]79 self,80 size: tuple[int] | tuple[()], # type: ignore[override]81 focus: bool = False,82 ) -> TextCanvas | CompositeCanvas: # type: ignore[override]83 """84 Render the text content of this widget with a cursor when85 in focus.86 87 >>> si = SelectableIcon("[!]")88 >>> si89 <SelectableIcon selectable fixed/flow widget '[!]'>90 >>> si.render((4,), focus=True).cursor91 (0, 0)92 >>> si = SelectableIcon("((*))", 2)93 >>> si.render((8,), focus=True).cursor94 (2, 0)95 >>> si.render((2,), focus=True).cursor96 (0, 1)97 >>> si.render(()).cursor98 >>> si.render(()).text99 [b'((*))']100 >>> si.render((), focus=True).cursor101 (2, 0)102 """103 c: TextCanvas | CompositeCanvas = super().render(size, focus)104 if focus:105 # create a new canvas so we can add a cursor106 c = CompositeCanvas(c)107 c.cursor = self.get_cursor_coords(size)108 return c109 110 def get_cursor_coords(self, size: tuple[int] | tuple[()]) -> tuple[int, int] | None:111 """112 Return the position of the cursor if visible. This method113 is required for widgets that display a cursor.114 """115 if self._cursor_position > len(self.text):116 return None117 # find out where the cursor will be displayed based on118 # the text layout119 if size:120 (maxcol,) = size121 else:122 maxcol, _ = self.pack()123 trans = self.get_line_translation(maxcol)124 x, y = calc_coords(self.text, trans, self._cursor_position)125 if maxcol <= x:126 return None127 return x, y128 129 def keypress(130 self,131 size: tuple[int] | tuple[()], # type: ignore[override]132 key: str,133 ) -> str:134 """135 No keys are handled by this widget. This method is136 required for selectable widgets.137 """138 return key139 140 141class CheckBoxError(WidgetError):142 pass143 144 145class CheckBox(WidgetWrap[Columns]):146 states: typing.ClassVar[dict[bool | Literal["mixed"], SelectableIcon]] = {147 True: SelectableIcon("[X]", 1),148 False: SelectableIcon("[ ]", 1),149 "mixed": SelectableIcon("[#]", 1),150 }151 reserve_columns = 4152 153 # allow users of this class to listen for change events154 # sent when the state of this widget is modified155 # (this variable is picked up by the MetaSignals metaclass)156 signals: typing.ClassVar[list[str]] = ["change", "postchange"]157 158 @typing.overload159 def __init__(160 self,161 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],162 state: bool = False,163 has_mixed: typing.Literal[False] = False,164 on_state_change: Callable[[Self, bool, _T], typing.Any] | None = None,165 user_data: _T = ...,166 checked_symbol: str | None = ...,167 ) -> None: ...168 169 @typing.overload170 def __init__(171 self,172 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],173 state: typing.Literal["mixed"] | bool = False,174 has_mixed: typing.Literal[True] = True,175 on_state_change: Callable[[Self, bool | typing.Literal["mixed"], _T], typing.Any] | None = None,176 user_data: _T = ...,177 checked_symbol: str | None = ...,178 ) -> None: ...179 180 @typing.overload181 def __init__(182 self,183 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],184 state: bool = False,185 has_mixed: typing.Literal[False] = False,186 on_state_change: Callable[[Self, bool], typing.Any] | None = None,187 user_data: None = None,188 checked_symbol: str | None = ...,189 ) -> None: ...190 191 @typing.overload192 def __init__(193 self,194 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],195 state: typing.Literal["mixed"] | bool = False,196 has_mixed: typing.Literal[True] = True,197 on_state_change: Callable[[Self, bool | typing.Literal["mixed"]], typing.Any] | None = None,198 user_data: None = None,199 checked_symbol: str | None = ...,200 ) -> None: ...201 202 def __init__(203 self,204 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],205 state: bool | Literal["mixed"] = False,206 has_mixed: typing.Literal[False, True] = False, # MyPy issue: Literal[True, False] is not equal `bool`207 on_state_change: (208 Callable[[Self, bool, _T], typing.Any]209 | Callable[[Self, bool | typing.Literal["mixed"], _T], typing.Any]210 | Callable[[Self, bool], typing.Any]211 | Callable[[Self, bool | typing.Literal["mixed"]], typing.Any]212 | None213 ) = None,214 user_data: _T | None = None,215 checked_symbol: str | None = None,216 ):217 """218 :param label: markup for check box label219 :param state: False, True or "mixed"220 :param has_mixed: True if "mixed" is a state to cycle through221 :param on_state_change: shorthand for connect_signal()222 function call for a single callback223 :param user_data: user_data for on_state_change224 225 ..note:: `pack` method expect, that `Columns` backend widget is not modified from outside226 227 Signals supported: ``'change'``, ``"postchange"``228 229 Register signal handler with::230 231 urwid.connect_signal(check_box, "change", callback, user_data)232 233 where callback is callback(check_box, new_state [,user_data])234 Unregister signal handlers with::235 236 urwid.disconnect_signal(check_box, "change", callback, user_data)237 238 >>> CheckBox("Confirm")239 <CheckBox selectable fixed/flow widget 'Confirm' state=False>240 >>> CheckBox("Yogourt", "mixed", True)241 <CheckBox selectable fixed/flow widget 'Yogourt' state='mixed'>242 >>> cb = CheckBox("Extra onions", True)243 >>> cb244 <CheckBox selectable fixed/flow widget 'Extra onions' state=True>245 >>> cb.render((20,), focus=True).text246 [b'[X] Extra onions ']247 >>> CheckBox("Test", None)248 Traceback (most recent call last):249 ...250 ValueError: None not in (True, False, 'mixed')251 """252 if state not in self.states:253 raise ValueError(f"{state!r} not in {tuple(self.states.keys())}")254 255 self._label = Text(label)256 self.has_mixed = has_mixed257 258 self._state = state259 if checked_symbol:260 self.states[True] = SelectableIcon(f"[{checked_symbol}]", 1)261 # The old way of listening for a change was to pass the callback262 # in to the constructor. Just convert it to the new way:263 if on_state_change:264 connect_signal(self, "change", on_state_change, user_data)265 266 # Initial create expect no callbacks call, create explicit267 super().__init__(268 Columns(269 [(self.reserve_columns, self.states[state]), self._label],270 focus_column=0,271 ),272 )273 274 def pack(self, size: tuple[()] | tuple[int] | None = None, focus: bool = False) -> tuple[str, str]:275 """Pack for widget.276 277 :param size: size data. Special case: None - get minimal widget size to fit278 :param focus: widget is focused279 280 >>> cb = CheckBox("test")281 >>> cb.pack((10,))282 (10, 1)283 >>> cb.pack()284 (8, 1)285 >>> ml_cb = CheckBox("Multi\\nline\\ncheckbox")286 >>> ml_cb.pack()287 (12, 3)288 >>> ml_cb.pack((), True)289 (12, 3)290 """291 return super().pack(size or (), focus)292 293 def _repr_words(self) -> list[str]:294 return [*super()._repr_words(), repr(self.label)]295 296 def _repr_attrs(self) -> dict[str, typing.Any]:297 return {**super()._repr_attrs(), "state": self.state}298 299 def set_label(self, label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]):300 """301 Change the check box label.302 303 label -- markup for label. See Text widget for description304 of text markup.305 306 >>> cb = CheckBox("foo")307 >>> cb308 <CheckBox selectable fixed/flow widget 'foo' state=False>309 >>> cb.set_label(("bright_attr", "bar"))310 >>> cb311 <CheckBox selectable fixed/flow widget 'bar' state=False>312 """313 self._label.set_text(label)314 # no need to call self._invalidate(). WidgetWrap takes care of315 # that when self.w changes316 317 def get_label(self):318 """319 Return label text.320 321 >>> cb = CheckBox("Seriously")322 >>> print(cb.get_label())323 Seriously324 >>> print(cb.label)325 Seriously326 >>> cb.set_label([("bright_attr", "flashy"), " normal"])327 >>> print(cb.label) # only text is returned328 flashy normal329 """330 return self._label.text331 332 label = property(get_label)333 334 def set_state(335 self,336 state: bool | Literal["mixed"],337 do_callback: bool = True,338 ) -> None:339 """340 Set the CheckBox state.341 342 state -- True, False or "mixed"343 do_callback -- False to suppress signal from this change344 345 >>> from urwid import disconnect_signal346 >>> changes = []347 >>> def callback_a(user_data, cb, state):348 ... changes.append("A %r %r" % (state, user_data))349 >>> def callback_b(cb, state):350 ... changes.append("B %r" % state)351 >>> cb = CheckBox("test", False, False)352 >>> key1 = connect_signal(cb, "change", callback_a, user_args=("user_a",))353 >>> key2 = connect_signal(cb, "change", callback_b)354 >>> cb.set_state(True) # both callbacks will be triggered355 >>> cb.state356 True357 >>> disconnect_signal(cb, "change", callback_a, user_args=("user_a",))358 >>> cb.state = False359 >>> cb.state360 False361 >>> cb.set_state(True)362 >>> cb.state363 True364 >>> cb.set_state(False, False) # don't send signal365 >>> changes366 ["A True 'user_a'", 'B True', 'B False', 'B True']367 """368 if self._state == state:369 return370 371 if state not in self.states:372 raise CheckBoxError(f"{self!r} Invalid state: {state!r}")373 374 # self._state is None is a special case when the CheckBox375 # has just been created376 old_state = self._state377 if do_callback:378 self._emit("change", state)379 self._state = state380 # rebuild the display widget with the new state381 self._w = Columns([(self.reserve_columns, self.states[state]), self._label], focus_column=0)382 if do_callback:383 self._emit("postchange", old_state)384 385 def get_state(self) -> bool | Literal["mixed"]:386 """Return the state of the checkbox."""387 return self._state388 389 state = property(get_state, set_state)390 391 def keypress(self, size: tuple[int], key: str) -> str | None:392 """393 Toggle state on 'activate' command.394 395 >>> assert CheckBox._command_map[" "] == "activate"396 >>> assert CheckBox._command_map["enter"] == "activate"397 >>> size = (10,)398 >>> cb = CheckBox("press me")399 >>> cb.state400 False401 >>> cb.keypress(size, " ")402 >>> cb.state403 True404 >>> cb.keypress(size, " ")405 >>> cb.state406 False407 """408 if self._command_map[key] != Command.ACTIVATE:409 return key410 411 self.toggle_state()412 return None413 414 def toggle_state(self) -> None:415 """416 Cycle to the next valid state.417 418 >>> cb = CheckBox("3-state", has_mixed=True)419 >>> cb.state420 False421 >>> cb.toggle_state()422 >>> cb.state423 True424 >>> cb.toggle_state()425 >>> cb.state426 'mixed'427 >>> cb.toggle_state()428 >>> cb.state429 False430 """431 if self.state is False:432 self.set_state(True)433 elif self.state is True:434 if self.has_mixed:435 self.set_state("mixed")436 else:437 self.set_state(False)438 elif self.state == "mixed":439 self.set_state(False)440 441 def mouse_event(self, size: tuple[int], event: str, button: int, x: int, y: int, focus: bool) -> bool:442 """443 Toggle state on button 1 press.444 445 >>> size = (20,)446 >>> cb = CheckBox("clickme")447 >>> cb.state448 False449 >>> cb.mouse_event(size, "mouse press", 1, 2, 0, True)450 True451 >>> cb.state452 True453 """454 if button != 1 or not is_mouse_press(event):455 return False456 self.toggle_state()457 return True458 459 460class RadioButton(CheckBox):461 states: typing.ClassVar[dict[bool | Literal["mixed"], SelectableIcon]] = {462 True: SelectableIcon("(X)", 1),463 False: SelectableIcon("( )", 1),464 "mixed": SelectableIcon("(#)", 1),465 }466 reserve_columns = 4467 468 @typing.overload469 def __init__(470 self,471 group: MutableSequence[RadioButton],472 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],473 state: bool | Literal["first True"] = ...,474 on_state_change: Callable[[Self, bool, _T], typing.Any] | None = None,475 user_data: _T = ...,476 ) -> None: ...477 478 @typing.overload479 def __init__(480 self,481 group: MutableSequence[RadioButton],482 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],483 state: bool | Literal["first True"] = ...,484 on_state_change: Callable[[Self, bool], typing.Any] | None = None,485 user_data: None = None,486 ) -> None: ...487 488 def __init__(489 self,490 group: MutableSequence[RadioButton],491 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],492 state: bool | Literal["first True"] = "first True",493 on_state_change: Callable[[Self, bool, _T], typing.Any] | Callable[[Self, bool], typing.Any] | None = None,494 user_data: _T | None = None,495 ) -> None:496 """497 :param group: list for radio buttons in same group498 :param label: markup for radio button label499 :param state: False, True, "mixed" or "first True"500 :param on_state_change: shorthand for connect_signal()501 function call for a single 'change' callback502 :param user_data: user_data for on_state_change503 504 ..note:: `pack` method expect, that `Columns` backend widget is not modified from outside505 506 This function will append the new radio button to group.507 "first True" will set to True if group is empty.508 509 Signals supported: ``'change'``, ``"postchange"``510 511 Register signal handler with::512 513 urwid.connect_signal(radio_button, "change", callback, user_data)514 515 where callback is callback(radio_button, new_state [,user_data])516 Unregister signal handlers with::517 518 urwid.disconnect_signal(radio_button, "change", callback, user_data)519 520 >>> bgroup = [] # button group521 >>> b1 = RadioButton(bgroup, "Agree")522 >>> b2 = RadioButton(bgroup, "Disagree")523 >>> len(bgroup)524 2525 >>> b1526 <RadioButton selectable fixed/flow widget 'Agree' state=True>527 >>> b2528 <RadioButton selectable fixed/flow widget 'Disagree' state=False>529 >>> b2.render((15,), focus=True).text # ... = b in Python 3530 [...'( ) Disagree ']531 """532 if state == "first True":533 state = not group534 535 self.group = group536 super().__init__(label, state, False, on_state_change, user_data) # type: ignore[call-overload]537 group.append(self)538 539 def set_state(self, state: bool | Literal["mixed"], do_callback: bool = True) -> None:540 """541 Set the RadioButton state.542 543 state -- True, False or "mixed"544 545 do_callback -- False to suppress signal from this change546 547 If state is True all other radio buttons in the same button548 group will be set to False.549 550 >>> bgroup = [] # button group551 >>> b1 = RadioButton(bgroup, "Agree")552 >>> b2 = RadioButton(bgroup, "Disagree")553 >>> b3 = RadioButton(bgroup, "Unsure")554 >>> b1.state, b2.state, b3.state555 (True, False, False)556 >>> b2.set_state(True)557 >>> b1.state, b2.state, b3.state558 (False, True, False)559 >>> def relabel_button(radio_button, new_state):560 ... radio_button.set_label("Think Harder!")561 >>> key = connect_signal(b3, "change", relabel_button)562 >>> b3563 <RadioButton selectable fixed/flow widget 'Unsure' state=False>564 >>> b3.set_state(True) # this will trigger the callback565 >>> b3566 <RadioButton selectable fixed/flow widget 'Think Harder!' state=True>567 """568 if self._state == state:569 return570 571 super().set_state(state, do_callback)572 573 # if we're clearing the state we don't have to worry about574 # other buttons in the button group575 if state is not True:576 return577 578 # clear the state of each other radio button579 for cb in self.group:580 if cb is self:581 continue582 if cb.state:583 cb.state = False584 585 def toggle_state(self) -> None:586 """587 Set state to True.588 589 >>> bgroup = [] # button group590 >>> b1 = RadioButton(bgroup, "Agree")591 >>> b2 = RadioButton(bgroup, "Disagree")592 >>> b1.state, b2.state593 (True, False)594 >>> b2.toggle_state()595 >>> b1.state, b2.state596 (False, True)597 >>> b2.toggle_state()598 >>> b1.state, b2.state599 (False, True)600 """601 self.set_state(True)602 603 604class Button(WidgetWrap[Columns]):605 button_left = Text("<")606 button_right = Text(">")607 608 signals: typing.ClassVar[list[str]] = ["click"]609 610 @typing.overload611 def __init__(612 self,613 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],614 on_press: Callable[[Self, _T], typing.Any] | None = None,615 user_data: _T = ...,616 *,617 align: Literal["left", "center", "right"] | Align = ...,618 wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = ...,619 layout: TextLayout | None = ...,620 ) -> None: ...621 622 @typing.overload623 def __init__(624 self,625 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],626 on_press: Callable[[Self], typing.Any] | None = None,627 user_data: None = None,628 *,629 align: Literal["left", "center", "right"] | Align = ...,630 wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = ...,631 layout: TextLayout | None = ...,632 ) -> None: ...633 634 def __init__(635 self,636 label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]],637 on_press: Callable[[Self, _T], typing.Any] | Callable[[Self], typing.Any] | None = None,638 user_data: _T | None = None,639 *,640 align: Literal["left", "center", "right"] | Align = Align.LEFT,641 wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,642 layout: TextLayout | None = None,643 ) -> None:644 """645 :param label: markup for button label646 :param on_press: shorthand for connect_signal()647 function call for a single callback648 :param user_data: user_data for on_press649 :param align: typically ``'left'``, ``'center'`` or ``'right'``650 :type align: label alignment mode651 :param wrap: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``652 :type wrap: label wrapping mode653 :param layout: defaults to a shared :class:`StandardTextLayout` instance654 :type layout: text layout instance655 656 ..note:: `pack` method expect, that `Columns` backend widget is not modified from outside657 658 Signals supported: ``'click'``659 660 Register signal handler with::661 662 urwid.connect_signal(button, "click", callback, user_data)663 664 where callback is callback(button [,user_data])665 Unregister signal handlers with::666 667 urwid.disconnect_signal(button, "click", callback, user_data)668 669 >>> from urwid.util import set_temporary_encoding670 >>> Button("Ok")671 <Button selectable fixed/flow widget 'Ok'>672 >>> b = Button("Cancel")673 >>> b.render((15,), focus=True).text # ... = b in Python 3674 [b'< Cancel >']675 >>> aligned_button = Button("Test", align=Align.CENTER)676 >>> aligned_button.render((10,), focus=True).text677 [b'< Test >']678 >>> wrapped_button = Button("Long label", wrap=WrapMode.ELLIPSIS)679 >>> with set_temporary_encoding("utf-8"):680 ... wrapped_button.render((7,), focus=False).text[0].decode("utf-8")681 '< Lo… >'682 """683 self._label = SelectableIcon(label, 0, align=align, wrap=wrap, layout=layout)684 cols = Columns(685 [(1, self.button_left), self._label, (1, self.button_right)],686 dividechars=1,687 )688 super().__init__(cols)689 690 # The old way of listening for a change was to pass the callback691 # in to the constructor. Just convert it to the new way:692 if on_press:693 connect_signal(self, "click", on_press, user_data)694 695 def pack(self, size: tuple[()] | tuple[int] | None = None, focus: bool = False) -> tuple[int, int]:696 """Pack for widget.697 698 :param size: size data. Special case: None - get minimal widget size to fit699 :param focus: widget is focused700 701 >>> btn = Button("Some button")702 >>> btn.pack((10,))703 (10, 2)704 >>> btn.pack()705 (15, 1)706 >>> btn.pack((), True)707 (15, 1)708 """709 return super().pack(size or (), focus)710 711 def _repr_words(self) -> list[str]:712 # include button.label in repr(button)713 return [*super()._repr_words(), repr(self.label)]714 715 def set_label(self, label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:716 """717 Change the button label.718 719 label -- markup for button label720 721 >>> b = Button("Ok")722 >>> b.set_label("Yup yup")723 >>> b724 <Button selectable fixed/flow widget 'Yup yup'>725 """726 self._label.set_text(label)727 728 def get_label(self) -> str | bytes:729 """730 Return label text.731 732 >>> b = Button("Ok")733 >>> print(b.get_label())734 Ok735 >>> print(b.label)736 Ok737 """738 return self._label.text739 740 label = property(get_label)741 742 def keypress(self, size: tuple[int], key: str) -> str | None:743 """744 Send 'click' signal on 'activate' command.745 746 >>> assert Button._command_map[" "] == "activate"747 >>> assert Button._command_map["enter"] == "activate"748 >>> size = (15,)749 >>> b = Button("Cancel")750 >>> clicked_buttons = []751 >>> def handle_click(button):752 ... clicked_buttons.append(button.label)753 >>> key = connect_signal(b, "click", handle_click)754 >>> b.keypress(size, "enter")755 >>> b.keypress(size, " ")756 >>> clicked_buttons # ... = u in Python 2757 [...'Cancel', ...'Cancel']758 """759 if self._command_map[key] != Command.ACTIVATE:760 return key761 762 self._emit("click")763 return None764 765 def mouse_event(self, size: tuple[int], event: str, button: int, x: int, y: int, focus: bool) -> bool:766 """767 Send 'click' signal on button 1 press.768 769 >>> size = (15,)770 >>> b = Button("Ok")771 >>> clicked_buttons = []772 >>> def handle_click(button):773 ... clicked_buttons.append(button.label)774 >>> key = connect_signal(b, "click", handle_click)775 >>> b.mouse_event(size, "mouse press", 1, 4, 0, True)776 True777 >>> b.mouse_event(size, "mouse press", 2, 4, 0, True) # ignored778 False779 >>> clicked_buttons # ... = u in Python 2780 [...'Ok']781 """782 if button != 1 or not is_mouse_press(event):783 return False784 785 self._emit("click")786 return True787 788 789def _test():790 import doctest791 792 doctest.testmod()793 794 795if __name__ == "__main__":796 _test()797 