1# ------------------------------------------------------------------------------
2#
3#  Copyright (c) 2006, Enthought, Inc.
4#  All rights reserved.
5#
6#  This software is provided without warranty under the terms of the BSD
7#  license included in LICENSE.txt and may be redistributed only
8#  under the conditions described in the aforementioned license.  The license
9#  is also available online at http://www.enthought.com/licenses/BSD.txt
10#
11#  Thanks for using Enthought open source!
12#
13#  Author: David C. Morrill
14#  Date:   06/25/2006
15#
16# ------------------------------------------------------------------------------
17
18""" Defines the various editors for a drag-and-drop editor,
19    for the wxPython user interface toolkit. A drag-and-drop editor represents
20    its value as a simple image which, depending upon the editor style, can be
21    a drag source only, a drop target only, or both a drag source and a drop
22    target.
23"""
24
25
26import wx
27import numpy
28
29from pickle import load
30
31from traits.api import Bool
32
33# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
34# compatibility. The class has been moved to the
35# traitsui.editors.dnd_editor file.
36from traitsui.editors.dnd_editor import ToolkitEditorFactory
37
38from pyface.wx.drag_and_drop import (
39    PythonDropSource,
40    PythonDropTarget,
41    clipboard,
42)
43
44
45try:
46    from apptools.io import File
47except ImportError:
48    File = None
49
50try:
51    from apptools.naming.api import Binding
52except ImportError:
53    Binding = None
54
55from pyface.image_resource import ImageResource
56
57from .editor import Editor
58
59
60# The image to use when the editor accepts files:
61file_image = ImageResource("file").create_image()
62
63# The image to use when the editor accepts objects:
64object_image = ImageResource("object").create_image()
65
66# The image to use when the editor is disabled:
67inactive_image = ImageResource("inactive").create_image()
68
69# String types:
70string_type = (str, str)
71
72# -------------------------------------------------------------------------
73#  'SimpleEditor' class:
74# -------------------------------------------------------------------------
75
76
77class SimpleEditor(Editor):
78    """ Simply style of editor for a drag-and-drop editor, which is both a drag
79        source and a drop target.
80    """
81
82    # -------------------------------------------------------------------------
83    #  Trait definitions:
84    # -------------------------------------------------------------------------
85
86    #: Is the editor a drop target?
87    drop_target = Bool(True)
88
89    #: Is the editor a drag source?
90    drag_source = Bool(True)
91
92    def init(self, parent):
93        """ Finishes initializing the editor by creating the underlying toolkit
94            widget.
95        """
96        # Determine the drag/drop type:
97        value = self.value
98        self._is_list = isinstance(value, list)
99        self._is_file = isinstance(value, string_type) or (
100            self._is_list
101            and (len(value) > 0)
102            and isinstance(value[0], string_type)
103        )
104
105        # Get the right image to use:
106        image = self.factory.image
107        if image is not None:
108            image = image.create_image()
109            disabled_image = self.factory.disabled_image
110            if disabled_image is not None:
111                disabled_image = disabled_image.create_image()
112        else:
113            disabled_image = inactive_image
114            image = object_image
115            if self._is_file:
116                image = file_image
117
118        self._image = image.ConvertToBitmap()
119        if disabled_image is not None:
120            self._disabled_image = disabled_image.ConvertToBitmap()
121        else:
122            data = numpy.reshape(
123                numpy.fromstring(image.GetData(), numpy.uint8), (-1, 3)
124            ) * numpy.array([[0.297, 0.589, 0.114]])
125            g = data[:, 0] + data[:, 1] + data[:, 2]
126            data[:, 0] = data[:, 1] = data[:, 2] = g
127            image.SetData(numpy.ravel(data.astype(numpy.uint8)).tostring())
128            image.SetMaskColour(0, 0, 0)
129            self._disabled_image = image.ConvertToBitmap()
130
131        # Create the control and set up the event handlers:
132        self.control = control = wx.Window(
133            parent, -1, size=wx.Size(image.GetWidth(), image.GetHeight())
134        )
135        self.set_tooltip()
136
137        if self.drop_target:
138            control.SetDropTarget(PythonDropTarget(self))
139
140        control.Bind(wx.EVT_LEFT_DOWN, self._left_down)
141        control.Bind(wx.EVT_LEFT_UP, self._left_up)
142        control.Bind(wx.EVT_MOTION, self._mouse_move)
143        control.Bind(wx.EVT_PAINT, self._on_paint)
144
145    def dispose(self):
146        """ Disposes of the contents of an editor.
147        """
148        control = self.control
149        control.Unbind(wx.EVT_LEFT_DOWN)
150        control.Unbind(wx.EVT_LEFT_UP)
151        control.Unbind(wx.EVT_MOTION)
152        control.Unbind(wx.EVT_PAINT)
153
154        super(SimpleEditor, self).dispose()
155
156    def update_editor(self):
157        """ Updates the editor when the object trait changes externally to the
158            editor.
159        """
160        return
161
162    # -- Private Methods ------------------------------------------------------
163
164    def _get_drag_data(self, data):
165        """ Returns the processed version of a drag request's data.
166        """
167        if isinstance(data, list):
168
169            if Binding is not None and isinstance(data[0], Binding):
170                data = [item.obj for item in data]
171
172            if File is not None and isinstance(data[0], File):
173                data = [item.absolute_path for item in data]
174                if not self._is_file:
175                    result = []
176                    for file in data:
177                        item = self._unpickle(file)
178                        if item is not None:
179                            result.append(item)
180                    data = result
181
182        else:
183            if Binding is not None and isinstance(data, Binding):
184                data = data.obj
185
186            if File is not None and isinstance(data, File):
187                data = data.absolute_path
188                if not self._is_file:
189                    object = self._unpickle(data)
190                    if object is not None:
191                        data = object
192
193        return data
194
195    def _unpickle(self, file_name):
196        """ Returns the unpickled version of a specified file (if possible).
197        """
198        with open(file_name, "rb") as fh:
199            try:
200                object = load(fh)
201            except Exception:
202                object = None
203
204        return object
205
206    # -- wxPython Event Handlers ----------------------------------------------
207
208    def _on_paint(self, event):
209        """ Called when the control needs repainting.
210        """
211        image = self._image
212        control = self.control
213        if not control.IsEnabled():
214            image = self._disabled_image
215
216        wdx, wdy = control.GetClientSize()
217        wx.PaintDC(control).DrawBitmap(
218            image,
219            (wdx - image.GetWidth()) // 2,
220            (wdy - image.GetHeight()) // 2,
221            True,
222        )
223
224    def _left_down(self, event):
225        """ Handles the left mouse button being pressed.
226        """
227        if self.control.IsEnabled() and self.drag_source:
228            self._x, self._y = event.GetX(), event.GetY()
229            self.control.CaptureMouse()
230
231        event.Skip()
232
233    def _left_up(self, event):
234        """ Handles the left mouse button being released.
235        """
236        if self._x is not None:
237            self._x = None
238            self.control.ReleaseMouse()
239
240        event.Skip()
241
242    def _mouse_move(self, event):
243        """ Handles the mouse being moved.
244        """
245        if self._x is not None:
246            if (
247                abs(self._x - event.GetX()) + abs(self._y - event.GetY())
248            ) >= 3:
249                self.control.ReleaseMouse()
250                self._x = None
251                if self._is_file:
252                    FileDropSource(self.control, self.value)
253                else:
254                    PythonDropSource(self.control, self.value)
255
256        event.Skip()
257
258    # ----- Drag and drop event handlers: -------------------------------------
259
260    def wx_dropped_on(self, x, y, data, drag_result):
261        """ Handles a Python object being dropped on the tree.
262        """
263        try:
264            self.value = self._get_drag_data(data)
265            return drag_result
266        except:
267            return wx.DragNone
268
269    def wx_drag_over(self, x, y, data, drag_result):
270        """ Handles a Python object being dragged over the tree.
271        """
272        try:
273            self.object.base_trait(self.name).validate(
274                self.object, self.name, self._get_drag_data(data)
275            )
276            return drag_result
277        except:
278            return wx.DragNone
279
280
281class CustomEditor(SimpleEditor):
282    """ Custom style of drag-and-drop editor, which is not a drag source.
283    """
284
285    # -------------------------------------------------------------------------
286    #  Trait definitions:
287    # -------------------------------------------------------------------------
288
289    #: Is the editor a drag source? This value overrides the default.
290    drag_source = False
291
292
293class ReadonlyEditor(SimpleEditor):
294    """ Read-only style of drag-and-drop editor, which is not a drop target.
295    """
296
297    # -------------------------------------------------------------------------
298    #  Trait definitions:
299    # -------------------------------------------------------------------------
300
301    #: Is the editor a drop target? This value overrides the default.
302    drop_target = False
303
304
305class FileDropSource(wx.DropSource):
306    """ Represents a draggable file.
307    """
308
309    def __init__(self, source, files):
310        """ Initializes the object.
311        """
312        self.handler = None
313        self.allow_move = True
314
315        # Put the data to be dragged on the clipboard:
316        clipboard.data = files
317        clipboard.source = source
318        clipboard.drop_source = self
319
320        data_object = wx.FileDataObject()
321        if isinstance(files, string_type):
322            files = [files]
323
324        for file in files:
325            data_object.AddFile(file)
326
327        # Create the drop source and begin the drag and drop operation:
328        super(FileDropSource, self).__init__(source)
329        self.SetData(data_object)
330        self.result = self.DoDragDrop(True)
331
332    def on_dropped(self, drag_result):
333        """ Called when the data has been dropped. """
334        return
335