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