From 9a27b147239c3326046ac439abccc07127b16f91 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Sat, 13 Jun 2020 18:37:19 +0100 Subject: [PATCH 01/17] Added TkDND support to tkinter --- Doc/library/tkinter.dnd.rst | 12 +- Doc/library/tkinter.rst | 581 +++++++++++++++++++++++++++++++++- Lib/tkinter/__init__.py | 193 ++++++++++- Lib/tkinter/constants.py | 31 ++ Lib/tkinter/dnd.py | 6 + PCbuild/get_externals.bat | 2 + PCbuild/tcltk.props | 5 + Tools/msi/tcltk/tcltk.wixproj | 8 + 8 files changed, 823 insertions(+), 15 deletions(-) diff --git a/Doc/library/tkinter.dnd.rst b/Doc/library/tkinter.dnd.rst index 6c11c739e1fa11..0d72dfeb5fbd7c 100644 --- a/Doc/library/tkinter.dnd.rst +++ b/Doc/library/tkinter.dnd.rst @@ -1,16 +1,18 @@ -:mod:`tkinter.dnd` --- Drag and drop support -============================================ +:mod:`tkinter.dnd` --- Deprecated drag and drop support +======================================================= .. module:: tkinter.dnd :platform: Tk :synopsis: Tkinter drag-and-drop interface + :deprecated: **Source code:** :source:`Lib/tkinter/dnd.py` --------------- +.. deprecated:: 3.10 + The :mod:`tkinter.dnd` module is deprecated in favour of the TkDND bindings + in the main :mod:`tkinter` module. -.. note:: This is experimental and due to be deprecated when it is replaced - with the Tk DND. +-------------- The :mod:`tkinter.dnd` module provides drag-and-drop support for objects within a single application, within the same window or between windows. To enable an diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 2dc44ad36a7f73..e112a6614a3e44 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -131,8 +131,8 @@ Other modules that provide Tk support include: Basic dialogs and convenience functions. :mod:`tkinter.dnd` - Drag-and-drop support for :mod:`tkinter`. This is experimental and should - become deprecated when it is replaced with the Tk DND. + Deprecated drag-and-drop support for :mod:`tkinter` + (see the :ref:`tkinter-tkdnd` section) :mod:`turtle` Turtle graphics in a Tk window. @@ -305,6 +305,583 @@ The legal values of *someOptions* is action dependent. Some actions, like command, would need arguments to specify what range of text to delete. +.. _tkinter-tkdnd: + +TkDND Support +------------- + +:mod:`tkinter` has support for a native, platform specific drag and drop +mechanism, through `Tkdnd `_, with the user +able to register widgets as drag sources or drop targets. + +On Windows, TkDND is packaged with +Tcl/Tk so should work "out-of-the-box" with a standard installation. On +other platforms, it will need to be installed seperately. + +.. note:: As a result of this functionality, the :mod:`tkinter.dnd` module is + now deprecated. + +.. versionadded:: 3.10 + +Functions +^^^^^^^^^ + +.. function:: Tk.load_dnd(dnd_path=None) + + This command will load the TkDND library and is called when ``Tk`` class is + instantiated. However, if the library is installed in a different + directory, this method will need to be called manually to load the library. + If the TkDND library has been found, ``True`` wil be returned, otherwise + ``False`` will be returned. + + If the TkDND library has already been successfully loaded, ``True`` will be + returned and no action taken. If Tk has not been loaded for the + interpreter (a call was made to ``tkinter.Tcl`` rather than ``tkinter.Tk``), + the value returned will always be ``False`` and no action taken. + + .. note:: Calling this method on one ``Tk`` instance will not affect any + others (i.e. the TkDND install directory will need to be declared to every + ``Tk`` instance where DND capabilities are required). + +.. function:: Misc.dnd_loaded() + + This function returns whether the TkDND library has been successfully loaded + for this ``Tk`` instance. + +.. function:: Misc.drop_target_register(types=()) + + This command will register the widget as a drop target (a widget than can + accept a drop action). An optional type-list can be provided, which + contains one or more types that the widget will accept during a drop + action. + + .. note:: This command can be executed multiple times on the same widget. + +.. function:: Misc.drop_target_unregister() + + This command will stop the widget from being a drop target. Thus, it will + stop receiving events related to drop operations. + +.. function:: Misc.drag_source_register(types=(), mouse_buttons=1) + + This command will register the widget as a drag source. A drag source is a + widget than can start a drag action. When the widget is registered as a + drag source, an optional type-list can be provided. This type list can + contain one or more types that the widget will provide during a drag + action. However, this type list is indicative/informative and the widget + *can* initiate a drag action with a type not in the list. + + Finally, ``mouse_buttons`` is one or more mouse buttons that can be used + for starting the drag action. It can have any of the values: + + * 1 - Left mouse button + * 2 - Middle mouse button (wheel) + * 3 - Right mouse button + + Multiple mouse buttons can be specified as a list/tuple of values. + + .. note:: This command can be executed multiple times on the same widget. + +.. function:: Misc.drag_source_unregister() + + This command will stop the widget from being a drag source. Thus, it will + stop receiving events related to drag operations. + +.. function:: Misc.platform_specific_types(types) + + This command will accept a list of types that can contain platform + independent or platform specific types. A new list will be returned, where + each platform independent type in type-list will be substituted by one or + more platform specific types. Thus, the returned list may have more + elements than type-list. + +.. function:: Misc.platform_independent_types(types) + + This command will accept a list of types that can contain platform + independent or platform specific types. A new list will be returned, where + each platform specific type in type-list will be substituted by one or more + platform independent types. Thus, the returned list may have more elements + than type-list. + +.. function:: Misc.get_drop_file_temp_directory() + + This command will return the temporary directory used by TkDND for storing + temporary files. When the package is loaded, this temporary directory will + be initialised to a proper directory according to the operating system. + + The default initial value can be changed by the following environment + variables: + + * ``TKDND_TEMP_DIR`` + * ``TEMP`` + * ``TMP`` + +.. function:: Misc.set_drop_file_temp_directory(directory) + + This command will change the temporary directory used by TkDND for storing + temporary files to the given directory. + +Constants +^^^^^^^^^ + +In order to declare the format that the data that will transferred during a +drag and drop operation, all drag and drop protocols use the notion of types. +Unfortunately, each protocol defines its own, usually platform specific, +types. TkDND, trying to maintain portability among different platforms, offers +some predefined types for basic kinds of data, like text and filenames. + +Platform Independent Types +>>>>>>>>>>>>>>>>>>>>>>>>>> + +Currently, the following predefined cross-platform values are available: + +.. data:: DND_ALL + + All supported types for the platform. + + .. warning:: ``DND_ALL`` can declare support for unexpected formats. For + example, it can declare special formats to receive text not declared by + ``DND_TEXT`` (such as RTF and HTML). While it appears to be able to receive + these types, support outside of the formats found as constants is not + guaranteed. + +.. data:: DND_TEXT + + This type can be used for transferring textual data. Internally, it is + translated to the following platform specific formats: + + +----------+------------------------------+ + | Platform | Type/s | + +==========+==============================+ + | Windows | .. data:: CF_UNICODETEXT | + | | CF_TEXT | + | | :noindex: | + +----------+------------------------------+ + | Unix | .. data:: TEXT_PLAIN_UNICODE | + | | TEXT_PLAIN | + | | :noindex: | + +----------+------------------------------+ + | Mac | .. data:: NSSTRINGPBOARDTYPE | + | | :noindex: | + +----------+------------------------------+ + +.. data:: DND_FILES + + This type can be used for transferring a list of filepaths/URLs. + Internally, it is translated to the following platform specific + formats: + + +----------+---------------------------------+ + | Platform | Type/s | + +==========+=================================+ + | Windows | .. data:: CF_HDROP | + | | :noindex: | + +----------+---------------------------------+ + | Unix | .. data:: URI_LIST | + | | :noindex: | + +----------+---------------------------------+ + | Mac | .. data:: NSFILENAMESPBOARDTYPE | + | | :noindex: | + +----------+---------------------------------+ + +Platform Specific Types +>>>>>>>>>>>>>>>>>>>>>>> + +Additionally to the platform independent types, TkDND supports the following +platform specific types (though it is often better to use an independent type +where possible): + +**Windows:** + + .. data:: CF_UNICODETEXT + + Text transfer encoded in Unicode. + + .. data:: CF_TEXT + + Text transfer with application dependent encoding. If the source has + specified an encoding it is used, else the system encoding is used for + the conversion. + + .. data:: CF_HDROP + + Filepath/URL transfer encoded in UTF-8. + +**Unix:** + + .. data:: TEXT_PLAIN_UNICODE + + Text transfer encoded in Unicode. + + .. data:: TEXT_PLAIN + + Text transfer with application dependent encoding. If the source has + specified an encoding it is used, else the system encoding is used for + the conversion. + + .. data:: URI_LIST + + Filepath/URL transfer encoded in ASCII. + +**Mac:** + + .. data:: NSSTRINGPBOARDTYPE + + Text transfer encoded using the system encoding. + + .. data:: NSFILENAMESPBOARDTYPE + + Filepath/URL transfer encoded using the system encoding. + +Finally, format types used for drop types can have wildcards, following the +same rules as "string match". For example, registering a drop target with the +type ``'*'`` (``DND_ALL``), will accept any drop, no matter what the drop +format is. + +Supported Actions +>>>>>>>>>>>>>>>>> + +These actions are returned by the relevant bindings to specify the action +that should take place. + +.. data:: ASK + + A dialog will be displayed to the user, in order to select an action. + +.. data:: COPY + + The data will be copied. + +.. data:: LINK + + The data will be linked. + +.. data:: MOVE + + The data will be moved. + +.. data:: PRIVATE + + A private action will be performed by the drop target. + +.. data:: REFUSE_DROP + + A drop cannot occur. + +Events +^^^^^^ + +Widgets registered as either drop targets or drag sources, will receive +certain events, during drag and drop operations. As a result, the widgets +are expected to have bindings for some of these events. Some events are +mandatory (in the sense that a drag or drop operation can be stopped if the +bindings do not exist), while others are not. + +In the following two sections all virtual events defined by the TkDND package +are presented. + +.. note:: It is a good practice to define bindings for all events, so that the + application will behave as expected during drag and drop operations. + +.. note:: While these event bindings are regular Tk events, they have a small + difference from plain Tk events, in that most of them are expected to return + a value. + +Drop Target Events +>>>>>>>>>>>>>>>>>> + +A widget registered as a drop target, can have bindings for the following +virtual events: + +``<>`` + + This event is triggered when the mouse enters the widget during a drop + action. The intention of this event is to change the visual state of the + widget, so as to notify the user whether the drop will be accepted or not. + The binding script is expected to return a single value that will define + the drop action. + + .. note:: This event is not mandatory, but if it is defined, it has to + return an action (otherwise the drop is refused). + +``<>`` + + This event is triggered when the mouse moves inside the widget during a + drop action (similar to the normal ```` event). The intention of + this event is to let widget decide if it will accept the drop and the + action of the drop, if a drop is going to happen at the specific mouse + coordinates. + + Thus, the script binding for such an event can get the mouse coordinates + and the pressed modifier buttons (such as ctrl, shift or alt), and is + expected to return the drop action. + + .. note:: This event is not mandatory, but if it is defined, it has to + return an action (otherwise the drop is refused). + +``<>`` + + This event is triggered when the mouse leaves the area covered by the + widget, without a drop happening. The binding of such an event is expected + to restore the visual state of the widget to normal (i.e. the visual state + the widget was in before the ``<>`` event was triggered). + + This event is not mandatory, and is not expected to return a value. + +``<>`` + + This event is triggered by a drop action, and it is expected to handle the + dropped data and reset the visual state of the widget. The binding script + is expected to return a value, which will be the action that has been + performed to the data. + + .. note:: This event is not mandatory, but if it is defined, it has to + return an action (otherwise the drop is refused). + +``*_DROP`` + + This event is a specialisation of the generic ``<>`` event, augmented + with a type. If such a binding exists and the drop type matches type, this + event binding will be executed, instead of the generic ``<>`` event + binding. + + For each content type constant (**except** ``DND_ALL``), there is a + ``_DROP`` version of it to provide the binding (e.g. the binding for a drop + of ``DND_FILES`` would be ``DND_FILES_DROP``) + + These events allow for easy specialisation of drop bindings, according to + the type of the dropped data. The binding script of such an event is + expected to return a value, which will be the action that has been + performed to the data. + + .. note:: This event is not mandatory, but if it is defined, it has to + return an action (otherwise the drop is refused). + +Drag Source Events +>>>>>>>>>>>>>>>>>> + +A widget registered as a drag source, is expected to have bindings for the +following virtual events: + +``<>`` + + This event is triggered when a drag action is about to start. This is a + mandatory event (whose absence will cancel the drag action), and is + responsible for providing a list containing three things: + + * A list of actions supported by the drag source + * A list format types supported by the drag source + * The data to be dropped + + A simple example of such a binding, is: :: + + drag_source.bind('<>', lambda event: \ + (COPY, DND_TEXT, 'Spam & eggs')) + +``<>`` + + This event is triggered when the drag action has finished (either when the + drop was successful or not). Its main purpose is to process the dropped + data according to the drop action returned by the drop target. + + This event is not mandatory, and is not expected to return a value. + +Event Data +>>>>>>>>>> + +.. data:: Event.action + + The current action of the drag/drop operation. + +.. data:: Event.code + + The code of the current type of the drag and drop operation. + +.. data:: Event.common_source_types + + The list of types from the drag source type list that are common to the + drop target type list. + +.. data:: Event.common_target_types + + The list of types from the drop target type list that are common to the + drag source type list. + +.. data:: Event.data + + The data that has been dropped. Under some platforms the data will be + available before the drop has occurred. The format of the data is the + current type of the drop operation. + + .. note:: This is always a list/tuple, even for text where there will only + be one item in the list/tuple. + +.. data:: Event.event_name + + The name of the current virtual event. One of ``<>``, + ``<>``, ``<>``, ``<>``, ``<>``, + ``<>`` and ``<>``. + +.. data:: Event.modifiers + + The list of modifier keyboard keys that are pressed. Modifier keys are some + special keys, like Shift, Control or Alt. Valid modifiers are "shift", + "ctrl" and "alt" (under all operating systems), and "mod1" to "mod5" under + Unix. + + .. note:: The usefulness of modifiers may differ across operating systems. + + For example, under Windows the drop target must examine + the modifiers, and decide upon the drop action that must be performed (by + selecting an action from the list of actions supported by the drag source). + + However, under Unix, the drag source is expected to decide on the drop + action, not the drop target. The drop target is expected to either accept + the action selected by the drag source (and return it back), or select + ``COPY``, ``DEFAULT``, or ``ASK``. So, under Unix examining the pressed + modifier keys fulfills only informative purposes. + +.. data:: Event.mouse_buttons + + The numbers of the mouse buttons pressed during a drag/drop operation. + + .. note:: Typically only a single mouse button is reported as pressed, even + if more than one mouse buttons are actually pressed. + + On Unix, however, the value will always be "1" unless the XKeyboard + extension is installed (in which case, the mouse buttons will be reported + correctly). + +.. data:: Event.source_actions + + The action list supported by the drag source. + +.. data:: Event.source_codes + + The codes of the list of types supported by the drag source. Codes are in + the same order as the list of types obtained through the ``source_types_t`` + attribute. + +.. data:: Event.source_types_L + + The list of types supported by the drag source (from the ``%L`` substitution). + +.. data:: Event.source_types_ST + + The list of types supported by the drag source (from the ``%ST`` substitution). + +.. data:: Event.source_types_t + + The list of types supported by the drag source (from the ``%t`` substitution). + +.. data:: Event.target_types + + The list of types supported by the drop target. + +.. data:: Event.type + + The current type of the drag and drop operation. + +.. data:: Event.widget + + The widget that the event is delivered to. + +.. data:: Event.x + + The mouse pointer x coordinate, relative to the drop target widget. + +.. data:: Event.x_root + + The mouse pointer x coordinate, relative to the root window. + +.. data:: Event.y + + The mouse pointer y coordinate, relative to the drop target widget. + +.. data:: Event.y_root + + The mouse pointer y coordinate, relative to the root window. + +Examples +^^^^^^^^ + +Specifying Drop Targets +>>>>>>>>>>>>>>>>>>>>>>> + +Creating drop targets is easy: we have to only register a widget as a drop +target with the list of format types it can accept, and add a few bindings. +For example, a widget that accepts textual drops can be as follows: :: + + def dropenter(event): + event.widget.configure(bg='yellow') + return COPY + drop_target = Label(text='Text Drop Target!', bg='white') + drop_target.pack() + drop_target.drop_target_register(DND_TEXT) + drop_target.bind('<>', dropenter) + drop_target.bind('<>', lambda event: COPY) + drop_target.bind('<>', lambda event: event.widget.configure(bg='white')) + drop_target.bind('<>', lambda event: \ + event.widget.configure(text=event.data, bg='white')) + +From the above bindings, none is obligatory. However, we usually want to +receive dropped data (thus the ``<>`` event must be handled) and we want +to give visual feedback to the users through the ``<>`` and +``<>`` events. Finally, ``<>`` is only necessary if +we want to only accept drops on specific areas of the widget, or we want to +change the drop action according to the pressed modifiers. + +Now, if we want to also add the ability to receive file drops, we could add: :: + + def filedrop(event): + print(event.data) + event.widget.configure(bg='white') + drop_target.drop_target_register(DND_FILES) + drop_target.bind(DND_FILES_DROP, filedrop) + +Note that we have added a "specialised" drop binding, for the event +``DND_FILES_DROP``: this means that when a text portion is dropped over the +widget, the ``<>`` event binding will be executed. But when a list of +files is dropped onto the widget, the ``DND_FILES_DROP`` event binding will be +executed. If we proceed and define a binding for the ``DND_TEXT_DROP`` event, +the binding of the "general" ``<>`` event will never be executed. + +Specifying Drag Sources +>>>>>>>>>>>>>>>>>>>>>>> + +In order to specify a drag source, we need to register a widget as a drag +source: :: + + text_drag_source.drag_source_register() + +The above command defines a drag source with an empty type list (and which +will be declared in the ``<>`` event binding) and arranges mouse +bindings such as a drag will be started with the left mouse button. Then, it +is absolutely necessary to define a binding for the ``<>``: this +event binding must return the list of actions, the list of format types and +the actual data to be dropped: :: + + text_drag_source.bind('<>', lambda event: \ + ((COPY, MOVE), DND_TEXT, 'Hello from Tk!')) + +Please note that all specified format types must be compatible to each other, +as they all characterise the same data. + +Specifying Drag Sources With Multiple Data Types +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + +In the case the drag source wants to send a set of (incompatible) data types, +the result of the ``<>`` event binding must be slightly +different, as it must return two items (instead of three described +previously). + +The first element is again a list of allowable actions. However, the second +item is a list of "format type" and "data" pairs (each pair is **not** in +their own tuple/list): :: + + text_drag_source.bind('<>', lambda event: + ((COPY, MOVE), (DND_TEXT, 'Hello from Tk!', DND_FILES, '/tmp'))) + .. _tkinter-basic-mapping: Mapping Basic Tk into Tkinter diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index a3378d012fb41a..519a6ff3566f61 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -31,6 +31,7 @@ """ import enum +import os import sys import types @@ -48,6 +49,7 @@ WRITABLE = _tkinter.WRITABLE EXCEPTION = _tkinter.EXCEPTION +_DND_SUPPORT = {} _magic_re = re.compile(r'([\\{}])') _space_re = re.compile(r'([\s])', re.ASCII) @@ -765,6 +767,117 @@ def focus_lastfor(self): if name == 'none' or not name: return None return self._nametowidget(name) + def dnd_loaded(self): + """This function returns whether the TkDND library has been + successfully loaded for this Tk instance. + """ + return _DND_SUPPORT[self.tk] + + def _check_dnd(self): + """Checks if the interpreter has found the TkDND library and raises + a TclError if not (to protect methods which require TkDND). + """ + if not _DND_SUPPORT[self.tk]: + raise TclError("The TkDND library hasn't successfully loaded. If " + 'TkDND is already installed, try calling the ' + '"load_dnd" method with the install location.') + + def drop_target_register(self, types=()): + """This command will register the widget as a drop target (a widget + than can accept a drop action). An optional type-list can be provided, + which contains one or more types that the widget will accept during a + drop action. + + Note: This command can be executed multiple times on the same widget. + """ + self._check_dnd() + self.tk.call(('tkdnd::drop_target', 'register', self._w, types)) + + def drop_target_unregister(self): + """This command will stop the widget from being a drop target. Thus, it + will stop receiving events related to drop operations. + + Warning: This command will raise a TclError if the widget has not been + registered as a drop target. + """ + self._check_dnd() + self.tk.call(('tkdnd::drop_target', 'unregister', self._w)) + + def drag_source_register(self, types=(), mouse_buttons=1): + """This command will register the widget as a drag source. A drag + source is a widget than can start a drag action. When the widget is + registered as a drag source, an optional type-list can be provided. + This type list can contain one or more types that the widget will + provide during a drag action. However, this type list is + indicative/informative and the widget can initiate a drag action with a + type not in the list. + + Finally, mouse_buttons is one or more mouse buttons that can be used + for starting the drag action. It can have any of the values: + 1 - Left mouse button + 2 - Middle mouse button (wheel) + 3 - Right mouse button + + Multiple mouse buttons can be specified as a list/tuple of values. + + Note: This command can be executed multiple times on the same widget. + """ + self._check_dnd() + self.tk.call(('tkdnd::drag_source', 'register', + self._w, types, mouse_buttons)) + + def drag_source_unregister(self): + """This command will stop the widget from being a drag source. Thus, it + will stop receiving events related to drag operations. + + Warning: This command will raise a TclError if the widget has not been + registered as a drag source. + """ + self._check_dnd() + self.tk.call(('tkdnd::drag_source', 'unregister', self._w)) + + def platform_specific_types(self, types): + """This command will accept a list of types that can contain platform + independent or platform specific types. A new list will be returned, + where each platform independent type in type-list will be substituted + by one or more platform specific types. Thus, the returned list may + have more elements than type-list. + """ + self._check_dnd() + return self.tk.call(('tkdnd::platform_specific_types', types)) + + def platform_independent_types(self, types): + """This command will accept a list of types that can contain platform + independent or platform specific types. A new list will be returned, + where each platform specific type in type-list will be substituted by + one or more platform independent types. Thus, the returned list may + have more elements than type-list. + """ + self._check_dnd() + return self.tk.call(('tkdnd::platform_independent_types', types)) + + def get_drop_file_temp_directory(self): + """This command will return the temporary directory used by TkDND for + storing temporary files. When the package is loaded, this temporary + directory will be initialised to a proper directory according to the + operating system. + + The default initial value can be changed by the following environment + variables: + TKDND_TEMP_DIR + TEMP + TMP + """ + self._check_dnd() + return self.tk.call('tkdnd::GetDropFileTempDirectory') + + def set_drop_file_temp_directory(self, directory): + """This command will change the temporary directory used by TkDND for + storing temporary files to the given directory. + """ + self._check_dnd() + self.tk.call(('tkdnd::SetDropFileTempDirectory', directory)) + def tk_focusFollowsMouse(self): """The widget under mouse will get automatically focus. Can not be disabled easily.""" @@ -1332,7 +1445,8 @@ def _bind(self, what, sequence, func, add, needcleanup=1): elif func: funcid = self._register(func, self._substitute, needcleanup) - cmd = ('%sif {"[%s %s]" == "break"} break\n' + cmd = ('%sset dat [%s %s];if {"$dat" == "break"} {break} ' + 'elseif {"$dat" != ""} {set dndrtn $dat}\n' % (add and '+' or '', funcid, self._subst_format_str)) @@ -1532,9 +1646,12 @@ def _root(self): w = self while w.master: w = w.master return w - _subst_format = ('%#', '%b', '%f', '%h', '%k', - '%s', '%t', '%w', '%x', '%y', - '%A', '%E', '%K', '%N', '%W', '%T', '%X', '%Y', '%D') + _subst_format = ('%#', '{%a}', '{%b}', '{%c}', '%e', + '%f', '%h', '%k', '{%m}', '%s', + '{%t}', '%w', '%x', '%y', + '%A', '%C', '{%CST}', '{%CTT}', '{%D}', + '%E', '%K', '{%L}', '%N', '{%ST}', + '%T', '{%TT}', '%W', '%X', '%Y') _subst_format_str = " ".join(_subst_format) def _substitute(self, *args): @@ -1550,7 +1667,21 @@ def getint_event(s): except (ValueError, TclError): return s - nsign, b, f, h, k, s, t, w, x, y, A, E, K, N, W, T, X, Y, D = args + def getlist_event(s): + if s.startswith('{') and s.endswith('}'): + s = s[1:-1] + else: + return s, + s = s.translate((None, '\x00')) + if not s: + return '', + try: + return self.tk.splitlist(s) + except ValueError: + return s, + + nsign, a, b, c, ed, f, h, k, m, s, t, w, x, y, \ + A, C, CST, CTT, D, E, K, L, N, ST, T, TT, W, X, Y = args # Missing: (a, c, d, m, o, v, B, R) e = Event() # serial field: valid for all events @@ -1566,26 +1697,43 @@ def getint_event(s): # keysym as decimal: KeyPress and KeyRelease events only # x_root, y_root fields: ButtonPress, ButtonRelease, KeyPress, # KeyRelease, and Motion events - e.serial = getint(nsign) - e.num = getint_event(b) + try: + e.serial = getint(nsign) + except (ValueError, TclError): + e.serial = None + e.source_actions = getlist_event(a) + e.mouse_buttons = tuple(map(getint_event, getlist_event(b))) + e.num = e.mouse_buttons[0] + e.source_codes = tuple(map(getint_event, getlist_event(c))) + e.event_name = ed try: e.focus = getboolean(f) except TclError: pass e.height = getint_event(h) e.keycode = getint_event(k) + e.modifiers = getlist_event(m) e.state = getint_event(s) - e.time = getint_event(t) + e.source_types_t = getlist_event(t) + e.time = getint_event(e.source_types_t[0]) e.width = getint_event(w) e.x = getint_event(x) e.y = getint_event(y) e.char = A + e.action = e.char + e.code = getint_event(C) + e.common_source_types = getlist_event(CST) + e.common_target_types = getlist_event(CTT) + e.data = getlist_event(D) try: e.send_event = getboolean(E) except TclError: pass e.keysym = K + e.source_types_L = getlist_event(L) e.keysym_num = getint_event(N) + e.source_types_ST = getlist_event(ST) try: e.type = EventType(T) except ValueError: e.type = T + e.target_types = getlist_event(TT) try: e.widget = self._nametowidget(W) except KeyError: @@ -2261,6 +2409,7 @@ def __init__(self, screenName=None, baseName=None, className='Tk', baseName = baseName + ext interactive = False self.tk = _tkinter.create(screenName, baseName, className, interactive, wantobjects, useTk, sync, use) + _DND_SUPPORT[self.tk] = False if useTk: self._loadtk() if not sys.flags.ignore_environment: @@ -2298,6 +2447,34 @@ def _loadtk(self): _default_root = self self.protocol("WM_DELETE_WINDOW", self.destroy) + self.load_dnd() + + def load_dnd(self, dnd_path=None): + """This command will load the TkDND library and is called when Tk class + is instantiated. However, if the library is installed in a different + directory, this method will need to be called manually to load the + library. If the TkDND library has been found, True wil be returned, + otherwise False will be returned. + + Note: If the TkDND library has already been successfully loaded, True + will be returned and no action taken. + """ + if _DND_SUPPORT.get(self.tk, False): + return True + if not self._tkloaded: + return False + if dnd_path is not None: + # An absolute path must always be used + self.tk.call(('lappend', 'auto_path', os.path.abspath(dnd_path))) + try: + self.tk.call(('package', 'require', 'tkdnd')) + except TclError: + sup = False + else: + sup = True + _DND_SUPPORT[self.tk] = sup + return sup + def destroy(self): """Destroy this and all descendants widgets. This will end the application of this Tcl interpreter.""" diff --git a/Lib/tkinter/constants.py b/Lib/tkinter/constants.py index 63eee33d24d6c7..2be25f0df96b4e 100644 --- a/Lib/tkinter/constants.py +++ b/Lib/tkinter/constants.py @@ -108,3 +108,34 @@ SCROLL='scroll' UNITS='units' PAGES='pages' + +# DND constants +DND_ALL = '*' +DND_FILES = 'DND_Files' +DND_TEXT = 'DND_Text' +CF_UNICODETEXT = 'CF_UNICODETEXT' +CF_TEXT = 'CF_TEXT' +CF_HDROP = 'CF_HDROP' +TEXT_PLAIN_UNICODE = 'text/plain;charset=UTF-8' +TEXT_PLAIN = 'text/plain' +URI_LIST = 'text/uri-list' +NSSTRINGPBOARDTYPE = 'NSStringPboardType' +NSFILENAMESPBOARDTYPE = 'NSFilenamesPboardType' + +DND_FILES_DROP = '<>' +DND_TEXT_DROP = '<>' +CF_UNICODETEXT_DROP = '<>' +CF_TEXT_DROP = '<>' +CF_HDROP_DROP = '<>' +TEXT_PLAIN_UNICODE_DROP = '<>' +TEXT_PLAIN_DROP = '<>' +URI_LIST_DROP = '<>' +NSSTRINGPBOARDTYPE_DROP = '<>' +NSFILENAMESPBOARDTYPE_DROP = '<>' + +ASK = 'ask' +COPY = 'copy' +LINK = 'link' +MOVE = 'move' +PRIVATE = 'private' +REFUSE_DROP = 'refuse_drop' diff --git a/Lib/tkinter/dnd.py b/Lib/tkinter/dnd.py index 3120ff342f8c0e..867dfd93bf9ab8 100644 --- a/Lib/tkinter/dnd.py +++ b/Lib/tkinter/dnd.py @@ -100,6 +100,12 @@ """ import tkinter +import warnings + +warnings.warn("the tkinter.dnd module is deprecated in favour of the TkDND " + "bindings found in the main tkinter module (see the " + "documentation)", + DeprecationWarning, stacklevel=2) __all__ = ["dnd_start", "DndHandler"] diff --git a/PCbuild/get_externals.bat b/PCbuild/get_externals.bat index 4171fd740c7083..77e4767f256325 100644 --- a/PCbuild/get_externals.bat +++ b/PCbuild/get_externals.bat @@ -58,6 +58,7 @@ set libraries=%libraries% sqlite-3.31.1.0 if NOT "%IncludeTkinterSrc%"=="false" set libraries=%libraries% tcl-core-8.6.9.0 if NOT "%IncludeTkinterSrc%"=="false" set libraries=%libraries% tk-8.6.9.0 if NOT "%IncludeTkinterSrc%"=="false" set libraries=%libraries% tix-8.4.3.6 +if NOT "%IncludeTkinterSrc%"=="false" set libraries=%libraries% tkdnd-2.9.2 set libraries=%libraries% xz-5.2.2 set libraries=%libraries% zlib-1.2.11 @@ -79,6 +80,7 @@ set binaries= if NOT "%IncludeLibffi%"=="false" set binaries=%binaries% libffi if NOT "%IncludeSSL%"=="false" set binaries=%binaries% openssl-bin-1.1.1g if NOT "%IncludeTkinter%"=="false" set binaries=%binaries% tcltk-8.6.9.0 +if NOT "%IncludeTkinter%"=="false" set binaries=%binaries% tkdnd-bin-2.9.2 if NOT "%IncludeSSLSrc%"=="false" set binaries=%binaries% nasm-2.11.06 for %%b in (%binaries%) do ( diff --git a/PCbuild/tcltk.props b/PCbuild/tcltk.props index 7fcd3e1c618c46..be8c7d49cb3bbb 100644 --- a/PCbuild/tcltk.props +++ b/PCbuild/tcltk.props @@ -14,10 +14,15 @@ 4 3 6 + 2 + 9 + 2 $(ExternalsDir)tcl-core-$(TclMajorVersion).$(TclMinorVersion).$(TclPatchLevel).$(TclRevision)\ $(ExternalsDir)tk-$(TkMajorVersion).$(TkMinorVersion).$(TkPatchLevel).$(TkRevision)\ $(ExternalsDir)tix-$(TixMajorVersion).$(TixMinorVersion).$(TixPatchLevel).$(TixRevision)\ $(ExternalsDir)tcltk-$(TclMajorVersion).$(TclMinorVersion).$(TclPatchLevel).$(TclRevision)\$(ArchName)\ + $(ExternalsDir)tkdnd-bin-$(TkdndMajorVersion).$(TkdndMinorVersion).$(TkdndPatchLevel)\$(ArchName)\ + tkdnd-$(TkdndMajorVersion).$(TkdndMinorVersion).$(TkdndPatchLevel) tcl$(TclMajorVersion)$(TclMinorVersion)t$(TclDebugExt).dll tcl$(TclMajorVersion)$(TclMinorVersion)t$(TclDebugExt).lib diff --git a/Tools/msi/tcltk/tcltk.wixproj b/Tools/msi/tcltk/tcltk.wixproj index 218f3d15ec88fc..249afbce8fa3b6 100644 --- a/Tools/msi/tcltk/tcltk.wixproj +++ b/Tools/msi/tcltk/tcltk.wixproj @@ -28,6 +28,14 @@ tcltk_lib + + $(tkdndDir) + $(tkdndDir) + $(tkdndDir) + tcl\$(tkdndDest)\ + tcltk_lib + + $(PySourcePath) From 46995aad6b6243c33f0cadf261b5895435069661 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Sat, 13 Jun 2020 19:54:41 +0100 Subject: [PATCH 02/17] Added news item --- .../NEWS.d/next/Library/2020-06-13-18-07-23.bpo-40893.3IK20U.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-06-13-18-07-23.bpo-40893.3IK20U.rst diff --git a/Misc/NEWS.d/next/Library/2020-06-13-18-07-23.bpo-40893.3IK20U.rst b/Misc/NEWS.d/next/Library/2020-06-13-18-07-23.bpo-40893.3IK20U.rst new file mode 100644 index 00000000000000..06706b6481d39c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-06-13-18-07-23.bpo-40893.3IK20U.rst @@ -0,0 +1 @@ +Added TkDND bindings to the main :mod:`tkinter` module From 986796aeca7324a82b19c51c929d2bcf59afa1b4 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Tue, 16 Jun 2020 16:45:47 +0200 Subject: [PATCH 03/17] Changed doc to fix build bots --- Doc/library/tkinter.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index e112a6614a3e44..cea70728c628c8 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -719,7 +719,7 @@ Event Data .. data:: Event.event_name The name of the current virtual event. One of ``<>``, - ``<>``, ``<>``, ``<>``, ``<>``, + ``<>``, ``<>``, ``*_DROP``, ``<>``, ``<>`` and ``<>``. .. data:: Event.modifiers @@ -1438,4 +1438,4 @@ use raw reads or ``os.read(file.fileno(), maxbytecount)``. WRITABLE EXCEPTION - Constants used in the *mask* arguments. \ No newline at end of file + Constants used in the *mask* arguments. From 8f357fed47ef1a2160f4da0a3cba90473916c15c Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Tue, 23 Jun 2020 12:04:23 +0100 Subject: [PATCH 04/17] Updated to use return properly The `return` command is now used to get the binding returns to TkDND rather than the `set` command (for details on `-level 0` see https://www.tcl.tk/man/tcl8.6/TclCmd/return.htm#M18) --- Lib/tkinter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 519a6ff3566f61..de87205b947162 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1446,7 +1446,7 @@ def _bind(self, what, sequence, func, add, needcleanup=1): funcid = self._register(func, self._substitute, needcleanup) cmd = ('%sset dat [%s %s];if {"$dat" == "break"} {break} ' - 'elseif {"$dat" != ""} {set dndrtn $dat}\n' + 'elseif {"$dat" != ""} {return -level 0 $dat}\n' % (add and '+' or '', funcid, self._subst_format_str)) From 8fbd233ba277e12a74a55401b76375af7391888d Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Thu, 25 Jun 2020 12:55:53 +0200 Subject: [PATCH 05/17] Added support for more types --- Doc/library/tkinter.rst | 212 +++++++++++++++++++-------------------- Lib/tkinter/__init__.py | 4 +- Lib/tkinter/constants.py | 50 +++++++-- 3 files changed, 143 insertions(+), 123 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index cea70728c628c8..a4f521b7fbf929 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -132,7 +132,7 @@ Other modules that provide Tk support include: :mod:`tkinter.dnd` Deprecated drag-and-drop support for :mod:`tkinter` - (see the :ref:`tkinter-tkdnd` section) + (see the :ref:`tkinter-tkdnd` section for details on the new bindings) :mod:`turtle` Turtle graphics in a Tk window. @@ -311,16 +311,20 @@ TkDND Support ------------- :mod:`tkinter` has support for a native, platform specific drag and drop -mechanism, through `Tkdnd `_, with the user +mechanism, through `TkDND `_, with the user able to register widgets as drag sources or drop targets. -On Windows, TkDND is packaged with -Tcl/Tk so should work "out-of-the-box" with a standard installation. On -other platforms, it will need to be installed seperately. +On Windows and MacOS, TkDND is packaged with Tcl/Tk so should work "out-of-the-box" +with a standard installation. On other platforms, it will need to be installed +separately. .. note:: As a result of this functionality, the :mod:`tkinter.dnd` module is now deprecated. +.. warning:: These bindings were written for TkDND 2.9 and, at the time of + writing, many third-party distributers are still using 2.6 meaning some + functionality described here is not guaranteed. + .. versionadded:: 3.10 Functions @@ -331,7 +335,7 @@ Functions This command will load the TkDND library and is called when ``Tk`` class is instantiated. However, if the library is installed in a different directory, this method will need to be called manually to load the library. - If the TkDND library has been found, ``True`` wil be returned, otherwise + If the TkDND library has been found, ``True`` will be returned, otherwise ``False`` will be returned. If the TkDND library has already been successfully loaded, ``True`` will be @@ -402,6 +406,9 @@ Functions each platform specific type in type-list will be substituted by one or more platform independent types. Thus, the returned list may have more elements than type-list. + + .. warning:: In many TkDND versions, there is a bug that causes this method + to raise a :class:`TclError`, so it should generally be avoided. .. function:: Misc.get_drop_file_temp_directory() @@ -430,113 +437,96 @@ Unfortunately, each protocol defines its own, usually platform specific, types. TkDND, trying to maintain portability among different platforms, offers some predefined types for basic kinds of data, like text and filenames. -Platform Independent Types ->>>>>>>>>>>>>>>>>>>>>>>>>> +Generic data formats +>>>>>>>>>>>>>>>>>>>> + +.. note:: Not all of the following types are cross-platform and are instead a + generic type that then defines support for more specific types -Currently, the following predefined cross-platform values are available: +Currently, the following predefined generic type-values are available: .. data:: DND_ALL All supported types for the platform. - .. warning:: ``DND_ALL`` can declare support for unexpected formats. For - example, it can declare special formats to receive text not declared by - ``DND_TEXT`` (such as RTF and HTML). While it appears to be able to receive - these types, support outside of the formats found as constants is not - guaranteed. - -.. data:: DND_TEXT - - This type can be used for transferring textual data. Internally, it is - translated to the following platform specific formats: - - +----------+------------------------------+ - | Platform | Type/s | - +==========+==============================+ - | Windows | .. data:: CF_UNICODETEXT | - | | CF_TEXT | - | | :noindex: | - +----------+------------------------------+ - | Unix | .. data:: TEXT_PLAIN_UNICODE | - | | TEXT_PLAIN | - | | :noindex: | - +----------+------------------------------+ - | Mac | .. data:: NSSTRINGPBOARDTYPE | - | | :noindex: | - +----------+------------------------------+ - -.. data:: DND_FILES - - This type can be used for transferring a list of filepaths/URLs. - Internally, it is translated to the following platform specific - formats: - - +----------+---------------------------------+ - | Platform | Type/s | - +==========+=================================+ - | Windows | .. data:: CF_HDROP | - | | :noindex: | - +----------+---------------------------------+ - | Unix | .. data:: URI_LIST | - | | :noindex: | - +----------+---------------------------------+ - | Mac | .. data:: NSFILENAMESPBOARDTYPE | - | | :noindex: | - +----------+---------------------------------+ - -Platform Specific Types ->>>>>>>>>>>>>>>>>>>>>>> - -Additionally to the platform independent types, TkDND supports the following -platform specific types (though it is often better to use an independent type -where possible): - -**Windows:** + .. warning:: ``DND_ALL`` can declare support for unexpected formats and + support outside of the formats found as constants is not guaranteed. - .. data:: CF_UNICODETEXT +.. data:: DND_COLOR - Text transfer encoded in Unicode. + This type can be used for transferring colour data. - .. data:: CF_TEXT +.. data:: DND_COLOUR - Text transfer with application dependent encoding. If the source has - specified an encoding it is used, else the system encoding is used for - the conversion. + Same as ``DND_COLOR``. - .. data:: CF_HDROP - - Filepath/URL transfer encoded in UTF-8. - -**Unix:** - - .. data:: TEXT_PLAIN_UNICODE - - Text transfer encoded in Unicode. - - .. data:: TEXT_PLAIN - - Text transfer with application dependent encoding. If the source has - specified an encoding it is used, else the system encoding is used for - the conversion. - - .. data:: URI_LIST - - Filepath/URL transfer encoded in ASCII. +.. data:: DND_FILES -**Mac:** + This type can be used for transferring a list of filepaths. On some + platforms it also supports URLs, though on Windows this needs to be + specified using the separate ``DND_URL``. - .. data:: NSSTRINGPBOARDTYPE +.. data:: DND_HTML - Text transfer encoded using the system encoding. + This type can be used for transferring HTML textual data. - .. data:: NSFILENAMESPBOARDTYPE +.. data:: DND_RTF - Filepath/URL transfer encoded using the system encoding. + This type can be used for transferring RTF textual data. +.. data:: DND_TEXT -Finally, format types used for drop types can have wildcards, following the -same rules as "string match". For example, registering a drop target with the -type ``'*'`` (``DND_ALL``), will accept any drop, no matter what the drop -format is. + This type can be used for transferring textual data. + +.. data:: DND_URL + + This type can be used for transferring URL textual data. + +Specific data formats +>>>>>>>>>>>>>>>>>>>>> + +Internally, these types are translated to more specific types (as shown +below): + +.. note:: In almost all circumstances, the generic types should be used. The + specific types should be used when a particular internal behaviour is + required of the operating system. + ++----------------------+----------+----------------------------------+ +| Generic type | Platform | Specific type/s | ++======================+==========+==================================+ +| .. data:: DND_COLOR | Unix | .. data:: APPLICATION_X_COLOR | +| DND_COLOUR | | | +| :noindex: | | | ++----------------------+----------+----------------------------------+ +| .. data:: DND_FILES | Windows | .. data:: CF_HDROP | +| :noindex: +----------+----------------------------------+ +| | Unix | .. data:: URI_LIST | +| +----------+----------------------------------+ +| | Mac | .. data:: NSFILENAMESPBOARDTYPE | ++----------------------+----------+----------------------------------+ +| .. data:: DND_HTML | Windows | .. data:: CF_HTML | +| :noindex: | | HTML_FORMAT | +| +----------+----------------------------------+ +| | Unix | .. data:: TEXT_HTML | +| | | TEXT_HTML_UNICODE | +| +----------+----------------------------------+ +| | Mac | .. data:: NSPASTEBOARDTYPEHTML | ++----------------------+----------+----------------------------------+ +| .. data:: DND_RTF | Windows | .. data:: CF_RTF | +| :noindex: | | CF_RTFTEXT | +| | | RICH_TEXT_FORMAT | ++----------------------+----------+----------------------------------+ +| .. data:: DND_TEXT | Windows | .. data:: CF_TEXT | +| :noindex: | | CF_UNICODETEXT | +| +----------+----------------------------------+ +| | Unix | .. data:: TEXT_PLAIN | +| | | TEXT_PLAIN_UNICODE | +| +----------+----------------------------------+ +| | Mac | .. data:: NSSTRINGPBOARDTYPE | ++----------------------+----------+----------------------------------+ +| .. data:: DND_URL | Windows | .. data:: UNIFORMRESOURCELOCATOR | +| :noindex: | | | ++----------------------+----------+----------------------------------+ Supported Actions >>>>>>>>>>>>>>>>> @@ -546,7 +536,7 @@ that should take place. .. data:: ASK - A dialog will be displayed to the user, in order to select an action. + A dialog will be displayed to the user for them to select an action. .. data:: COPY @@ -577,7 +567,7 @@ are expected to have bindings for some of these events. Some events are mandatory (in the sense that a drag or drop operation can be stopped if the bindings do not exist), while others are not. -In the following two sections all virtual events defined by the TkDND package +In the following two sections, all virtual events defined by the TkDND package are presented. .. note:: It is a good practice to define bindings for all events, so that the @@ -590,7 +580,7 @@ are presented. Drop Target Events >>>>>>>>>>>>>>>>>> -A widget registered as a drop target, can have bindings for the following +A widget registered as a drop target can have bindings for the following virtual events: ``<>`` @@ -602,14 +592,14 @@ virtual events: the drop action. .. note:: This event is not mandatory, but if it is defined, it has to - return an action (otherwise the drop is refused). + return an action (otherwise the drop could be refused). ``<>`` This event is triggered when the mouse moves inside the widget during a drop action (similar to the normal ```` event). The intention of this event is to let widget decide if it will accept the drop and the - action of the drop, if a drop is going to happen at the specific mouse + action of the drop if a drop is going to happen at the specific mouse coordinates. Thus, the script binding for such an event can get the mouse coordinates @@ -617,7 +607,7 @@ virtual events: expected to return the drop action. .. note:: This event is not mandatory, but if it is defined, it has to - return an action (otherwise the drop is refused). + return an action (otherwise the drop could be refused). ``<>`` @@ -626,7 +616,7 @@ virtual events: to restore the visual state of the widget to normal (i.e. the visual state the widget was in before the ``<>`` event was triggered). - This event is not mandatory, and is not expected to return a value. + This event is not mandatory and is not expected to return a value. ``<>`` @@ -636,7 +626,7 @@ virtual events: performed to the data. .. note:: This event is not mandatory, but if it is defined, it has to - return an action (otherwise the drop is refused). + return an action (otherwise the drop could be refused). ``*_DROP`` @@ -655,13 +645,13 @@ virtual events: performed to the data. .. note:: This event is not mandatory, but if it is defined, it has to - return an action (otherwise the drop is refused). + return an action (otherwise the drop could be refused). Drag Source Events >>>>>>>>>>>>>>>>>> -A widget registered as a drag source, is expected to have bindings for the -following virtual events: +A widget registered as a drag source can have bindings for the following +virtual events: ``<>`` @@ -684,7 +674,7 @@ following virtual events: drop was successful or not). Its main purpose is to process the dropped data according to the drop action returned by the drop target. - This event is not mandatory, and is not expected to return a value. + This event is not mandatory and is not expected to return a value. Event Data >>>>>>>>>> @@ -739,7 +729,7 @@ Event Data action, not the drop target. The drop target is expected to either accept the action selected by the drag source (and return it back), or select ``COPY``, ``DEFAULT``, or ``ASK``. So, under Unix examining the pressed - modifier keys fulfills only informative purposes. + modifier keys fulfils only informative purposes. .. data:: Event.mouse_buttons @@ -808,8 +798,8 @@ Examples Specifying Drop Targets >>>>>>>>>>>>>>>>>>>>>>> -Creating drop targets is easy: we have to only register a widget as a drop -target with the list of format types it can accept, and add a few bindings. +Creating drop targets is easy: we only have to register a widget as a drop +target with the list of format types it can accept and add a few bindings. For example, a widget that accepts textual drops can be as follows: :: def dropenter(event): diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index de87205b947162..9b2a06f2150420 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -844,7 +844,7 @@ def platform_specific_types(self, types): have more elements than type-list. """ self._check_dnd() - return self.tk.call(('tkdnd::platform_specific_types', types)) + return self.tk.splitlist(self.tk.call(('tkdnd::platform_specific_types', types))) def platform_independent_types(self, types): """This command will accept a list of types that can contain platform @@ -854,7 +854,7 @@ def platform_independent_types(self, types): have more elements than type-list. """ self._check_dnd() - return self.tk.call(('tkdnd::platform_independent_types', types)) + return self.tk.splitlist(self.tk.call(('tkdnd::platform_independent_types', types))) def get_drop_file_temp_directory(self): """This command will return the temporary directory used by TkDND for diff --git a/Lib/tkinter/constants.py b/Lib/tkinter/constants.py index 2be25f0df96b4e..7330d2772ef554 100644 --- a/Lib/tkinter/constants.py +++ b/Lib/tkinter/constants.py @@ -111,8 +111,23 @@ # DND constants DND_ALL = '*' +DND_COLOR = 'DND_Color' +DND_COLOUR = DND_COLOR DND_FILES = 'DND_Files' +DND_HTML = 'DND_HTML' +DND_RTF = 'DND_RTF' DND_TEXT = 'DND_Text' +DND_URL = 'DND_URL' +APPLICATION_X_COLOR = 'application/x-color' +CF_HTML = 'CF_HTML' +HTML_FORMAT = 'HTML Format' +TEXT_HTML = 'text/html' +TEXT_HTML_UNICODE = 'text/html\;charset=utf-8' +NSPASTEBOARDTYPEHTML = 'NSPasteboardTypeHTML' +CF_RTF = 'CF_RTF' +CF_RTFTEXT = 'CF_RTFTEXT' +RICH_TEXT_FORMAT = 'Rich Text Format' +UNIFORMRESOURCELOCATOR = 'UniformResourceLocator' CF_UNICODETEXT = 'CF_UNICODETEXT' CF_TEXT = 'CF_TEXT' CF_HDROP = 'CF_HDROP' @@ -122,16 +137,31 @@ NSSTRINGPBOARDTYPE = 'NSStringPboardType' NSFILENAMESPBOARDTYPE = 'NSFilenamesPboardType' -DND_FILES_DROP = '<>' -DND_TEXT_DROP = '<>' -CF_UNICODETEXT_DROP = '<>' -CF_TEXT_DROP = '<>' -CF_HDROP_DROP = '<>' -TEXT_PLAIN_UNICODE_DROP = '<>' -TEXT_PLAIN_DROP = '<>' -URI_LIST_DROP = '<>' -NSSTRINGPBOARDTYPE_DROP = '<>' -NSFILENAMESPBOARDTYPE_DROP = '<>' +DND_COLOR_DROP = f'<>' +DND_COLOUR_DROP = DND_COLOR_DROP +DND_FILES_DROP = f'<>' +DND_HTML_DROP = f'<>' +DND_RTF_DROP = f'<>' +DND_TEXT_DROP = f'<>' +DND_URL_DROP = f'<>' +APPLICATION_X_COLOR_DROP = f'<>' +CF_HTML_DROP = f'<>' +HTML_FORMAT_DROP = f'<>' +TEXT_HTML_DROP = f'<>' +TEXT_HTML_UNICODE_DROP = f'<>' +NSPASTEBOARDTYPEHTML_DROP = f'<>' +CF_RTF_DROP = f'<>' +CF_RTFTEXT_DROP = f'<>' +RICH_TEXT_FORMAT_DROP = f'<>' +UNIFORMRESOURCELOCATOR_DROP = f'<>' +CF_UNICODETEXT_DROP = f'<>' +CF_TEXT_DROP = f'<>' +CF_HDROP_DROP = f'<>' +TEXT_PLAIN_UNICODE_DROP = f'<>' +TEXT_PLAIN_DROP = f'<>' +URI_LIST_DROP = f'<>' +NSSTRINGPBOARDTYPE_DROP = f'<>' +NSFILENAMESPBOARDTYPE_DROP = f'<>' ASK = 'ask' COPY = 'copy' From a9d5751e56183ebcd0b19ff9d0f0f3f50b693dd5 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Thu, 25 Jun 2020 14:09:41 +0200 Subject: [PATCH 06/17] Fixed whitespace --- Doc/library/tkinter.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index a4f521b7fbf929..11d0686565ee93 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -406,7 +406,7 @@ Functions each platform specific type in type-list will be substituted by one or more platform independent types. Thus, the returned list may have more elements than type-list. - + .. warning:: In many TkDND versions, there is a bug that causes this method to raise a :class:`TclError`, so it should generally be avoided. @@ -473,6 +473,7 @@ Currently, the following predefined generic type-values are available: .. data:: DND_RTF This type can be used for transferring RTF textual data. + .. data:: DND_TEXT This type can be used for transferring textual data. From ff753b4e04371068204ec414bb87239ae518f23d Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Thu, 25 Jun 2020 16:57:24 +0100 Subject: [PATCH 07/17] Removed final newline in tkinter.rst From 4caf8bc2c24d7ecbf6d12bbbf69ddaaea7cea8b7 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Thu, 25 Jun 2020 17:35:22 +0100 Subject: [PATCH 08/17] Fixed TEXT_PLAIN_UNICODE value --- Lib/tkinter/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/tkinter/constants.py b/Lib/tkinter/constants.py index 7330d2772ef554..62b54cc7c7b7e1 100644 --- a/Lib/tkinter/constants.py +++ b/Lib/tkinter/constants.py @@ -131,7 +131,7 @@ CF_UNICODETEXT = 'CF_UNICODETEXT' CF_TEXT = 'CF_TEXT' CF_HDROP = 'CF_HDROP' -TEXT_PLAIN_UNICODE = 'text/plain;charset=UTF-8' +TEXT_PLAIN_UNICODE = 'text/plain;charset=utf-8' TEXT_PLAIN = 'text/plain' URI_LIST = 'text/uri-list' NSSTRINGPBOARDTYPE = 'NSStringPboardType' From fc7818f885bdaacba4a992190d34f85dc62792a2 Mon Sep 17 00:00:00 2001 From: "E. Paine" Date: Thu, 25 Jun 2020 18:26:45 +0100 Subject: [PATCH 09/17] Added the "DEFAULT" constant --- Doc/library/tkinter.rst | 4 ++++ Lib/tkinter/constants.py | 1 + 2 files changed, 5 insertions(+) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 11d0686565ee93..abfef9da709d38 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -543,6 +543,10 @@ that should take place. The data will be copied. +.. data:: DEFAULT + + Select the preferred action of the drag source. + .. data:: LINK The data will be linked. diff --git a/Lib/tkinter/constants.py b/Lib/tkinter/constants.py index 62b54cc7c7b7e1..02fe27a1487fef 100644 --- a/Lib/tkinter/constants.py +++ b/Lib/tkinter/constants.py @@ -165,6 +165,7 @@ ASK = 'ask' COPY = 'copy' +DEFAULT = 'default' LINK = 'link' MOVE = 'move' PRIVATE = 'private' From 4eaf1f3aee58066d3e4315e649053a9bd668ca38 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Thu, 25 Jun 2020 20:53:37 +0100 Subject: [PATCH 10/17] Fixed "TEXT_HTML_UNICODE" value --- Lib/tkinter/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/tkinter/constants.py b/Lib/tkinter/constants.py index 02fe27a1487fef..7d5618d346c09e 100644 --- a/Lib/tkinter/constants.py +++ b/Lib/tkinter/constants.py @@ -122,7 +122,7 @@ CF_HTML = 'CF_HTML' HTML_FORMAT = 'HTML Format' TEXT_HTML = 'text/html' -TEXT_HTML_UNICODE = 'text/html\;charset=utf-8' +TEXT_HTML_UNICODE = 'text/html;charset=utf-8' NSPASTEBOARDTYPEHTML = 'NSPasteboardTypeHTML' CF_RTF = 'CF_RTF' CF_RTFTEXT = 'CF_RTFTEXT' From 5022ce8cd5e355704b8d539f0b5b9418f34e39fd Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Thu, 25 Jun 2020 22:07:17 +0200 Subject: [PATCH 11/17] Added new TkDND tests Added a few tests for the new bindings --- Lib/tkinter/test/test_tkinter/test_tkdnd.py | 136 ++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 Lib/tkinter/test/test_tkinter/test_tkdnd.py diff --git a/Lib/tkinter/test/test_tkinter/test_tkdnd.py b/Lib/tkinter/test/test_tkinter/test_tkdnd.py new file mode 100644 index 00000000000000..f583feb2f22a8f --- /dev/null +++ b/Lib/tkinter/test/test_tkinter/test_tkdnd.py @@ -0,0 +1,136 @@ +import tempfile +import _thread +import tkinter as tk +import unittest +from tkinter.test.support import AbstractTkTest + +class BaseDndTest(AbstractTkTest, unittest.TestCase): + + @classmethod + def setUpClass(cls): + sup = super(BaseDndTest, cls) + sup.setUpClass() + if not cls.root.dnd_loaded(): + sup.skipTest(cls, "TkDND installation not found") + +class DropTargetTest(BaseDndTest): + + def test_register(self): + self.root.drop_target_register() + self.root.drop_target_register(tk.DND_ALL) + self.root.drop_target_register((tk.DND_TEXT, tk.DND_FILES)) + + def test_unregister(self): + self.root.drop_target_unregister() + + def test_bind(self): + # Being virtual events, they should not raise an exception + # even if undefined (unlike a regular event) + self.root.bind("<>", lambda event: event.action) + self.root.event_generate("<>") + self.root.bind(tk.DND_FILES_DROP, lambda event: event.action) + self.root.bind("<>", lambda event: event.action) + self.root.bind("<>", lambda event: event.action) + self.root.bind("<>", lambda event: event.action) + + def test_unbind(self): + def fail_test(event): + nonlocal failed + failed = True + self.root.bind("<>", fail_test) + self.root.unbind("<>") + failed = False + self.root.event_generate("<>") + if failed: + raise RuntimeError() + +class DragSourceTest(BaseDndTest): + + def test_register(self): + self.root.drag_source_register() + self.root.drag_source_register(tk.DND_ALL, 3) + self.root.drag_source_register((tk.DND_TEXT, tk.DND_FILES), (1, 3)) + + def test_unregister(self): + self.root.drag_source_unregister() + + def test_bind(self): + self.root.bind("<>", lambda event: ((tk.COPY, tk.MOVE), + tk.DND_TEXT, + 'Hello from Tk!')) + self.root.event_generate("<>") + self.root.bind("<>", lambda event: event.action) + + def test_unbind(self): + def fail_test(event): + nonlocal failed + failed = True + self.root.bind("<>", fail_test) + self.root.unbind("<>") + failed = False + self.root.event_generate("<>") + if failed: + raise RuntimeError() + +class MiscTest(BaseDndTest): + + def test_loaded(self): + self.assertEqual(self.root.load_dnd(), True) + + def test_specific(self): + self.root.platform_specific_types(tk.DND_ALL) + self.root.platform_specific_types(tk.DND_TEXT) + self.root.platform_specific_types(tk.DND_FILES) + for tps in ((tk.CF_UNICODETEXT, tk.CF_TEXT), # Windows + (tk.TEXT_PLAIN_UNICODE, tk.TEXT_PLAIN), # Unix + (tk.NSSTRINGPBOARDTYPE,)): # MacOS + self.assertEqual(self.root.platform_specific_types(tps), tps) + + def test_independent(self): + try: + self.root.platform_independent_types(tk.DND_ALL) + except tk.TclError: + # tkdnd has a bug which causes this method to fail in many versions + self.skipTest("Possible TkDND issue ignored") + # If the previous call succeeded, so should these + self.assertEqual(self.root.platform_independent_types(tk.DND_TEXT), + (tk.DND_TEXT,)) + self.assertEqual(self.root.platform_independent_types(tk.DND_FILES), + (tk.DND_FILES,)) + self.assertTrue(tk.DND_TEXT in \ + self.root.platform_independent_types((tk.CF_TEXT, + tk.TEXT_PLAIN, + tk.NSSTRINGPBOARDTYPE))) + self.assertTrue(tk.DND_TEXT in \ + self.root.platform_independent_types((tk.CF_UNICODETEXT, + tk.TEXT_PLAIN_UNICODE, + tk.NSSTRINGPBOARDTYPE))) + self.assertTrue(tk.DND_FILES in \ + self.root.platform_independent_types((tk.CF_HDROP, + tk.URI_LIST, + tk.NSFILENAMESPBOARDTYPE))) + self.assertTrue(tk.DND_HTML in \ + self.root.platform_independent_types((tk.CF_HTML, + tk.TEXT_HTML, + tk.NSPASTEBOARDTYPEHTML))) + self.assertTrue(tk.DND_HTML in \ + self.root.platform_independent_types((tk.HTML_FORMAT, + tk.TEXT_HTML_UNICODE, + tk.NSPASTEBOARDTYPEHTML))) + + def test_tempdir_get(self): + self.root.get_drop_file_temp_directory() + + def test_tempdir_set(self): + with tempfile.TemporaryDirectory() as di: + self.root.set_drop_file_temp_directory(di) + self.assertEqual(self.root.get_drop_file_temp_directory(), di) + +tests_gui = ( + DropTargetTest, + DragSourceTest, + MiscTest +) + +if __name__ == "__main__": + unittest.main() From fb3337ef5ec6e5010576c9d5e353071a23f5be3b Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Thu, 25 Jun 2020 21:09:14 +0100 Subject: [PATCH 12/17] Remove unused import Removed an import introduced early in development (which I forgot to remove) --- Lib/tkinter/test/test_tkinter/test_tkdnd.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/tkinter/test/test_tkinter/test_tkdnd.py b/Lib/tkinter/test/test_tkinter/test_tkdnd.py index f583feb2f22a8f..d0188d15d951b8 100644 --- a/Lib/tkinter/test/test_tkinter/test_tkdnd.py +++ b/Lib/tkinter/test/test_tkinter/test_tkdnd.py @@ -1,5 +1,4 @@ import tempfile -import _thread import tkinter as tk import unittest from tkinter.test.support import AbstractTkTest From 3aa441a9f435601f8a8d4da265ff8c9f7ccf2b70 Mon Sep 17 00:00:00 2001 From: "E. Paine" Date: Fri, 26 Jun 2020 10:05:42 +0100 Subject: [PATCH 13/17] Drafted whatsnew and changed ACKS --- Doc/whatsnew/3.10.rst | 5 +++++ Misc/ACKS | 1 + 2 files changed, 6 insertions(+) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 629909b79e2aa0..a2ef0a7681ef4c 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -49,6 +49,8 @@ This article explains the new features in Python 3.10, compared to 3.9. For full details, see the :ref:`changelog `. +.. note:: XXX TkDND is a new optional dependecy for Python 3.10. + .. note:: Prerelease users should be aware that this document is currently in draft @@ -100,6 +102,9 @@ New Modules Improved Modules ================ +* XXX brief summary of new TkDND bindings in tkinter module + (Contributed by Elisha Paine in :issue:`40893`.) + Optimizations ============= diff --git a/Misc/ACKS b/Misc/ACKS index a505a3d7840369..7ab93f0716f4f1 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1265,6 +1265,7 @@ Richard Oudkerk Russel Owen Joonas Paalasmaa Martin Packman +Elisha Paine Shriphani Palakodety Julien Palard Aviv Palivoda From 074b3951eb04a454839df947d8d125c9223bd280 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Fri, 26 Jun 2020 10:09:17 +0100 Subject: [PATCH 14/17] Fixed whatnew typo --- Doc/whatsnew/3.10.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index a2ef0a7681ef4c..17bc12576056f7 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -49,7 +49,7 @@ This article explains the new features in Python 3.10, compared to 3.9. For full details, see the :ref:`changelog `. -.. note:: XXX TkDND is a new optional dependecy for Python 3.10. +.. note:: XXX TkDND is a new optional dependency for Python 3.10. .. note:: From 637230f5186f2ca70304ef3c3b2c84772d8679e1 Mon Sep 17 00:00:00 2001 From: E-Paine <63801254+E-Paine@users.noreply.github.com> Date: Fri, 26 Jun 2020 13:54:06 +0100 Subject: [PATCH 15/17] Updated platform_independent_types warnings --- Doc/library/tkinter.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index abfef9da709d38..689a24cf96f7e3 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -408,7 +408,7 @@ Functions than type-list. .. warning:: In many TkDND versions, there is a bug that causes this method - to raise a :class:`TclError`, so it should generally be avoided. + to raise a :class:`TclError`, so it should be used with caution. .. function:: Misc.get_drop_file_temp_directory() From 33a620397f8600f98a9aa23a4bc6db2081c7fc25 Mon Sep 17 00:00:00 2001 From: "E. Paine" <63801254+E-Paine@users.noreply.github.com> Date: Fri, 26 Jun 2020 17:33:22 +0100 Subject: [PATCH 16/17] Added "TkdndVersion" variable --- Doc/library/tkinter.rst | 10 ++++++++++ Lib/tkinter/__init__.py | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 689a24cf96f7e3..9975233641cc5c 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -563,6 +563,16 @@ that should take place. A drop cannot occur. +TkDND Version +>>>>>>>>>>>>> + +.. data:: TkdndVersion + + This variable contains the TkDND version loaded as a float (like + ``TclVersion`` and ``TkVersion``), but only once TkDND has been successfully + loaded. Before TkDND has been loaded (while ``dnd_loaded()`` returns + ``False``), its value is ``None``. + Events ^^^^^^ diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 9b2a06f2150420..1e523a72ac00c3 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -44,6 +44,7 @@ TkVersion = float(_tkinter.TK_VERSION) TclVersion = float(_tkinter.TCL_VERSION) +TkdndVersion = None READABLE = _tkinter.READABLE WRITABLE = _tkinter.WRITABLE @@ -2450,6 +2451,7 @@ def _loadtk(self): self.load_dnd() def load_dnd(self, dnd_path=None): + global TkdndVersion """This command will load the TkDND library and is called when Tk class is instantiated. However, if the library is installed in a different directory, this method will need to be called manually to load the @@ -2467,7 +2469,7 @@ def load_dnd(self, dnd_path=None): # An absolute path must always be used self.tk.call(('lappend', 'auto_path', os.path.abspath(dnd_path))) try: - self.tk.call(('package', 'require', 'tkdnd')) + TkdndVersion = float(self.tk.call(('package', 'require', 'tkdnd'))) except TclError: sup = False else: From d71cef127adaff1bab2f2d64b8f302d9a62fec07 Mon Sep 17 00:00:00 2001 From: "E. Paine" <63801254+E-Paine@users.noreply.github.com> Date: Tue, 30 Jun 2020 17:47:57 +0100 Subject: [PATCH 17/17] Added data_raw and data_split event attributes --- Doc/library/tkinter.rst | 18 ++++++++++++++---- Lib/tkinter/__init__.py | 15 ++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Doc/library/tkinter.rst b/Doc/library/tkinter.rst index 9975233641cc5c..b4cd966d763cce 100644 --- a/Doc/library/tkinter.rst +++ b/Doc/library/tkinter.rst @@ -714,12 +714,22 @@ Event Data .. data:: Event.data - The data that has been dropped. Under some platforms the data will be + The raw data that has been dropped. Under some platforms the data will be available before the drop has occurred. The format of the data is the - current type of the drop operation. + current type of the drop operation. This attribute should be used for + text-based drops, whereas file drops should use the ``data_split`` + attribute. + +.. data:: Event.data_split + + The dropped data split into a tuple. This should not be used for text-based + drops as the data is split on each word and is instead intended for file + drops where there can be multiple files in a single drop (and handles paths + with spaces correctly). + +.. data:: Event.data_raw - .. note:: This is always a list/tuple, even for text where there will only - be one item in the list/tuple. + Same as ``data``. .. data:: Event.event_name diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 1e523a72ac00c3..bc930a9ebb8867 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1669,17 +1669,17 @@ def getint_event(s): return s def getlist_event(s): - if s.startswith('{') and s.endswith('}'): + if " " in s: s = s[1:-1] else: - return s, + return s.replace(r"\{", "{"), s = s.translate((None, '\x00')) if not s: return '', try: - return self.tk.splitlist(s) + return tuple(map(lambda i: i.replace(r"\{", "{"), self.tk.splitlist(s))) except ValueError: - return s, + return s.replace(r"\{", "{"), nsign, a, b, c, ed, f, h, k, m, s, t, w, x, y, \ A, C, CST, CTT, D, E, K, L, N, ST, T, TT, W, X, Y = args @@ -1723,7 +1723,12 @@ def getlist_event(s): e.code = getint_event(C) e.common_source_types = getlist_event(CST) e.common_target_types = getlist_event(CTT) - e.data = getlist_event(D) + if " " in D: + rd = D[1:-1] + else: + rd = D + e.data = e.data_raw = rd.replace(r"\{", "{") + e.data_split = getlist_event(D) try: e.send_event = getboolean(E) except TclError: pass e.keysym = K