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