1# ------------------------------------------------------------------------------ 2# 3# Copyright (c) 2005, 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: 10/21/2004 15# 16# ------------------------------------------------------------------------------ 17 18""" Defines file editors for the wxPython user interface toolkit. 19""" 20 21 22import wx 23 24from os.path import abspath, split, splitext, isfile, exists 25 26from traits.api import List, Str, Event, Any, on_trait_change, TraitError 27 28# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward 29# compatibility. The class has been moved to the 30# traitsui.editors.file_editor file. 31from traitsui.editors.file_editor import ToolkitEditorFactory 32 33from .text_editor import SimpleEditor as SimpleTextEditor 34 35from .helper import TraitsUIPanel, PopupControl 36 37# ------------------------------------------------------------------------- 38# Trait definitions: 39# ------------------------------------------------------------------------- 40 41# Wildcard filter: 42filter_trait = List(Str) 43 44# ------------------------------------------------------------------------- 45# 'SimpleEditor' class: 46# ------------------------------------------------------------------------- 47 48 49class SimpleEditor(SimpleTextEditor): 50 """ Simple style of file editor, consisting of a text field and a **Browse** 51 button that opens a file-selection dialog box. The user can also drag 52 and drop a file onto this control. 53 """ 54 55 #: The history control (used if the factory 'entries' > 0): 56 history = Any() 57 58 #: The popup file control (an Instance( PopupFile )): 59 popup = Any() 60 61 def init(self, parent): 62 """ Finishes initializing the editor by creating the underlying toolkit 63 widget. 64 """ 65 self.control = panel = TraitsUIPanel(parent, -1) 66 sizer = wx.BoxSizer(wx.HORIZONTAL) 67 factory = self.factory 68 69 if factory.entries > 0: 70 from .history_control import HistoryControl 71 72 self.history = HistoryControl( 73 entries=factory.entries, auto_set=factory.auto_set 74 ) 75 control = self.history.create_control(panel) 76 pad = 3 77 button = wx.Button(panel, -1, "...", size=wx.Size(28, -1)) 78 else: 79 if factory.enter_set: 80 control = wx.TextCtrl(panel, -1, "", style=wx.TE_PROCESS_ENTER) 81 panel.Bind(wx.EVT_TEXT_ENTER, self.update_object, id=control.GetId()) 82 else: 83 control = wx.TextCtrl(panel, -1, "") 84 85 control.Bind(wx.EVT_KILL_FOCUS, self.update_object) 86 87 if factory.auto_set: 88 panel.Bind(wx.EVT_TEXT, self.update_object, id=control.GetId()) 89 90 bmp = wx.ArtProvider.GetBitmap(wx.ART_FOLDER_OPEN, size=(15, 15)) 91 button = wx.BitmapButton(panel, -1, bitmap=bmp) 92 93 pad = 8 94 95 self._file_name = control 96 sizer.Add(control, 1, wx.EXPAND | wx.ALIGN_CENTER) 97 sizer.Add(button, 0, wx.LEFT | wx.ALIGN_CENTER, pad) 98 panel.Bind(wx.EVT_BUTTON, self.show_file_dialog, id=button.GetId()) 99 panel.SetDropTarget(FileDropTarget(self)) 100 panel.SetSizerAndFit(sizer) 101 self._button = button 102 103 self.set_tooltip(control) 104 105 def dispose(self): 106 """ Disposes of the contents of an editor. 107 """ 108 panel = self.control 109 panel.Unbind(wx.EVT_BUTTON, id=self._button.GetId()) 110 self._button = None 111 112 if self.history is not None: 113 self.history.dispose() 114 self.history = None 115 else: 116 control, self._file_name = self._file_name, None 117 control.Unbind(wx.EVT_KILL_FOCUS) 118 panel.Unbind(wx.EVT_TEXT_ENTER, id=control.GetId()) 119 panel.Unbind(wx.EVT_TEXT, id=control.GetId()) 120 121 super(SimpleEditor, self).dispose() 122 123 @on_trait_change("history:value") 124 def _history_value_changed(self, value): 125 """ Handles the history 'value' trait being changed. 126 """ 127 if not self._no_update: 128 self._update(value) 129 130 def update_object(self, event): 131 """ Handles the user changing the contents of the edit control. 132 """ 133 if isinstance(event, wx.FocusEvent): 134 event.Skip() 135 self._update(self._file_name.GetValue()) 136 137 def update_editor(self): 138 """ Updates the editor when the object trait changes externally to the 139 editor. 140 """ 141 if self.history is not None: 142 self._no_update = True 143 self.history.value = self.str_value 144 self._no_update = False 145 else: 146 self._file_name.SetValue(self.str_value) 147 148 def show_file_dialog(self, event): 149 """ Displays the pop-up file dialog. 150 """ 151 if self.history is not None: 152 self.popup = self._create_file_popup() 153 else: 154 dlg = self._create_file_dialog() 155 rc = dlg.ShowModal() == wx.ID_OK 156 file_name = abspath(dlg.GetPath()) 157 dlg.Destroy() 158 if rc: 159 if self.factory.truncate_ext: 160 file_name = splitext(file_name)[0] 161 162 self.value = file_name 163 self.update_editor() 164 165 def get_error_control(self): 166 """ Returns the editor's control for indicating error status. 167 """ 168 return self._file_name 169 170 # -- Traits Event Handlers ------------------------------------------------ 171 172 @on_trait_change("popup:value") 173 def _popup_value_changed(self, file_name): 174 """ Handles the popup value being changed. 175 """ 176 if self.factory.truncate_ext: 177 file_name = splitext(file_name)[0] 178 179 self.value = file_name 180 self._no_update = True 181 self.history.set_value(self.str_value) 182 self._no_update = False 183 184 @on_trait_change("popup:closed") 185 def _popup_closed_changed(self): 186 """ Handles the popup control being closed. 187 """ 188 self.popup = None 189 190 # -- UI preference save/restore interface --------------------------------- 191 192 def restore_prefs(self, prefs): 193 """ Restores any saved user preference information associated with the 194 editor. 195 """ 196 if self.history is not None: 197 self.history.history = prefs.get("history", [])[ 198 : self.factory.entries 199 ] 200 201 def save_prefs(self): 202 """ Returns any user preference information associated with the editor. 203 """ 204 if self.history is not None: 205 return {"history": self.history.history[:]} 206 207 return None 208 209 # -- Private Methods ------------------------------------------------------ 210 211 def _create_file_dialog(self): 212 """ Creates the correct type of file dialog. 213 """ 214 if len(self.factory.filter) > 0: 215 wildcard = "|".join(self.factory.filter[:]) 216 else: 217 wildcard = "All Files (*.*)|*.*" 218 219 if self.factory.dialog_style == "save": 220 style = wx.FD_SAVE 221 elif self.factory.dialog_style == "open": 222 style = wx.FD_OPEN 223 else: 224 style = wx.FD_DEFAULT_STYLE 225 226 directory, filename = split(self._get_value()) 227 228 dlg = wx.FileDialog( 229 self.control, 230 defaultDir=directory, 231 defaultFile=filename, 232 message="Select a File", 233 wildcard=wildcard, 234 style=style, 235 ) 236 237 return dlg 238 239 def _create_file_popup(self): 240 """ Creates the correct type of file popup. 241 """ 242 return PopupFile( 243 control=self.control, 244 file_name=self.str_value, 245 filter=self.factory.filter, 246 height=300, 247 ) 248 249 def _update(self, file_name): 250 """ Updates the editor value with a specified file name. 251 """ 252 try: 253 if self.factory.truncate_ext: 254 file_name = splitext(file_name)[0] 255 256 self.value = file_name 257 except TraitError as excp: 258 pass 259 260 def _get_value(self): 261 """ Returns the current file name from the edit control. 262 """ 263 if self.history is not None: 264 return self.history.value 265 266 return self._file_name.GetValue() 267 268 269class CustomEditor(SimpleTextEditor): 270 """ Custom style of file editor, consisting of a file system tree view. 271 """ 272 273 #: Is the file editor scrollable? This value overrides the default. 274 scrollable = True 275 276 #: Wildcard filter to apply to the file dialog: 277 filter = filter_trait 278 279 #: Event fired when the file system view should be rebuilt: 280 reload = Event() 281 282 #: Event fired when the user double-clicks a file: 283 dclick = Event() 284 285 def init(self, parent): 286 """ Finishes initializing the editor by creating the underlying toolkit 287 widget. 288 """ 289 style = self.get_style() 290 factory = self.factory 291 if (len(factory.filter) > 0) or (factory.filter_name != ""): 292 style |= wx.DIRCTRL_SHOW_FILTERS 293 294 self.control = wx.GenericDirCtrl(parent, style=style) 295 self._tree = tree = self.control.GetTreeCtrl() 296 id = tree.GetId() 297 tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.update_object, id=id) 298 tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_dclick, id=id) 299 tree.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tooltip, id=id) 300 301 self.filter = factory.filter 302 self.sync_value(factory.filter_name, "filter", "from", is_list=True) 303 self.sync_value(factory.reload_name, "reload", "from") 304 self.sync_value(factory.dclick_name, "dclick", "to") 305 306 self.set_tooltip() 307 308 def dispose(self): 309 """ Disposes of the contents of an editor. 310 """ 311 tree, self._tree = self._tree, None 312 313 tree.Unbind(wx.EVT_TREE_SEL_CHANGED) 314 tree.Unbind(wx.EVT_TREE_ITEM_ACTIVATED) 315 316 super(CustomEditor, self).dispose() 317 318 def update_object(self, event): 319 """ Handles the user changing the contents of the edit control. 320 """ 321 if self.control is not None: 322 path = self.control.GetPath() 323 if self.factory.allow_dir or isfile(path): 324 if self.factory.truncate_ext: 325 path = splitext(path)[0] 326 327 self.value = path 328 329 def update_editor(self): 330 """ Updates the editor when the object trait changes externally to the 331 editor. 332 """ 333 if exists(self.str_value): 334 self.control.SetPath(self.str_value) 335 336 def get_style(self): 337 """ Returns the basic style to use for the control. 338 """ 339 return wx.DIRCTRL_EDIT_LABELS 340 341 def get_error_control(self): 342 """ Returns the editor's control for indicating error status. 343 """ 344 return self._tree 345 346 def _filter_changed(self): 347 """ Handles the 'filter' trait being changed. 348 """ 349 self.control.SetFilter("|".join(self.filter[:])) 350 351 def _on_dclick(self, event): 352 """ Handles the user double-clicking on a file name. 353 """ 354 self.dclick = self.control.GetPath() 355 356 def _on_tooltip(self, event): 357 """ Handles the user hovering on a file name for a tooltip. 358 """ 359 text = self._tree.GetItemText(event.GetItem()) 360 event.SetToolTip(text) 361 362 def _reload_changed(self): 363 """ Handles the 'reload' trait being changed. 364 """ 365 self.control.ReCreateTree() 366 367 368class PopupFile(PopupControl): 369 370 #: The initially specified file name: 371 file_name = Str() 372 373 #: The file name filter to support: 374 filter = filter_trait 375 376 #: Override of PopupControl trait to make the popup resizable: 377 resizable = True 378 379 # -- PopupControl Method Overrides ---------------------------------------- 380 381 def create_control(self, parent): 382 """ Creates the file control and gets it ready for use. 383 """ 384 style = self.get_style() 385 if len(self.filter) > 0: 386 style |= wx.DIRCTRL_SHOW_FILTERS 387 388 self._files = files = wx.GenericDirCtrl( 389 parent, style=style, filter="|".join(self.filter) 390 ) 391 files.SetPath(self.file_name) 392 self._tree = tree = files.GetTreeCtrl() 393 tree.Bind(wx.EVT_TREE_SEL_CHANGED, self._select_file, id=tree.GetId()) 394 395 def dispose(self): 396 self._tree.Unbind(wx.EVT_TREE_SEL_CHANGED) 397 self._tree = self._files = None 398 399 def get_style(self): 400 """ Returns the base style for this type of popup. 401 """ 402 return wx.DIRCTRL_EDIT_LABELS 403 404 def is_valid(self, path): 405 """ Returns whether or not the path is valid. 406 """ 407 return isfile(path) 408 409 # -- Private Methods ------------------------------------------------------ 410 411 def _select_file(self, event): 412 """ Handles a file being selected in the file control. 413 """ 414 path = self._files.GetPath() 415 416 # We have to make sure the selected path is different than the original 417 # path because when a filter is changed we get called with the currently 418 # selected path, even though no file was actually selected by the user. 419 # So we only count it if it is a different path. 420 # 421 # We also check the last character of the path, because under Windows 422 # we get a call when the filter is changed for each drive letter. If the 423 # drive is not available, it can take the 'isfile' call a long time to 424 # time out, so we attempt to ignore them by doing a quick test to see 425 # if it could be a valid file name, and ignore it if it is not: 426 if ( 427 (path != abspath(self.file_name)) 428 and (path[-1:] not in ("/\\")) 429 and self.is_valid(path) 430 ): 431 self.value = path 432 433 434class FileDropTarget(wx.FileDropTarget): 435 """ A target for a drag and drop operation, which accepts a file. 436 """ 437 438 def __init__(self, editor): 439 wx.FileDropTarget.__init__(self) 440 self.editor = editor 441 442 def OnDropFiles(self, x, y, file_names): 443 self.editor.value = file_names[-1] 444 self.editor.update_editor() 445 446 return True 447