Beispiel #1
0
def add_scroll_command(
    widget: tkinter.Text,
    option: Literal["xscrollcommand", "yscrollcommand"],
    callback: Callable[[], None],
) -> None:
    """Schedule ``callback`` to run with no arguments when ``widget`` is scrolled.

    The option should be ``'xscrollcommand'`` for horizontal scrolling or
    ``'yscrollcommand'`` for vertical scrolling.

    Unlike when setting the option directly, this function can be called
    multiple times with the same widget and the same option to set multiple
    callbacks.
    """
    if not widget[option]:
        widget[option] = lambda *args: None
    tcl_code = widget[option]
    assert isinstance(tcl_code, str)
    assert tcl_code

    # from options(3tk): "... the widget will generate a Tcl command by
    # concatenating the scroll command and two numbers."
    #
    # So if tcl_code is like this:  bla bla bla
    #
    # it would be called like this:  bla bla bla 0.123 0.456
    #
    # and by putting something in front on separate line we can make it get called like this
    #
    #   something
    #   bla bla bla 0.123 0.456
    widget[option] = widget.register(callback) + "\n" + tcl_code
Beispiel #2
0
def add_scroll_command(widget: tkinter.Text, option: Literal['xscrollcommand',
                                                             'yscrollcommand'],
                       callback: Callable[[], None]) -> None:
    if not widget[option]:
        widget[option] = (lambda *args: None)
    tcl_code = widget[option]
    assert isinstance(tcl_code, str)
    assert tcl_code

    # from options(3tk): "... the widget will generate a Tcl command by
    # concatenating the scroll command and two numbers."
    #
    # So if tcl_code is like this:  bla bla bla
    #
    # it would be called like this:  bla bla bla 0.123 0.456
    #
    # and by putting something in front on separate line we can make it get called like this
    #
    #   something
    #   bla bla bla 0.123 0.456
    widget[option] = widget.register(callback) + '\n' + tcl_code
Beispiel #3
0
    def setup(self, widget: tkinter.Text) -> None:
        old_cursor_pos = widget.index("insert")  # must be widget specific

        def cursor_pos_changed() -> None:
            nonlocal old_cursor_pos

            new_pos = widget.index("insert")
            if new_pos == widget.index("end"):
                new_pos = widget.index("end - 1 char")

            if new_pos != old_cursor_pos:
                old_cursor_pos = new_pos
                widget.event_generate("<<CursorMoved>>")

        #       /\
        #      /  \  WARNING: serious tkinter magic coming up
        #     / !! \          proceed at your own risk
        #    /______\
        #
        # this irc conversation might give you an idea of how this works:
        #
        #    <Akuli> __Myst__, why do you want to know how it works?
        #    <__Myst__> Akuli: cause it seems cool
        #    <Akuli> there's 0 reason to docment it in the langserver
        #    <Akuli> ok i can explain :)
        #    <Akuli> in tcl, all statements are command calls
        #    <Akuli> set x lol    ;# set variable x to string lol
        #    <Akuli> set is a command, x and lol are strings
        #    <Akuli> adding stuff to widgets is also command calls
        #    <Akuli> .textwidget insert end hello   ;# add hello to the text
        #            widget
        #    <Akuli> my magic renames the textwidget command to
        #            actual_widget_command, and creates a fake text widget
        #            command that tkinter calls instead
        #    <Akuli> then this fake command checks for all possible widget
        #            commands that can move the cursor or change the content
        #    <Akuli> making sense?
        #    <__Myst__> ooh
        #    <__Myst__> so it's like you're proxying actual calls to the text
        #               widget and calculating change events based on that?
        #    <Akuli> yes
        #    <__Myst__> very cool

        # all widget stuff is implemented in python and in tcl as calls to a
        # tcl command named str(widget), and replacing that with a custom
        # command is a very powerful way to do magic; for example, moving the
        # cursor with arrow keys calls the 'mark set' widget command :D
        actual_widget_command = str(widget) + "_actual_widget"
        widget.tk.call("rename", str(widget), actual_widget_command)

        # this part is tcl because i couldn't get a python callback to work
        widget.tk.eval(
            """
        proc %(fake_widget)s {args} {
            # subcommand is e.g. insert, delete, replace, index, search, ...
            # see text(3tk) for all possible subcommands
            set subcommand [lindex $args 0]

            # issue #5: don't let the cursor to go to the very top or bottom of
            # the view
            if {$subcommand == "see"} {
                # cleaned_index is always a "LINE.COLUMN" string
                set cleaned_index [%(actual_widget)s index [lindex $args 1]]

                # from text(3tk): "If index is far out of view, then the
                # command centers index in the window." and we want to center
                # it correctly, so first go to the center, then a few
                # characters around it, and finally back to center because it
                # feels less error-prone that way
                %(actual_widget)s see $cleaned_index
                %(actual_widget)s see "$cleaned_index - 4 lines"
                %(actual_widget)s see "$cleaned_index + 4 lines"
                %(actual_widget)s see $cleaned_index
                return
            }

            # only these subcommands can change the text, but they can also
            # move the cursor by changing the text before the cursor
            if {$subcommand == "delete" || $subcommand == "insert" || $subcommand == "replace"} {
                # Validate and clean up indexes here so that any problems
                # result in Tcl error
                if {$subcommand == "delete"} {
                    for {set i 1} {$i < [llength $args]} {incr i} {
                        lset args $i [%(actual_widget)s index [lindex $args $i]]
                    }
                }
                if {$subcommand == "insert"} {
                    lset args 1 [%(actual_widget)s index [lindex $args 1]]
                }
                if {$subcommand == "replace"} {
                    lset args 1 [%(actual_widget)s index [lindex $args 1]]
                    lset args 2 [%(actual_widget)s index [lindex $args 2]]
                }
                set prepared_event [%(change_event_from_command)s {*}$args]
            } else {
                set prepared_event ""
            }

            # it's important that this comes after the change cb stuff because
            # this way it's possible to get old_length in self._change_cb()...
            # however, it's also important that this is before the mark set
            # stuff because the documented way to access the new index in a
            # <<CursorMoved>> binding is getting it directly from the widget
            set result [%(actual_widget)s {*}$args]

            if {$prepared_event != ""} {
                # must be after calling actual widget command
                event generate %(event_receiver)s <<ContentChanged>> -data $prepared_event
            }

            # only[*] 'textwidget mark set insert new_location' can change the
            # cursor position, because the cursor position is implemented as a
            # mark named "insert" and there are no other commands that move
            # marks
            #
            # [*] i lied, hehe >:D MUHAHAHA ... inserting text before the
            # cursor also changes it
            if {[lrange $args 0 2] == {mark set insert} || $prepared_event != ""} {
                %(cursor_moved_callback)s
            }

            return $result
        }
        """ % {
                "fake_widget":
                str(widget),
                "actual_widget":
                actual_widget_command,
                "change_event_from_command":
                widget.register(
                    partial(self._change_event_from_command, widget)),
                "event_receiver":
                self._event_receiver_widget,
                "cursor_moved_callback":
                widget.register(cursor_pos_changed),
            })