~ubuntu-branches/ubuntu/wily/spyder/wily

« back to all changes in this revision

Viewing changes to spyderlib/widgets/explorer.py

  • Committer: Package Import Robot
  • Author(s): Benjamin Drung
  • Date: 2015-01-15 12:20:11 UTC
  • mfrom: (18.1.7 experimental)
  • Revision ID: package-import@ubuntu.com-20150115122011-cc7j5dhy2h9uo13m
Tags: 2.3.2+dfsg-1ubuntu1
Backport patch to support pylint3.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# Copyright © 2009-2010 Pierre Raybaut
4
 
# Licensed under the terms of the MIT License
5
 
# (see spyderlib/__init__.py for details)
6
 
 
7
 
"""Files and Directories Explorer"""
8
 
 
9
 
# pylint: disable=C0103
10
 
# pylint: disable=R0903
11
 
# pylint: disable=R0911
12
 
# pylint: disable=R0201
13
 
 
14
 
from __future__ import with_statement
15
 
 
16
 
from spyderlib.qt.QtGui import (QVBoxLayout, QLabel, QHBoxLayout, QInputDialog,
17
 
                                QFileSystemModel, QMenu, QWidget, QToolButton,
18
 
                                QLineEdit, QMessageBox, QToolBar, QTreeView,
19
 
                                QDrag, QSortFilterProxyModel)
20
 
from spyderlib.qt.QtCore import (Qt, SIGNAL, QMimeData, QSize, QDir, QUrl,
21
 
                                 Signal, QTimer)
22
 
from spyderlib.qt.compat import getsavefilename, getexistingdirectory
23
 
 
24
 
import os
25
 
import sys
26
 
import re
27
 
import os.path as osp
28
 
import shutil
29
 
 
30
 
# Local imports
31
 
from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions,
32
 
                                       file_uri)
33
 
from spyderlib.utils import misc, encoding, programs, vcs
34
 
from spyderlib.baseconfig import _
35
 
from spyderlib.py3compat import to_text_string, getcwd, str_lower
36
 
 
37
 
 
38
 
def fixpath(path):
39
 
    """Normalize path fixing case, making absolute and removing symlinks"""
40
 
    norm = osp.normcase if os.name == 'nt' else osp.normpath
41
 
    return norm(osp.abspath(osp.realpath(path)))
42
 
 
43
 
 
44
 
def create_script(fname):
45
 
    """Create a new Python script"""
46
 
    text = os.linesep.join(["# -*- coding: utf-8 -*-", "", ""])
47
 
    encoding.write(to_text_string(text), fname, 'utf-8')
48
 
 
49
 
 
50
 
def listdir(path, include='.', exclude=r'\.pyc$|^\.', show_all=False,
51
 
            folders_only=False):
52
 
    """List files and directories"""
53
 
    namelist = []
54
 
    dirlist = [to_text_string(osp.pardir)]
55
 
    for item in os.listdir(to_text_string(path)):
56
 
        if re.search(exclude, item) and not show_all:
57
 
            continue
58
 
        if osp.isdir(osp.join(path, item)):
59
 
            dirlist.append(item)
60
 
        elif folders_only:
61
 
            continue
62
 
        elif re.search(include, item) or show_all:
63
 
            namelist.append(item)
64
 
    return sorted(dirlist, key=str_lower) + \
65
 
           sorted(namelist, key=str_lower)
66
 
 
67
 
 
68
 
def has_subdirectories(path, include, exclude, show_all):
69
 
    """Return True if path has subdirectories"""
70
 
    try:
71
 
        # > 1 because of '..'
72
 
        return len( listdir(path, include, exclude,
73
 
                            show_all, folders_only=True) ) > 1
74
 
    except (IOError, OSError):
75
 
        return False
76
 
 
77
 
 
78
 
class DirView(QTreeView):
79
 
    """Base file/directory tree view"""
80
 
    def __init__(self, parent=None):
81
 
        super(DirView, self).__init__(parent)
82
 
        self.name_filters = None
83
 
        self.parent_widget = parent
84
 
        self.valid_types = None
85
 
        self.show_all = None
86
 
        self.menu = None
87
 
        self.common_actions = None
88
 
        self.__expanded_state = None
89
 
        self._to_be_loaded = None
90
 
        self.fsmodel = None
91
 
        self.setup_fs_model()
92
 
        self._scrollbar_positions = None
93
 
                
94
 
    #---- Model
95
 
    def setup_fs_model(self):
96
 
        """Setup filesystem model"""
97
 
        filters = QDir.AllDirs | QDir.Files | QDir.Drives | QDir.NoDotAndDotDot
98
 
        self.fsmodel = QFileSystemModel(self)
99
 
        self.fsmodel.setFilter(filters)
100
 
        self.fsmodel.setNameFilterDisables(False)
101
 
        
102
 
    def install_model(self):
103
 
        """Install filesystem model"""
104
 
        self.setModel(self.fsmodel)
105
 
        
106
 
    def setup_view(self):
107
 
        """Setup view"""
108
 
        self.install_model()
109
 
        self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
110
 
                     lambda: self.resizeColumnToContents(0))
111
 
        self.setAnimated(False)
112
 
        self.setSortingEnabled(True)
113
 
        self.sortByColumn(0, Qt.AscendingOrder)
114
 
        
115
 
    def set_name_filters(self, name_filters):
116
 
        """Set name filters"""
117
 
        self.name_filters = name_filters
118
 
        self.fsmodel.setNameFilters(name_filters)
119
 
        
120
 
    def set_show_all(self, state):
121
 
        """Toggle 'show all files' state"""
122
 
        if state:
123
 
            self.fsmodel.setNameFilters([])
124
 
        else:
125
 
            self.fsmodel.setNameFilters(self.name_filters)
126
 
            
127
 
    def get_filename(self, index):
128
 
        """Return filename associated with *index*"""
129
 
        if index:
130
 
            return osp.normpath(to_text_string(self.fsmodel.filePath(index)))
131
 
        
132
 
    def get_index(self, filename):
133
 
        """Return index associated with filename"""
134
 
        return self.fsmodel.index(filename)
135
 
        
136
 
    def get_selected_filenames(self):
137
 
        """Return selected filenames"""
138
 
        if self.selectionMode() == self.ExtendedSelection:
139
 
            return [self.get_filename(idx) for idx in self.selectedIndexes()]
140
 
        else:
141
 
            return [self.get_filename(self.currentIndex())]
142
 
            
143
 
    def get_dirname(self, index):
144
 
        """Return dirname associated with *index*"""
145
 
        fname = self.get_filename(index)
146
 
        if fname:
147
 
            if osp.isdir(fname):
148
 
                return fname
149
 
            else:
150
 
                return osp.dirname(fname)
151
 
        
152
 
    #---- Tree view widget
153
 
    def setup(self, name_filters=['*.py', '*.pyw'],
154
 
              valid_types= ('.py', '.pyw'), show_all=False):
155
 
        """Setup tree widget"""
156
 
        self.setup_view()
157
 
        
158
 
        self.set_name_filters(name_filters)
159
 
        self.valid_types = valid_types
160
 
        self.show_all = show_all
161
 
        
162
 
        # Setup context menu
163
 
        self.menu = QMenu(self)
164
 
        self.common_actions = self.setup_common_actions()
165
 
        
166
 
    #---- Context menu
167
 
    def setup_common_actions(self):
168
 
        """Setup context menu common actions"""
169
 
        # Filters
170
 
        filters_action = create_action(self, _("Edit filename filters..."),
171
 
                                       None, get_icon('filter.png'),
172
 
                                       triggered=self.edit_filter)
173
 
        # Show all files
174
 
        all_action = create_action(self, _("Show all files"),
175
 
                                   toggled=self.toggle_all)
176
 
        all_action.setChecked(self.show_all)
177
 
        self.toggle_all(self.show_all)
178
 
        
179
 
        return [filters_action, all_action]
180
 
        
181
 
    def edit_filter(self):
182
 
        """Edit name filters"""
183
 
        filters, valid = QInputDialog.getText(self, _('Edit filename filters'),
184
 
                                              _('Name filters:'),
185
 
                                              QLineEdit.Normal,
186
 
                                              ", ".join(self.name_filters))
187
 
        if valid:
188
 
            filters = [f.strip() for f in to_text_string(filters).split(',')]
189
 
            self.parent_widget.sig_option_changed.emit('name_filters', filters)
190
 
            self.set_name_filters(filters)
191
 
            
192
 
    def toggle_all(self, checked):
193
 
        """Toggle all files mode"""
194
 
        self.parent_widget.sig_option_changed.emit('show_all', checked)
195
 
        self.show_all = checked
196
 
        self.set_show_all(checked)
197
 
        
198
 
    def create_file_new_actions(self, fnames):
199
 
        """Return actions for submenu 'New...'"""
200
 
        if not fnames:
201
 
            return []
202
 
        new_file_act = create_action(self, _("File..."), icon='filenew.png',
203
 
                                     triggered=lambda:
204
 
                                     self.new_file(fnames[-1]))
205
 
        new_module_act = create_action(self, _("Module..."), icon='py.png',
206
 
                                       triggered=lambda:
207
 
                                         self.new_module(fnames[-1]))
208
 
        new_folder_act = create_action(self, _("Folder..."),
209
 
                                       icon='folder_new.png',
210
 
                                       triggered=lambda:
211
 
                                        self.new_folder(fnames[-1]))
212
 
        new_package_act = create_action(self, _("Package..."),
213
 
                                        icon=get_icon('package_collapsed.png'),
214
 
                                        triggered=lambda:
215
 
                                         self.new_package(fnames[-1]))
216
 
        return [new_file_act, new_folder_act, None,
217
 
                new_module_act, new_package_act]
218
 
        
219
 
    def create_file_import_actions(self, fnames):
220
 
        """Return actions for submenu 'Import...'"""
221
 
        return []
222
 
 
223
 
    def create_file_manage_actions(self, fnames):
224
 
        """Return file management actions"""
225
 
        only_files = all([osp.isfile(_fn) for _fn in fnames])
226
 
        only_modules = all([osp.splitext(_fn)[1] in ('.py', '.pyw', '.ipy')
227
 
                            for _fn in fnames])
228
 
        only_valid = all([osp.splitext(_fn)[1] in self.valid_types
229
 
                          for _fn in fnames])
230
 
        run_action = create_action(self, _("Run"), icon="run_small.png",
231
 
                                   triggered=self.run)
232
 
        edit_action = create_action(self, _("Edit"), icon="edit.png",
233
 
                                    triggered=self.clicked)
234
 
        move_action = create_action(self, _("Move..."),
235
 
                                    icon="move.png",
236
 
                                    triggered=self.move)
237
 
        delete_action = create_action(self, _("Delete..."),
238
 
                                      icon="delete.png",
239
 
                                      triggered=self.delete)
240
 
        rename_action = create_action(self, _("Rename..."),
241
 
                                      icon="rename.png",
242
 
                                      triggered=self.rename)
243
 
        open_action = create_action(self, _("Open"), triggered=self.open)
244
 
        
245
 
        actions = []
246
 
        if only_modules:
247
 
            actions.append(run_action)
248
 
        if only_valid and only_files:
249
 
            actions.append(edit_action)
250
 
        else:
251
 
            actions.append(open_action)
252
 
        actions += [delete_action, rename_action]
253
 
        basedir = fixpath(osp.dirname(fnames[0]))
254
 
        if all([fixpath(osp.dirname(_fn)) == basedir for _fn in fnames]):
255
 
            actions.append(move_action)
256
 
        actions += [None]
257
 
        
258
 
        # VCS support is quite limited for now, so we are enabling the VCS
259
 
        # related actions only when a single file/folder is selected:
260
 
        dirname = fnames[0] if osp.isdir(fnames[0]) else osp.dirname(fnames[0])
261
 
        if len(fnames) == 1 and vcs.is_vcs_repository(dirname):
262
 
            vcs_ci = create_action(self, _("Commit"),
263
 
                                   icon="vcs_commit.png",
264
 
                                   triggered=lambda fnames=[dirname]:
265
 
                                   self.vcs_command(fnames, 'commit'))
266
 
            vcs_log = create_action(self, _("Browse repository"),
267
 
                                    icon="vcs_browse.png",
268
 
                                    triggered=lambda fnames=[dirname]:
269
 
                                    self.vcs_command(fnames, 'browse'))
270
 
            actions += [None, vcs_ci, vcs_log]
271
 
        
272
 
        return actions
273
 
 
274
 
    def create_folder_manage_actions(self, fnames):
275
 
        """Return folder management actions"""
276
 
        actions = []
277
 
        if os.name == 'nt':
278
 
            _title = _("Open command prompt here")
279
 
        else:
280
 
            _title = _("Open terminal here")
281
 
        action = create_action(self, _title, icon="cmdprompt.png",
282
 
                               triggered=lambda fnames=fnames:
283
 
                               self.open_terminal(fnames))
284
 
        actions.append(action)
285
 
        _title = _("Open Python console here")
286
 
        action = create_action(self, _title, icon="python.png",
287
 
                               triggered=lambda fnames=fnames:
288
 
                               self.open_interpreter(fnames))
289
 
        actions.append(action)
290
 
        return actions
291
 
        
292
 
    def create_context_menu_actions(self):
293
 
        """Create context menu actions"""
294
 
        actions = []
295
 
        fnames = self.get_selected_filenames()
296
 
        new_actions = self.create_file_new_actions(fnames)
297
 
        if len(new_actions) > 1:
298
 
            # Creating a submenu only if there is more than one entry
299
 
            new_act_menu = QMenu(_('New'), self)
300
 
            add_actions(new_act_menu, new_actions)
301
 
            actions.append(new_act_menu)
302
 
        else:
303
 
            actions += new_actions
304
 
        import_actions = self.create_file_import_actions(fnames)
305
 
        if len(import_actions) > 1:
306
 
            # Creating a submenu only if there is more than one entry
307
 
            import_act_menu = QMenu(_('Import'), self)
308
 
            add_actions(import_act_menu, import_actions)
309
 
            actions.append(import_act_menu)
310
 
        else:
311
 
            actions += import_actions
312
 
        if actions:
313
 
            actions.append(None)
314
 
        if fnames:
315
 
            actions += self.create_file_manage_actions(fnames)
316
 
        if actions:
317
 
            actions.append(None)
318
 
        if fnames and all([osp.isdir(_fn) for _fn in fnames]):
319
 
            actions += self.create_folder_manage_actions(fnames)
320
 
        if actions:
321
 
            actions.append(None)
322
 
        actions += self.common_actions
323
 
        return actions
324
 
 
325
 
    def update_menu(self):
326
 
        """Update context menu"""
327
 
        self.menu.clear()
328
 
        add_actions(self.menu, self.create_context_menu_actions())
329
 
    
330
 
    #---- Events
331
 
    def viewportEvent(self, event):
332
 
        """Reimplement Qt method"""
333
 
 
334
 
        # Prevent Qt from crashing or showing warnings like:
335
 
        # "QSortFilterProxyModel: index from wrong model passed to 
336
 
        # mapFromSource", probably due to the fact that the file system model 
337
 
        # is being built. See Issue 1250.
338
 
        #
339
 
        # This workaround was inspired by the following KDE bug:
340
 
        # https://bugs.kde.org/show_bug.cgi?id=172198
341
 
        #
342
 
        # Apparently, this is a bug from Qt itself.
343
 
        self.executeDelayedItemsLayout()
344
 
        
345
 
        return QTreeView.viewportEvent(self, event)        
346
 
                
347
 
    def contextMenuEvent(self, event):
348
 
        """Override Qt method"""
349
 
        self.update_menu()
350
 
        self.menu.popup(event.globalPos())
351
 
 
352
 
    def keyPressEvent(self, event):
353
 
        """Reimplement Qt method"""
354
 
        if event.key() in (Qt.Key_Enter, Qt.Key_Return):
355
 
            self.clicked()
356
 
        elif event.key() == Qt.Key_F2:
357
 
            self.rename()
358
 
        elif event.key() == Qt.Key_Delete:
359
 
            self.delete()
360
 
        else:
361
 
            QTreeView.keyPressEvent(self, event)
362
 
 
363
 
    def mouseDoubleClickEvent(self, event):
364
 
        """Reimplement Qt method"""
365
 
        QTreeView.mouseDoubleClickEvent(self, event)
366
 
        self.clicked()
367
 
        
368
 
    def clicked(self):
369
 
        """Selected item was double-clicked or enter/return was pressed"""
370
 
        fnames = self.get_selected_filenames()
371
 
        for fname in fnames:
372
 
            if osp.isdir(fname):
373
 
                self.directory_clicked(fname)
374
 
            else:
375
 
                self.open([fname])
376
 
                
377
 
    def directory_clicked(self, dirname):
378
 
        """Directory was just clicked"""
379
 
        pass
380
 
        
381
 
    #---- Drag
382
 
    def dragEnterEvent(self, event):
383
 
        """Drag and Drop - Enter event"""
384
 
        event.setAccepted(event.mimeData().hasFormat("text/plain"))
385
 
 
386
 
    def dragMoveEvent(self, event):
387
 
        """Drag and Drop - Move event"""
388
 
        if (event.mimeData().hasFormat("text/plain")):
389
 
            event.setDropAction(Qt.MoveAction)
390
 
            event.accept()
391
 
        else:
392
 
            event.ignore()
393
 
            
394
 
    def startDrag(self, dropActions):
395
 
        """Reimplement Qt Method - handle drag event"""
396
 
        data = QMimeData()
397
 
        data.setUrls([QUrl(fname) for fname in self.get_selected_filenames()])
398
 
        drag = QDrag(self)
399
 
        drag.setMimeData(data)
400
 
        drag.exec_()
401
 
        
402
 
    #---- File/Directory actions
403
 
    def open(self, fnames=None):
404
 
        """Open files with the appropriate application"""
405
 
        if fnames is None:
406
 
            fnames = self.get_selected_filenames()
407
 
        for fname in fnames:
408
 
            ext = osp.splitext(fname)[1]
409
 
            if osp.isfile(fname) and ext in self.valid_types:
410
 
                self.parent_widget.sig_open_file.emit(fname)
411
 
            else:
412
 
                self.open_outside_spyder([fname])
413
 
        
414
 
    def open_outside_spyder(self, fnames):
415
 
        """Open file outside Spyder with the appropriate application
416
 
        If this does not work, opening unknown file in Spyder, as text file"""
417
 
        for path in sorted(fnames):
418
 
            path = file_uri(path)
419
 
            ok = programs.start_file(path)
420
 
            if not ok:
421
 
                self.parent_widget.emit(SIGNAL("edit(QString)"), path)
422
 
                
423
 
    def open_terminal(self, fnames):
424
 
        """Open terminal"""
425
 
        for path in sorted(fnames):
426
 
            self.parent_widget.emit(SIGNAL("open_terminal(QString)"), path)
427
 
            
428
 
    def open_interpreter(self, fnames):
429
 
        """Open interpreter"""
430
 
        for path in sorted(fnames):
431
 
            self.parent_widget.emit(SIGNAL("open_interpreter(QString)"), path)
432
 
        
433
 
    def run(self, fnames=None):
434
 
        """Run Python scripts"""
435
 
        if fnames is None:
436
 
            fnames = self.get_selected_filenames()
437
 
        for fname in fnames:
438
 
            self.parent_widget.emit(SIGNAL("run(QString)"), fname)
439
 
    
440
 
    def remove_tree(self, dirname):
441
 
        """Remove whole directory tree
442
 
        Reimplemented in project explorer widget"""
443
 
        shutil.rmtree(dirname, onerror=misc.onerror)
444
 
    
445
 
    def delete_file(self, fname, multiple, yes_to_all):
446
 
        """Delete file"""
447
 
        if multiple:
448
 
            buttons = QMessageBox.Yes|QMessageBox.YesAll| \
449
 
                      QMessageBox.No|QMessageBox.Cancel
450
 
        else:
451
 
            buttons = QMessageBox.Yes|QMessageBox.No
452
 
        if yes_to_all is None:
453
 
            answer = QMessageBox.warning(self, _("Delete"),
454
 
                                 _("Do you really want "
455
 
                                   "to delete <b>%s</b>?"
456
 
                                   ) % osp.basename(fname), buttons)
457
 
            if answer == QMessageBox.No:
458
 
                return yes_to_all
459
 
            elif answer == QMessageBox.Cancel:
460
 
                return False
461
 
            elif answer == QMessageBox.YesAll:
462
 
                yes_to_all = True
463
 
        try:
464
 
            if osp.isfile(fname):
465
 
                misc.remove_file(fname)
466
 
                self.parent_widget.emit(SIGNAL("removed(QString)"),
467
 
                                        fname)
468
 
            else:
469
 
                self.remove_tree(fname)
470
 
                self.parent_widget.emit(SIGNAL("removed_tree(QString)"),
471
 
                                        fname)
472
 
            return yes_to_all
473
 
        except EnvironmentError as error:
474
 
            action_str = _('delete')
475
 
            QMessageBox.critical(self, _("Project Explorer"),
476
 
                            _("<b>Unable to %s <i>%s</i></b>"
477
 
                              "<br><br>Error message:<br>%s"
478
 
                              ) % (action_str, fname, to_text_string(error)))
479
 
        return False
480
 
        
481
 
    def delete(self, fnames=None):
482
 
        """Delete files"""
483
 
        if fnames is None:
484
 
            fnames = self.get_selected_filenames()
485
 
        multiple = len(fnames) > 1
486
 
        yes_to_all = None
487
 
        for fname in fnames:
488
 
            yes_to_all = self.delete_file(fname, multiple, yes_to_all)
489
 
            if yes_to_all is not None and not yes_to_all:
490
 
                # Canceled
491
 
                return
492
 
 
493
 
    def rename_file(self, fname):
494
 
        """Rename file"""
495
 
        path, valid = QInputDialog.getText(self, _('Rename'),
496
 
                              _('New name:'), QLineEdit.Normal,
497
 
                              osp.basename(fname))
498
 
        if valid:
499
 
            path = osp.join(osp.dirname(fname), to_text_string(path))
500
 
            if path == fname:
501
 
                return
502
 
            if osp.exists(path):
503
 
                if QMessageBox.warning(self, _("Rename"),
504
 
                         _("Do you really want to rename <b>%s</b> and "
505
 
                           "overwrite the existing file <b>%s</b>?"
506
 
                           ) % (osp.basename(fname), osp.basename(path)),
507
 
                         QMessageBox.Yes|QMessageBox.No) == QMessageBox.No:
508
 
                    return
509
 
            try:
510
 
                misc.rename_file(fname, path)
511
 
                self.parent_widget.emit( \
512
 
                     SIGNAL("renamed(QString,QString)"), fname, path)
513
 
                return path
514
 
            except EnvironmentError as error:
515
 
                QMessageBox.critical(self, _("Rename"),
516
 
                            _("<b>Unable to rename file <i>%s</i></b>"
517
 
                              "<br><br>Error message:<br>%s"
518
 
                              ) % (osp.basename(fname), to_text_string(error)))
519
 
    
520
 
    def rename(self, fnames=None):
521
 
        """Rename files"""
522
 
        if fnames is None:
523
 
            fnames = self.get_selected_filenames()
524
 
        if not isinstance(fnames, (tuple, list)):
525
 
            fnames = [fnames]
526
 
        for fname in fnames:
527
 
            self.rename_file(fname)
528
 
        
529
 
    def move(self, fnames=None):
530
 
        """Move files/directories"""
531
 
        if fnames is None:
532
 
            fnames = self.get_selected_filenames()
533
 
        orig = fixpath(osp.dirname(fnames[0]))
534
 
        while True:
535
 
            self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), False)
536
 
            folder = getexistingdirectory(self, _("Select directory"), orig)
537
 
            self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), True)
538
 
            if folder:
539
 
                folder = fixpath(folder)
540
 
                if folder != orig:
541
 
                    break
542
 
            else:
543
 
                return
544
 
        for fname in fnames:
545
 
            basename = osp.basename(fname)
546
 
            try:
547
 
                misc.move_file(fname, osp.join(folder, basename))
548
 
            except EnvironmentError as error:
549
 
                QMessageBox.critical(self, _("Error"),
550
 
                                     _("<b>Unable to move <i>%s</i></b>"
551
 
                                       "<br><br>Error message:<br>%s"
552
 
                                       ) % (basename, to_text_string(error)))
553
 
        
554
 
    def create_new_folder(self, current_path, title, subtitle, is_package):
555
 
        """Create new folder"""
556
 
        if current_path is None:
557
 
            current_path = ''
558
 
        if osp.isfile(current_path):
559
 
            current_path = osp.dirname(current_path)
560
 
        name, valid = QInputDialog.getText(self, title, subtitle,
561
 
                                           QLineEdit.Normal, "")
562
 
        if valid:
563
 
            dirname = osp.join(current_path, to_text_string(name))
564
 
            try:
565
 
                os.mkdir(dirname)
566
 
            except EnvironmentError as error:
567
 
                QMessageBox.critical(self, title,
568
 
                                     _("<b>Unable "
569
 
                                       "to create folder <i>%s</i></b>"
570
 
                                       "<br><br>Error message:<br>%s"
571
 
                                       ) % (dirname, to_text_string(error)))
572
 
            finally:
573
 
                if is_package:
574
 
                    fname = osp.join(dirname, '__init__.py')
575
 
                    try:
576
 
                        open(fname, 'wb').write('#')
577
 
                        return dirname
578
 
                    except EnvironmentError as error:
579
 
                        QMessageBox.critical(self, title,
580
 
                                             _("<b>Unable "
581
 
                                               "to create file <i>%s</i></b>"
582
 
                                               "<br><br>Error message:<br>%s"
583
 
                                               ) % (fname,
584
 
                                                    to_text_string(error)))
585
 
 
586
 
    def new_folder(self, basedir):
587
 
        """New folder"""
588
 
        title = _('New folder')
589
 
        subtitle = _('Folder name:')
590
 
        self.create_new_folder(basedir, title, subtitle, is_package=False)
591
 
    
592
 
    def new_package(self, basedir):
593
 
        """New package"""
594
 
        title = _('New package')
595
 
        subtitle = _('Package name:')
596
 
        self.create_new_folder(basedir, title, subtitle, is_package=True)
597
 
    
598
 
    def create_new_file(self, current_path, title, filters, create_func):
599
 
        """Create new file
600
 
        Returns True if successful"""
601
 
        if current_path is None:
602
 
            current_path = ''
603
 
        if osp.isfile(current_path):
604
 
            current_path = osp.dirname(current_path)
605
 
        self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), False)
606
 
        fname, _selfilter = getsavefilename(self, title, current_path, filters)
607
 
        self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), True)
608
 
        if fname:
609
 
            try:
610
 
                create_func(fname)
611
 
                return fname
612
 
            except EnvironmentError as error:
613
 
                QMessageBox.critical(self, _("New file"),
614
 
                                     _("<b>Unable to create file <i>%s</i>"
615
 
                                       "</b><br><br>Error message:<br>%s"
616
 
                                       ) % (fname, to_text_string(error)))
617
 
 
618
 
    def new_file(self, basedir):
619
 
        """New file"""
620
 
        title = _("New file")
621
 
        filters = _("All files")+" (*)"
622
 
        def create_func(fname):
623
 
            """File creation callback"""
624
 
            if osp.splitext(fname)[1] in ('.py', '.pyw', '.ipy'):
625
 
                create_script(fname)
626
 
            else:
627
 
                open(fname, 'wb').write('')
628
 
        fname = self.create_new_file(basedir, title, filters, create_func)
629
 
        if fname is not None:
630
 
            self.open([fname])
631
 
    
632
 
    def new_module(self, basedir):
633
 
        """New module"""
634
 
        title = _("New module")
635
 
        filters = _("Python scripts")+" (*.py *.pyw *.ipy)"
636
 
        create_func = lambda fname: self.parent_widget.emit( \
637
 
                                     SIGNAL("create_module(QString)"), fname)
638
 
        self.create_new_file(basedir, title, filters, create_func)
639
 
        
640
 
    #----- VCS actions
641
 
    def vcs_command(self, fnames, action):
642
 
        """VCS action (commit, browse)"""
643
 
        try:
644
 
            for path in sorted(fnames):
645
 
                vcs.run_vcs_tool(path, action)
646
 
        except vcs.ActionToolNotFound as error:
647
 
            msg = _("For %s support, please install one of the<br/> "
648
 
                    "following tools:<br/><br/>  %s")\
649
 
                        % (error.vcsname, ', '.join(error.tools))
650
 
            QMessageBox.critical(self, _("Error"),
651
 
                _("""<b>Unable to find external program.</b><br><br>%s""")
652
 
                    % to_text_string(msg))
653
 
        
654
 
    #----- Settings
655
 
    def get_scrollbar_position(self):
656
 
        """Return scrollbar positions"""
657
 
        return (self.horizontalScrollBar().value(),
658
 
                self.verticalScrollBar().value())
659
 
        
660
 
    def set_scrollbar_position(self, position):
661
 
        """Set scrollbar positions"""
662
 
        # Scrollbars will be restored after the expanded state
663
 
        self._scrollbar_positions = position
664
 
        if self._to_be_loaded is not None and len(self._to_be_loaded) == 0:
665
 
            self.restore_scrollbar_positions()
666
 
            
667
 
    def restore_scrollbar_positions(self):
668
 
        """Restore scrollbar positions once tree is loaded"""
669
 
        hor, ver = self._scrollbar_positions
670
 
        self.horizontalScrollBar().setValue(hor)
671
 
        self.verticalScrollBar().setValue(ver)
672
 
        
673
 
    def get_expanded_state(self):
674
 
        """Return expanded state"""
675
 
        self.save_expanded_state()
676
 
        return self.__expanded_state
677
 
    
678
 
    def set_expanded_state(self, state):
679
 
        """Set expanded state"""
680
 
        self.__expanded_state = state
681
 
        self.restore_expanded_state()
682
 
    
683
 
    def save_expanded_state(self):
684
 
        """Save all items expanded state"""
685
 
        model = self.model()
686
 
        # If model is not installed, 'model' will be None: this happens when
687
 
        # using the Project Explorer without having selected a workspace yet
688
 
        if model is not None:
689
 
            self.__expanded_state = []
690
 
            for idx in model.persistentIndexList():
691
 
                if self.isExpanded(idx):
692
 
                    self.__expanded_state.append(self.get_filename(idx))
693
 
 
694
 
    def restore_directory_state(self, fname):
695
 
        """Restore directory expanded state"""
696
 
        root = osp.normpath(to_text_string(fname))
697
 
        if not osp.exists(root):
698
 
            # Directory has been (re)moved outside Spyder
699
 
            return
700
 
        for basename in os.listdir(root):
701
 
            path = osp.normpath(osp.join(root, basename))
702
 
            if osp.isdir(path) and path in self.__expanded_state:
703
 
                self.__expanded_state.pop(self.__expanded_state.index(path))
704
 
                if self._to_be_loaded is None:
705
 
                    self._to_be_loaded = []
706
 
                self._to_be_loaded.append(path)
707
 
                self.setExpanded(self.get_index(path), True)
708
 
        if not self.__expanded_state:
709
 
            self.disconnect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
710
 
                            self.restore_directory_state)
711
 
                
712
 
    def follow_directories_loaded(self, fname):
713
 
        """Follow directories loaded during startup"""
714
 
        if self._to_be_loaded is None:
715
 
            return
716
 
        path = osp.normpath(to_text_string(fname))
717
 
        if path in self._to_be_loaded:
718
 
            self._to_be_loaded.remove(path)
719
 
        if self._to_be_loaded is not None and len(self._to_be_loaded) == 0:
720
 
            self.disconnect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
721
 
                            self.follow_directories_loaded)
722
 
            if self._scrollbar_positions is not None:
723
 
                # The tree view need some time to render branches:
724
 
                QTimer.singleShot(50, self.restore_scrollbar_positions)
725
 
 
726
 
    def restore_expanded_state(self):
727
 
        """Restore all items expanded state"""
728
 
        if self.__expanded_state is not None:
729
 
            # In the old project explorer, the expanded state was a dictionnary:
730
 
            if isinstance(self.__expanded_state, list):
731
 
                self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
732
 
                             self.restore_directory_state)
733
 
                self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
734
 
                             self.follow_directories_loaded)
735
 
 
736
 
 
737
 
class ProxyModel(QSortFilterProxyModel):
738
 
    """Proxy model: filters tree view"""
739
 
    def __init__(self, parent):
740
 
        super(ProxyModel, self).__init__(parent)
741
 
        self.root_path = None
742
 
        self.path_list = []
743
 
        self.setDynamicSortFilter(True)
744
 
        
745
 
    def setup_filter(self, root_path, path_list):
746
 
        """Setup proxy model filter parameters"""
747
 
        self.root_path = osp.normpath(to_text_string(root_path))
748
 
        self.path_list = [osp.normpath(to_text_string(p)) for p in path_list]
749
 
        self.invalidateFilter()
750
 
 
751
 
    def sort(self, column, order=Qt.AscendingOrder):
752
 
        """Reimplement Qt method"""
753
 
        self.sourceModel().sort(column, order)
754
 
        
755
 
    def filterAcceptsRow(self, row, parent_index):
756
 
        """Reimplement Qt method"""
757
 
        if self.root_path is None:
758
 
            return True
759
 
        index = self.sourceModel().index(row, 0, parent_index)
760
 
        path = osp.normpath(to_text_string(self.sourceModel().filePath(index)))
761
 
        if self.root_path.startswith(path):
762
 
            # This is necessary because parent folders need to be scanned
763
 
            return True
764
 
        else:
765
 
            for p in self.path_list:
766
 
                if path == p or path.startswith(p+os.sep):
767
 
                    return True
768
 
            else:
769
 
                return False
770
 
 
771
 
 
772
 
class FilteredDirView(DirView):
773
 
    """Filtered file/directory tree view"""
774
 
    def __init__(self, parent=None):
775
 
        super(FilteredDirView, self).__init__(parent)
776
 
        self.proxymodel = None
777
 
        self.setup_proxy_model()
778
 
        self.root_path = None
779
 
        
780
 
    #---- Model
781
 
    def setup_proxy_model(self):
782
 
        """Setup proxy model"""
783
 
        self.proxymodel = ProxyModel(self)
784
 
        self.proxymodel.setSourceModel(self.fsmodel)
785
 
        
786
 
    def install_model(self):
787
 
        """Install proxy model"""
788
 
        if self.root_path is not None:
789
 
            self.fsmodel.setNameFilters(self.name_filters)
790
 
            self.setModel(self.proxymodel)
791
 
        
792
 
    def set_root_path(self, root_path):
793
 
        """Set root path"""
794
 
        self.root_path = root_path
795
 
        self.install_model()
796
 
        index = self.fsmodel.setRootPath(root_path)
797
 
        self.setRootIndex(self.proxymodel.mapFromSource(index))
798
 
        
799
 
    def get_index(self, filename):
800
 
        """Return index associated with filename"""
801
 
        index = self.fsmodel.index(filename)
802
 
        if index.isValid() and index.model() is self.fsmodel:
803
 
            return self.proxymodel.mapFromSource(index)
804
 
        
805
 
    def set_folder_names(self, folder_names):
806
 
        """Set folder names"""
807
 
        assert self.root_path is not None
808
 
        path_list = [osp.join(self.root_path, dirname)
809
 
                     for dirname in folder_names]
810
 
        self.proxymodel.setup_filter(self.root_path, path_list)
811
 
        
812
 
    def get_filename(self, index):
813
 
        """Return filename from index"""
814
 
        if index:
815
 
            path = self.fsmodel.filePath(self.proxymodel.mapToSource(index))
816
 
            return osp.normpath(to_text_string(path))
817
 
 
818
 
 
819
 
class ExplorerTreeWidget(DirView):
820
 
    """File/directory explorer tree widget
821
 
    show_cd_only: Show current directory only
822
 
    (True/False: enable/disable the option
823
 
     None: enable the option and do not allow the user to disable it)"""
824
 
    def __init__(self, parent=None, show_cd_only=None):
825
 
        DirView.__init__(self, parent)
826
 
                
827
 
        self.history = []
828
 
        self.histindex = None
829
 
 
830
 
        self.show_cd_only = show_cd_only
831
 
        self.__original_root_index = None
832
 
        self.__last_folder = None
833
 
 
834
 
        self.menu = None
835
 
        self.common_actions = None
836
 
        
837
 
        # Enable drag events
838
 
        self.setDragEnabled(True)
839
 
        
840
 
    #---- Context menu
841
 
    def setup_common_actions(self):
842
 
        """Setup context menu common actions"""
843
 
        actions = super(ExplorerTreeWidget, self).setup_common_actions()
844
 
        if self.show_cd_only is None:
845
 
            # Enabling the 'show current directory only' option but do not
846
 
            # allow the user to disable it
847
 
            self.show_cd_only = True
848
 
        else:
849
 
            # Show current directory only
850
 
            cd_only_action = create_action(self,
851
 
                                           _("Show current directory only"),
852
 
                                           toggled=self.toggle_show_cd_only)
853
 
            cd_only_action.setChecked(self.show_cd_only)
854
 
            self.toggle_show_cd_only(self.show_cd_only)
855
 
            actions.append(cd_only_action)
856
 
        return actions
857
 
            
858
 
    def toggle_show_cd_only(self, checked):
859
 
        """Toggle show current directory only mode"""
860
 
        self.parent_widget.sig_option_changed.emit('show_cd_only', checked)
861
 
        self.show_cd_only = checked
862
 
        if checked:
863
 
            if self.__last_folder is not None:
864
 
                self.set_current_folder(self.__last_folder)
865
 
        elif self.__original_root_index is not None:
866
 
            self.setRootIndex(self.__original_root_index)
867
 
        
868
 
    #---- Refreshing widget
869
 
    def set_current_folder(self, folder):
870
 
        """Set current folder and return associated model index"""
871
 
        index = self.fsmodel.setRootPath(folder)
872
 
        self.__last_folder = folder
873
 
        if self.show_cd_only:
874
 
            if self.__original_root_index is None:
875
 
                self.__original_root_index = self.rootIndex()
876
 
            self.setRootIndex(index)
877
 
        return index
878
 
        
879
 
    def refresh(self, new_path=None, force_current=False):
880
 
        """Refresh widget
881
 
        force=False: won't refresh widget if path has not changed"""
882
 
        if new_path is None:
883
 
            new_path = getcwd()
884
 
        if force_current:
885
 
            index = self.set_current_folder(new_path)
886
 
            self.expand(index)
887
 
            self.setCurrentIndex(index)
888
 
        self.emit(SIGNAL("set_previous_enabled(bool)"),
889
 
                  self.histindex is not None and self.histindex > 0)
890
 
        self.emit(SIGNAL("set_next_enabled(bool)"),
891
 
                  self.histindex is not None and \
892
 
                  self.histindex < len(self.history)-1)
893
 
            
894
 
    #---- Events
895
 
    def directory_clicked(self, dirname):
896
 
        """Directory was just clicked"""
897
 
        self.chdir(directory=dirname)
898
 
        
899
 
    #---- Files/Directories Actions
900
 
    def go_to_parent_directory(self):
901
 
        """Go to parent directory"""
902
 
        self.chdir( osp.abspath(osp.join(getcwd(), os.pardir)) )
903
 
        
904
 
    def go_to_previous_directory(self):
905
 
        """Back to previous directory"""
906
 
        self.histindex -= 1
907
 
        self.chdir(browsing_history=True)
908
 
        
909
 
    def go_to_next_directory(self):
910
 
        """Return to next directory"""
911
 
        self.histindex += 1
912
 
        self.chdir(browsing_history=True)
913
 
        
914
 
    def update_history(self, directory):
915
 
        """Update browse history"""
916
 
        directory = osp.abspath(to_text_string(directory))
917
 
        if directory in self.history:
918
 
            self.histindex = self.history.index(directory)
919
 
        
920
 
    def chdir(self, directory=None, browsing_history=False):
921
 
        """Set directory as working directory"""
922
 
        if directory is not None:
923
 
            directory = osp.abspath(to_text_string(directory))
924
 
        if browsing_history:
925
 
            directory = self.history[self.histindex]
926
 
        elif directory in self.history:
927
 
            self.histindex = self.history.index(directory)
928
 
        else:
929
 
            if self.histindex is None:
930
 
                self.history = []
931
 
            else:
932
 
                self.history = self.history[:self.histindex+1]
933
 
            if len(self.history) == 0 or \
934
 
               (self.history and self.history[-1] != directory):
935
 
                self.history.append(directory)
936
 
            self.histindex = len(self.history)-1
937
 
        directory = to_text_string(directory)
938
 
        os.chdir(directory)
939
 
        self.parent_widget.emit(SIGNAL("open_dir(QString)"), directory)
940
 
        self.refresh(new_path=directory, force_current=True)
941
 
 
942
 
 
943
 
class ExplorerWidget(QWidget):
944
 
    """Explorer widget"""
945
 
    sig_option_changed = Signal(str, object)
946
 
    sig_open_file = Signal(str)
947
 
    
948
 
    def __init__(self, parent=None, name_filters=['*.py', '*.pyw'],
949
 
                 valid_types=('.py', '.pyw'), show_all=False,
950
 
                 show_cd_only=None, show_toolbar=True, show_icontext=True):
951
 
        QWidget.__init__(self, parent)
952
 
        
953
 
        self.treewidget = ExplorerTreeWidget(self, show_cd_only=show_cd_only)
954
 
        self.treewidget.setup(name_filters=name_filters,
955
 
                              valid_types=valid_types, show_all=show_all)
956
 
        self.treewidget.chdir(getcwd())
957
 
        
958
 
        toolbar_action = create_action(self, _("Show toolbar"),
959
 
                                       toggled=self.toggle_toolbar)
960
 
        icontext_action = create_action(self, _("Show icons and text"),
961
 
                                        toggled=self.toggle_icontext)
962
 
        self.treewidget.common_actions += [None,
963
 
                                           toolbar_action, icontext_action]
964
 
        
965
 
        # Setup toolbar
966
 
        self.toolbar = QToolBar(self)
967
 
        self.toolbar.setIconSize(QSize(16, 16))
968
 
        
969
 
        self.previous_action = create_action(self, text=_("Previous"),
970
 
                            icon=get_icon('previous.png'),
971
 
                            triggered=self.treewidget.go_to_previous_directory)
972
 
        self.toolbar.addAction(self.previous_action)
973
 
        self.previous_action.setEnabled(False)
974
 
        self.connect(self.treewidget, SIGNAL("set_previous_enabled(bool)"),
975
 
                     self.previous_action.setEnabled)
976
 
        
977
 
        self.next_action = create_action(self, text=_("Next"),
978
 
                            icon=get_icon('next.png'),
979
 
                            triggered=self.treewidget.go_to_next_directory)
980
 
        self.toolbar.addAction(self.next_action)
981
 
        self.next_action.setEnabled(False)
982
 
        self.connect(self.treewidget, SIGNAL("set_next_enabled(bool)"),
983
 
                     self.next_action.setEnabled)
984
 
        
985
 
        parent_action = create_action(self, text=_("Parent"),
986
 
                            icon=get_icon('up.png'),
987
 
                            triggered=self.treewidget.go_to_parent_directory)
988
 
        self.toolbar.addAction(parent_action)
989
 
 
990
 
        options_action = create_action(self, text='', tip=_("Options"),
991
 
                                       icon=get_icon('tooloptions.png'))
992
 
        self.toolbar.addAction(options_action)
993
 
        widget = self.toolbar.widgetForAction(options_action)
994
 
        widget.setPopupMode(QToolButton.InstantPopup)
995
 
        menu = QMenu(self)
996
 
        add_actions(menu, self.treewidget.common_actions)
997
 
        options_action.setMenu(menu)
998
 
            
999
 
        toolbar_action.setChecked(show_toolbar)
1000
 
        self.toggle_toolbar(show_toolbar)   
1001
 
        icontext_action.setChecked(show_icontext)
1002
 
        self.toggle_icontext(show_icontext)     
1003
 
        
1004
 
        vlayout = QVBoxLayout()
1005
 
        vlayout.addWidget(self.toolbar)
1006
 
        vlayout.addWidget(self.treewidget)
1007
 
        self.setLayout(vlayout)
1008
 
        
1009
 
    def toggle_toolbar(self, state):
1010
 
        """Toggle toolbar"""
1011
 
        self.sig_option_changed.emit('show_toolbar', state)
1012
 
        self.toolbar.setVisible(state)
1013
 
            
1014
 
    def toggle_icontext(self, state):
1015
 
        """Toggle icon text"""
1016
 
        self.sig_option_changed.emit('show_icontext', state)
1017
 
        for action in self.toolbar.actions():
1018
 
            widget = self.toolbar.widgetForAction(action)
1019
 
            if state:
1020
 
                widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
1021
 
            else:
1022
 
                widget.setToolButtonStyle(Qt.ToolButtonIconOnly)
1023
 
 
1024
 
 
1025
 
class FileExplorerTest(QWidget):
1026
 
    def __init__(self):
1027
 
        QWidget.__init__(self)
1028
 
        vlayout = QVBoxLayout()
1029
 
        self.setLayout(vlayout)
1030
 
        self.explorer = ExplorerWidget(self, show_cd_only=None)
1031
 
        vlayout.addWidget(self.explorer)
1032
 
        
1033
 
        hlayout1 = QHBoxLayout()
1034
 
        vlayout.addLayout(hlayout1)
1035
 
        label = QLabel("<b>Open file:</b>")
1036
 
        label.setAlignment(Qt.AlignRight)
1037
 
        hlayout1.addWidget(label)
1038
 
        self.label1 = QLabel()
1039
 
        hlayout1.addWidget(self.label1)
1040
 
        self.explorer.sig_open_file.connect(self.label1.setText)
1041
 
        
1042
 
        hlayout2 = QHBoxLayout()
1043
 
        vlayout.addLayout(hlayout2)
1044
 
        label = QLabel("<b>Open dir:</b>")
1045
 
        label.setAlignment(Qt.AlignRight)
1046
 
        hlayout2.addWidget(label)
1047
 
        self.label2 = QLabel()
1048
 
        hlayout2.addWidget(self.label2)
1049
 
        self.connect(self.explorer, SIGNAL("open_dir(QString)"),
1050
 
                     self.label2.setText)
1051
 
        
1052
 
        hlayout3 = QHBoxLayout()
1053
 
        vlayout.addLayout(hlayout3)
1054
 
        label = QLabel("<b>Option changed:</b>")
1055
 
        label.setAlignment(Qt.AlignRight)
1056
 
        hlayout3.addWidget(label)
1057
 
        self.label3 = QLabel()
1058
 
        hlayout3.addWidget(self.label3)
1059
 
        self.explorer.sig_option_changed.connect(
1060
 
           lambda x, y: self.label3.setText('option_changed: %r, %r' % (x, y)))
1061
 
 
1062
 
        self.connect(self.explorer, SIGNAL("open_parent_dir()"),
1063
 
                     lambda: self.explorer.listwidget.refresh('..'))
1064
 
 
1065
 
class ProjectExplorerTest(QWidget):
1066
 
    def __init__(self, parent=None):
1067
 
        QWidget.__init__(self, parent)
1068
 
        vlayout = QVBoxLayout()
1069
 
        self.setLayout(vlayout)
1070
 
        self.treewidget = FilteredDirView(self)
1071
 
        self.treewidget.setup_view()
1072
 
        self.treewidget.set_root_path(r'D:\Python')
1073
 
        self.treewidget.set_folder_names(['spyder', 'spyder-2.0'])
1074
 
        vlayout.addWidget(self.treewidget)
1075
 
 
1076
 
    
1077
 
if __name__ == "__main__":
1078
 
    from spyderlib.utils.qthelpers import qapplication
1079
 
    app = qapplication()
1080
 
    test = FileExplorerTest()
1081
 
#    test = ProjectExplorerTest()
1082
 
    test.resize(640, 480)
1083
 
    test.show()
1084
 
    sys.exit(app.exec_())
1085
 
    
 
 
b'\\ No newline at end of file'
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright © 2009-2010 Pierre Raybaut
 
4
# Licensed under the terms of the MIT License
 
5
# (see spyderlib/__init__.py for details)
 
6
 
 
7
"""Files and Directories Explorer"""
 
8
 
 
9
# pylint: disable=C0103
 
10
# pylint: disable=R0903
 
11
# pylint: disable=R0911
 
12
# pylint: disable=R0201
 
13
 
 
14
from __future__ import with_statement
 
15
 
 
16
from spyderlib.qt.QtGui import (QVBoxLayout, QLabel, QHBoxLayout, QInputDialog,
 
17
                                QFileSystemModel, QMenu, QWidget, QToolButton,
 
18
                                QLineEdit, QMessageBox, QToolBar, QTreeView,
 
19
                                QDrag, QSortFilterProxyModel)
 
20
from spyderlib.qt.QtCore import (Qt, SIGNAL, QMimeData, QSize, QDir, QUrl,
 
21
                                 Signal, QTimer)
 
22
from spyderlib.qt.compat import getsavefilename, getexistingdirectory
 
23
 
 
24
import os
 
25
import sys
 
26
import re
 
27
import os.path as osp
 
28
import shutil
 
29
 
 
30
# Local imports
 
31
from spyderlib.utils.qthelpers import (get_icon, create_action, add_actions,
 
32
                                       file_uri)
 
33
from spyderlib.utils import misc, encoding, programs, vcs
 
34
from spyderlib.baseconfig import _
 
35
from spyderlib.py3compat import to_text_string, getcwd, str_lower
 
36
 
 
37
if programs.is_module_installed('IPython'):
 
38
    import IPython.nbformat as nbformat
 
39
    import IPython.nbformat.current  # in IPython 0.13.2, current is not loaded
 
40
                                     # with nbformat.
 
41
    try:
 
42
        from IPython.nbconvert import PythonExporter as nbexporter  # >= 1.0
 
43
    except:
 
44
        nbexporter = None
 
45
else:
 
46
    nbformat = None
 
47
 
 
48
 
 
49
def fixpath(path):
 
50
    """Normalize path fixing case, making absolute and removing symlinks"""
 
51
    norm = osp.normcase if os.name == 'nt' else osp.normpath
 
52
    return norm(osp.abspath(osp.realpath(path)))
 
53
 
 
54
 
 
55
def create_script(fname):
 
56
    """Create a new Python script"""
 
57
    text = os.linesep.join(["# -*- coding: utf-8 -*-", "", ""])
 
58
    encoding.write(to_text_string(text), fname, 'utf-8')
 
59
 
 
60
 
 
61
def listdir(path, include='.', exclude=r'\.pyc$|^\.', show_all=False,
 
62
            folders_only=False):
 
63
    """List files and directories"""
 
64
    namelist = []
 
65
    dirlist = [to_text_string(osp.pardir)]
 
66
    for item in os.listdir(to_text_string(path)):
 
67
        if re.search(exclude, item) and not show_all:
 
68
            continue
 
69
        if osp.isdir(osp.join(path, item)):
 
70
            dirlist.append(item)
 
71
        elif folders_only:
 
72
            continue
 
73
        elif re.search(include, item) or show_all:
 
74
            namelist.append(item)
 
75
    return sorted(dirlist, key=str_lower) + \
 
76
           sorted(namelist, key=str_lower)
 
77
 
 
78
 
 
79
def has_subdirectories(path, include, exclude, show_all):
 
80
    """Return True if path has subdirectories"""
 
81
    try:
 
82
        # > 1 because of '..'
 
83
        return len( listdir(path, include, exclude,
 
84
                            show_all, folders_only=True) ) > 1
 
85
    except (IOError, OSError):
 
86
        return False
 
87
 
 
88
 
 
89
class DirView(QTreeView):
 
90
    """Base file/directory tree view"""
 
91
    def __init__(self, parent=None):
 
92
        super(DirView, self).__init__(parent)
 
93
        self.name_filters = None
 
94
        self.parent_widget = parent
 
95
        self.valid_types = None
 
96
        self.show_all = None
 
97
        self.menu = None
 
98
        self.common_actions = None
 
99
        self.__expanded_state = None
 
100
        self._to_be_loaded = None
 
101
        self.fsmodel = None
 
102
        self.setup_fs_model()
 
103
        self._scrollbar_positions = None
 
104
                
 
105
    #---- Model
 
106
    def setup_fs_model(self):
 
107
        """Setup filesystem model"""
 
108
        filters = QDir.AllDirs | QDir.Files | QDir.Drives | QDir.NoDotAndDotDot
 
109
        self.fsmodel = QFileSystemModel(self)
 
110
        self.fsmodel.setFilter(filters)
 
111
        self.fsmodel.setNameFilterDisables(False)
 
112
        
 
113
    def install_model(self):
 
114
        """Install filesystem model"""
 
115
        self.setModel(self.fsmodel)
 
116
        
 
117
    def setup_view(self):
 
118
        """Setup view"""
 
119
        self.install_model()
 
120
        self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
 
121
                     lambda: self.resizeColumnToContents(0))
 
122
        self.setAnimated(False)
 
123
        self.setSortingEnabled(True)
 
124
        self.sortByColumn(0, Qt.AscendingOrder)
 
125
        
 
126
    def set_name_filters(self, name_filters):
 
127
        """Set name filters"""
 
128
        self.name_filters = name_filters
 
129
        self.fsmodel.setNameFilters(name_filters)
 
130
        
 
131
    def set_show_all(self, state):
 
132
        """Toggle 'show all files' state"""
 
133
        if state:
 
134
            self.fsmodel.setNameFilters([])
 
135
        else:
 
136
            self.fsmodel.setNameFilters(self.name_filters)
 
137
            
 
138
    def get_filename(self, index):
 
139
        """Return filename associated with *index*"""
 
140
        if index:
 
141
            return osp.normpath(to_text_string(self.fsmodel.filePath(index)))
 
142
        
 
143
    def get_index(self, filename):
 
144
        """Return index associated with filename"""
 
145
        return self.fsmodel.index(filename)
 
146
        
 
147
    def get_selected_filenames(self):
 
148
        """Return selected filenames"""
 
149
        if self.selectionMode() == self.ExtendedSelection:
 
150
            return [self.get_filename(idx) for idx in self.selectedIndexes()]
 
151
        else:
 
152
            return [self.get_filename(self.currentIndex())]
 
153
            
 
154
    def get_dirname(self, index):
 
155
        """Return dirname associated with *index*"""
 
156
        fname = self.get_filename(index)
 
157
        if fname:
 
158
            if osp.isdir(fname):
 
159
                return fname
 
160
            else:
 
161
                return osp.dirname(fname)
 
162
        
 
163
    #---- Tree view widget
 
164
    def setup(self, name_filters=['*.py', '*.pyw'],
 
165
              valid_types= ('.py', '.pyw'), show_all=False):
 
166
        """Setup tree widget"""
 
167
        self.setup_view()
 
168
        
 
169
        self.set_name_filters(name_filters)
 
170
        self.valid_types = valid_types
 
171
        self.show_all = show_all
 
172
        
 
173
        # Setup context menu
 
174
        self.menu = QMenu(self)
 
175
        self.common_actions = self.setup_common_actions()
 
176
        
 
177
    #---- Context menu
 
178
    def setup_common_actions(self):
 
179
        """Setup context menu common actions"""
 
180
        # Filters
 
181
        filters_action = create_action(self, _("Edit filename filters..."),
 
182
                                       None, get_icon('filter.png'),
 
183
                                       triggered=self.edit_filter)
 
184
        # Show all files
 
185
        all_action = create_action(self, _("Show all files"),
 
186
                                   toggled=self.toggle_all)
 
187
        all_action.setChecked(self.show_all)
 
188
        self.toggle_all(self.show_all)
 
189
        
 
190
        return [filters_action, all_action]
 
191
        
 
192
    def edit_filter(self):
 
193
        """Edit name filters"""
 
194
        filters, valid = QInputDialog.getText(self, _('Edit filename filters'),
 
195
                                              _('Name filters:'),
 
196
                                              QLineEdit.Normal,
 
197
                                              ", ".join(self.name_filters))
 
198
        if valid:
 
199
            filters = [f.strip() for f in to_text_string(filters).split(',')]
 
200
            self.parent_widget.sig_option_changed.emit('name_filters', filters)
 
201
            self.set_name_filters(filters)
 
202
            
 
203
    def toggle_all(self, checked):
 
204
        """Toggle all files mode"""
 
205
        self.parent_widget.sig_option_changed.emit('show_all', checked)
 
206
        self.show_all = checked
 
207
        self.set_show_all(checked)
 
208
        
 
209
    def create_file_new_actions(self, fnames):
 
210
        """Return actions for submenu 'New...'"""
 
211
        if not fnames:
 
212
            return []
 
213
        new_file_act = create_action(self, _("File..."), icon='filenew.png',
 
214
                                     triggered=lambda:
 
215
                                     self.new_file(fnames[-1]))
 
216
        new_module_act = create_action(self, _("Module..."), icon='py.png',
 
217
                                       triggered=lambda:
 
218
                                         self.new_module(fnames[-1]))
 
219
        new_folder_act = create_action(self, _("Folder..."),
 
220
                                       icon='folder_new.png',
 
221
                                       triggered=lambda:
 
222
                                        self.new_folder(fnames[-1]))
 
223
        new_package_act = create_action(self, _("Package..."),
 
224
                                        icon=get_icon('package_collapsed.png'),
 
225
                                        triggered=lambda:
 
226
                                         self.new_package(fnames[-1]))
 
227
        return [new_file_act, new_folder_act, None,
 
228
                new_module_act, new_package_act]
 
229
        
 
230
    def create_file_import_actions(self, fnames):
 
231
        """Return actions for submenu 'Import...'"""
 
232
        return []
 
233
 
 
234
    def create_file_manage_actions(self, fnames):
 
235
        """Return file management actions"""
 
236
        only_files = all([osp.isfile(_fn) for _fn in fnames])
 
237
        only_modules = all([osp.splitext(_fn)[1] in ('.py', '.pyw', '.ipy')
 
238
                            for _fn in fnames])
 
239
        only_notebooks = all([osp.splitext(_fn)[1] == '.ipynb'
 
240
                              for _fn in fnames])
 
241
        only_valid = all([osp.splitext(_fn)[1] in self.valid_types
 
242
                          for _fn in fnames])
 
243
        run_action = create_action(self, _("Run"), icon="run_small.png",
 
244
                                   triggered=self.run)
 
245
        edit_action = create_action(self, _("Edit"), icon="edit.png",
 
246
                                    triggered=self.clicked)
 
247
        move_action = create_action(self, _("Move..."),
 
248
                                    icon="move.png",
 
249
                                    triggered=self.move)
 
250
        delete_action = create_action(self, _("Delete..."),
 
251
                                      icon="delete.png",
 
252
                                      triggered=self.delete)
 
253
        rename_action = create_action(self, _("Rename..."),
 
254
                                      icon="rename.png",
 
255
                                      triggered=self.rename)
 
256
        open_action = create_action(self, _("Open"), triggered=self.open)
 
257
        ipynb_convert_action = create_action(self, _("Convert to Python script"),
 
258
                                             icon="python.png",
 
259
                                             triggered=self.convert)
 
260
        
 
261
        actions = []
 
262
        if only_modules:
 
263
            actions.append(run_action)
 
264
        if only_valid and only_files:
 
265
            actions.append(edit_action)
 
266
        else:
 
267
            actions.append(open_action)
 
268
        actions += [delete_action, rename_action]
 
269
        basedir = fixpath(osp.dirname(fnames[0]))
 
270
        if all([fixpath(osp.dirname(_fn)) == basedir for _fn in fnames]):
 
271
            actions.append(move_action)
 
272
        actions += [None]
 
273
        if only_notebooks and nbformat is not None:
 
274
            actions.append(ipynb_convert_action)
 
275
        
 
276
        # VCS support is quite limited for now, so we are enabling the VCS
 
277
        # related actions only when a single file/folder is selected:
 
278
        dirname = fnames[0] if osp.isdir(fnames[0]) else osp.dirname(fnames[0])
 
279
        if len(fnames) == 1 and vcs.is_vcs_repository(dirname):
 
280
            vcs_ci = create_action(self, _("Commit"),
 
281
                                   icon="vcs_commit.png",
 
282
                                   triggered=lambda fnames=[dirname]:
 
283
                                   self.vcs_command(fnames, 'commit'))
 
284
            vcs_log = create_action(self, _("Browse repository"),
 
285
                                    icon="vcs_browse.png",
 
286
                                    triggered=lambda fnames=[dirname]:
 
287
                                    self.vcs_command(fnames, 'browse'))
 
288
            actions += [None, vcs_ci, vcs_log]
 
289
        
 
290
        return actions
 
291
 
 
292
    def create_folder_manage_actions(self, fnames):
 
293
        """Return folder management actions"""
 
294
        actions = []
 
295
        if os.name == 'nt':
 
296
            _title = _("Open command prompt here")
 
297
        else:
 
298
            _title = _("Open terminal here")
 
299
        action = create_action(self, _title, icon="cmdprompt.png",
 
300
                               triggered=lambda fnames=fnames:
 
301
                               self.open_terminal(fnames))
 
302
        actions.append(action)
 
303
        _title = _("Open Python console here")
 
304
        action = create_action(self, _title, icon="python.png",
 
305
                               triggered=lambda fnames=fnames:
 
306
                               self.open_interpreter(fnames))
 
307
        actions.append(action)
 
308
        return actions
 
309
        
 
310
    def create_context_menu_actions(self):
 
311
        """Create context menu actions"""
 
312
        actions = []
 
313
        fnames = self.get_selected_filenames()
 
314
        new_actions = self.create_file_new_actions(fnames)
 
315
        if len(new_actions) > 1:
 
316
            # Creating a submenu only if there is more than one entry
 
317
            new_act_menu = QMenu(_('New'), self)
 
318
            add_actions(new_act_menu, new_actions)
 
319
            actions.append(new_act_menu)
 
320
        else:
 
321
            actions += new_actions
 
322
        import_actions = self.create_file_import_actions(fnames)
 
323
        if len(import_actions) > 1:
 
324
            # Creating a submenu only if there is more than one entry
 
325
            import_act_menu = QMenu(_('Import'), self)
 
326
            add_actions(import_act_menu, import_actions)
 
327
            actions.append(import_act_menu)
 
328
        else:
 
329
            actions += import_actions
 
330
        if actions:
 
331
            actions.append(None)
 
332
        if fnames:
 
333
            actions += self.create_file_manage_actions(fnames)
 
334
        if actions:
 
335
            actions.append(None)
 
336
        if fnames and all([osp.isdir(_fn) for _fn in fnames]):
 
337
            actions += self.create_folder_manage_actions(fnames)
 
338
        if actions:
 
339
            actions.append(None)
 
340
        actions += self.common_actions
 
341
        return actions
 
342
 
 
343
    def update_menu(self):
 
344
        """Update context menu"""
 
345
        self.menu.clear()
 
346
        add_actions(self.menu, self.create_context_menu_actions())
 
347
    
 
348
    #---- Events
 
349
    def viewportEvent(self, event):
 
350
        """Reimplement Qt method"""
 
351
 
 
352
        # Prevent Qt from crashing or showing warnings like:
 
353
        # "QSortFilterProxyModel: index from wrong model passed to 
 
354
        # mapFromSource", probably due to the fact that the file system model 
 
355
        # is being built. See Issue 1250.
 
356
        #
 
357
        # This workaround was inspired by the following KDE bug:
 
358
        # https://bugs.kde.org/show_bug.cgi?id=172198
 
359
        #
 
360
        # Apparently, this is a bug from Qt itself.
 
361
        self.executeDelayedItemsLayout()
 
362
        
 
363
        return QTreeView.viewportEvent(self, event)        
 
364
                
 
365
    def contextMenuEvent(self, event):
 
366
        """Override Qt method"""
 
367
        self.update_menu()
 
368
        self.menu.popup(event.globalPos())
 
369
 
 
370
    def keyPressEvent(self, event):
 
371
        """Reimplement Qt method"""
 
372
        if event.key() in (Qt.Key_Enter, Qt.Key_Return):
 
373
            self.clicked()
 
374
        elif event.key() == Qt.Key_F2:
 
375
            self.rename()
 
376
        elif event.key() == Qt.Key_Delete:
 
377
            self.delete()
 
378
        else:
 
379
            QTreeView.keyPressEvent(self, event)
 
380
 
 
381
    def mouseDoubleClickEvent(self, event):
 
382
        """Reimplement Qt method"""
 
383
        QTreeView.mouseDoubleClickEvent(self, event)
 
384
        self.clicked()
 
385
        
 
386
    def clicked(self):
 
387
        """Selected item was double-clicked or enter/return was pressed"""
 
388
        fnames = self.get_selected_filenames()
 
389
        for fname in fnames:
 
390
            if osp.isdir(fname):
 
391
                self.directory_clicked(fname)
 
392
            else:
 
393
                self.open([fname])
 
394
                
 
395
    def directory_clicked(self, dirname):
 
396
        """Directory was just clicked"""
 
397
        pass
 
398
        
 
399
    #---- Drag
 
400
    def dragEnterEvent(self, event):
 
401
        """Drag and Drop - Enter event"""
 
402
        event.setAccepted(event.mimeData().hasFormat("text/plain"))
 
403
 
 
404
    def dragMoveEvent(self, event):
 
405
        """Drag and Drop - Move event"""
 
406
        if (event.mimeData().hasFormat("text/plain")):
 
407
            event.setDropAction(Qt.MoveAction)
 
408
            event.accept()
 
409
        else:
 
410
            event.ignore()
 
411
            
 
412
    def startDrag(self, dropActions):
 
413
        """Reimplement Qt Method - handle drag event"""
 
414
        data = QMimeData()
 
415
        data.setUrls([QUrl(fname) for fname in self.get_selected_filenames()])
 
416
        drag = QDrag(self)
 
417
        drag.setMimeData(data)
 
418
        drag.exec_()
 
419
        
 
420
    #---- File/Directory actions
 
421
    def open(self, fnames=None):
 
422
        """Open files with the appropriate application"""
 
423
        if fnames is None:
 
424
            fnames = self.get_selected_filenames()
 
425
        for fname in fnames:
 
426
            ext = osp.splitext(fname)[1]
 
427
            if osp.isfile(fname) and ext in self.valid_types:
 
428
                self.parent_widget.sig_open_file.emit(fname)
 
429
            else:
 
430
                self.open_outside_spyder([fname])
 
431
        
 
432
    def open_outside_spyder(self, fnames):
 
433
        """Open file outside Spyder with the appropriate application
 
434
        If this does not work, opening unknown file in Spyder, as text file"""
 
435
        for path in sorted(fnames):
 
436
            path = file_uri(path)
 
437
            ok = programs.start_file(path)
 
438
            if not ok:
 
439
                self.parent_widget.emit(SIGNAL("edit(QString)"), path)
 
440
                
 
441
    def open_terminal(self, fnames):
 
442
        """Open terminal"""
 
443
        for path in sorted(fnames):
 
444
            self.parent_widget.emit(SIGNAL("open_terminal(QString)"), path)
 
445
            
 
446
    def open_interpreter(self, fnames):
 
447
        """Open interpreter"""
 
448
        for path in sorted(fnames):
 
449
            self.parent_widget.emit(SIGNAL("open_interpreter(QString)"), path)
 
450
        
 
451
    def run(self, fnames=None):
 
452
        """Run Python scripts"""
 
453
        if fnames is None:
 
454
            fnames = self.get_selected_filenames()
 
455
        for fname in fnames:
 
456
            self.parent_widget.emit(SIGNAL("run(QString)"), fname)
 
457
    
 
458
    def remove_tree(self, dirname):
 
459
        """Remove whole directory tree
 
460
        Reimplemented in project explorer widget"""
 
461
        shutil.rmtree(dirname, onerror=misc.onerror)
 
462
    
 
463
    def delete_file(self, fname, multiple, yes_to_all):
 
464
        """Delete file"""
 
465
        if multiple:
 
466
            buttons = QMessageBox.Yes|QMessageBox.YesAll| \
 
467
                      QMessageBox.No|QMessageBox.Cancel
 
468
        else:
 
469
            buttons = QMessageBox.Yes|QMessageBox.No
 
470
        if yes_to_all is None:
 
471
            answer = QMessageBox.warning(self, _("Delete"),
 
472
                                 _("Do you really want "
 
473
                                   "to delete <b>%s</b>?"
 
474
                                   ) % osp.basename(fname), buttons)
 
475
            if answer == QMessageBox.No:
 
476
                return yes_to_all
 
477
            elif answer == QMessageBox.Cancel:
 
478
                return False
 
479
            elif answer == QMessageBox.YesAll:
 
480
                yes_to_all = True
 
481
        try:
 
482
            if osp.isfile(fname):
 
483
                misc.remove_file(fname)
 
484
                self.parent_widget.emit(SIGNAL("removed(QString)"),
 
485
                                        fname)
 
486
            else:
 
487
                self.remove_tree(fname)
 
488
                self.parent_widget.emit(SIGNAL("removed_tree(QString)"),
 
489
                                        fname)
 
490
            return yes_to_all
 
491
        except EnvironmentError as error:
 
492
            action_str = _('delete')
 
493
            QMessageBox.critical(self, _("Project Explorer"),
 
494
                            _("<b>Unable to %s <i>%s</i></b>"
 
495
                              "<br><br>Error message:<br>%s"
 
496
                              ) % (action_str, fname, to_text_string(error)))
 
497
        return False
 
498
        
 
499
    def delete(self, fnames=None):
 
500
        """Delete files"""
 
501
        if fnames is None:
 
502
            fnames = self.get_selected_filenames()
 
503
        multiple = len(fnames) > 1
 
504
        yes_to_all = None
 
505
        for fname in fnames:
 
506
            yes_to_all = self.delete_file(fname, multiple, yes_to_all)
 
507
            if yes_to_all is not None and not yes_to_all:
 
508
                # Canceled
 
509
                return
 
510
 
 
511
    def convert_notebook(self, fname):
 
512
        """Convert an IPython notebook to a Python script in editor"""
 
513
        if nbformat is not None:
 
514
            # Use writes_py if nbconvert is not available.
 
515
            if nbexporter is None:
 
516
                script = nbformat.current.writes_py(nbformat.current.read(
 
517
                                                    open(fname, 'r'), 'ipynb'))
 
518
            else:
 
519
                script = nbexporter().from_filename(fname)[0]
 
520
            self.parent_widget.sig_new_file.emit(script)
 
521
    
 
522
    def convert(self, fnames=None):
 
523
        """Convert IPython notebooks to Python scripts in editor"""
 
524
        if fnames is None:
 
525
            fnames = self.get_selected_filenames()
 
526
        if not isinstance(fnames, (tuple, list)):
 
527
            fnames = [fnames]
 
528
        for fname in fnames:
 
529
            self.convert_notebook(fname)
 
530
 
 
531
    def rename_file(self, fname):
 
532
        """Rename file"""
 
533
        path, valid = QInputDialog.getText(self, _('Rename'),
 
534
                              _('New name:'), QLineEdit.Normal,
 
535
                              osp.basename(fname))
 
536
        if valid:
 
537
            path = osp.join(osp.dirname(fname), to_text_string(path))
 
538
            if path == fname:
 
539
                return
 
540
            if osp.exists(path):
 
541
                if QMessageBox.warning(self, _("Rename"),
 
542
                         _("Do you really want to rename <b>%s</b> and "
 
543
                           "overwrite the existing file <b>%s</b>?"
 
544
                           ) % (osp.basename(fname), osp.basename(path)),
 
545
                         QMessageBox.Yes|QMessageBox.No) == QMessageBox.No:
 
546
                    return
 
547
            try:
 
548
                misc.rename_file(fname, path)
 
549
                self.parent_widget.emit( \
 
550
                     SIGNAL("renamed(QString,QString)"), fname, path)
 
551
                return path
 
552
            except EnvironmentError as error:
 
553
                QMessageBox.critical(self, _("Rename"),
 
554
                            _("<b>Unable to rename file <i>%s</i></b>"
 
555
                              "<br><br>Error message:<br>%s"
 
556
                              ) % (osp.basename(fname), to_text_string(error)))
 
557
    
 
558
    def rename(self, fnames=None):
 
559
        """Rename files"""
 
560
        if fnames is None:
 
561
            fnames = self.get_selected_filenames()
 
562
        if not isinstance(fnames, (tuple, list)):
 
563
            fnames = [fnames]
 
564
        for fname in fnames:
 
565
            self.rename_file(fname)
 
566
        
 
567
    def move(self, fnames=None):
 
568
        """Move files/directories"""
 
569
        if fnames is None:
 
570
            fnames = self.get_selected_filenames()
 
571
        orig = fixpath(osp.dirname(fnames[0]))
 
572
        while True:
 
573
            self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), False)
 
574
            folder = getexistingdirectory(self, _("Select directory"), orig)
 
575
            self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), True)
 
576
            if folder:
 
577
                folder = fixpath(folder)
 
578
                if folder != orig:
 
579
                    break
 
580
            else:
 
581
                return
 
582
        for fname in fnames:
 
583
            basename = osp.basename(fname)
 
584
            try:
 
585
                misc.move_file(fname, osp.join(folder, basename))
 
586
            except EnvironmentError as error:
 
587
                QMessageBox.critical(self, _("Error"),
 
588
                                     _("<b>Unable to move <i>%s</i></b>"
 
589
                                       "<br><br>Error message:<br>%s"
 
590
                                       ) % (basename, to_text_string(error)))
 
591
        
 
592
    def create_new_folder(self, current_path, title, subtitle, is_package):
 
593
        """Create new folder"""
 
594
        if current_path is None:
 
595
            current_path = ''
 
596
        if osp.isfile(current_path):
 
597
            current_path = osp.dirname(current_path)
 
598
        name, valid = QInputDialog.getText(self, title, subtitle,
 
599
                                           QLineEdit.Normal, "")
 
600
        if valid:
 
601
            dirname = osp.join(current_path, to_text_string(name))
 
602
            try:
 
603
                os.mkdir(dirname)
 
604
            except EnvironmentError as error:
 
605
                QMessageBox.critical(self, title,
 
606
                                     _("<b>Unable "
 
607
                                       "to create folder <i>%s</i></b>"
 
608
                                       "<br><br>Error message:<br>%s"
 
609
                                       ) % (dirname, to_text_string(error)))
 
610
            finally:
 
611
                if is_package:
 
612
                    fname = osp.join(dirname, '__init__.py')
 
613
                    try:
 
614
                        open(fname, 'wb').write('#')
 
615
                        return dirname
 
616
                    except EnvironmentError as error:
 
617
                        QMessageBox.critical(self, title,
 
618
                                             _("<b>Unable "
 
619
                                               "to create file <i>%s</i></b>"
 
620
                                               "<br><br>Error message:<br>%s"
 
621
                                               ) % (fname,
 
622
                                                    to_text_string(error)))
 
623
 
 
624
    def new_folder(self, basedir):
 
625
        """New folder"""
 
626
        title = _('New folder')
 
627
        subtitle = _('Folder name:')
 
628
        self.create_new_folder(basedir, title, subtitle, is_package=False)
 
629
    
 
630
    def new_package(self, basedir):
 
631
        """New package"""
 
632
        title = _('New package')
 
633
        subtitle = _('Package name:')
 
634
        self.create_new_folder(basedir, title, subtitle, is_package=True)
 
635
    
 
636
    def create_new_file(self, current_path, title, filters, create_func):
 
637
        """Create new file
 
638
        Returns True if successful"""
 
639
        if current_path is None:
 
640
            current_path = ''
 
641
        if osp.isfile(current_path):
 
642
            current_path = osp.dirname(current_path)
 
643
        self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), False)
 
644
        fname, _selfilter = getsavefilename(self, title, current_path, filters)
 
645
        self.parent_widget.emit(SIGNAL('redirect_stdio(bool)'), True)
 
646
        if fname:
 
647
            try:
 
648
                create_func(fname)
 
649
                return fname
 
650
            except EnvironmentError as error:
 
651
                QMessageBox.critical(self, _("New file"),
 
652
                                     _("<b>Unable to create file <i>%s</i>"
 
653
                                       "</b><br><br>Error message:<br>%s"
 
654
                                       ) % (fname, to_text_string(error)))
 
655
 
 
656
    def new_file(self, basedir):
 
657
        """New file"""
 
658
        title = _("New file")
 
659
        filters = _("All files")+" (*)"
 
660
        def create_func(fname):
 
661
            """File creation callback"""
 
662
            if osp.splitext(fname)[1] in ('.py', '.pyw', '.ipy'):
 
663
                create_script(fname)
 
664
            else:
 
665
                open(fname, 'wb').write('')
 
666
        fname = self.create_new_file(basedir, title, filters, create_func)
 
667
        if fname is not None:
 
668
            self.open([fname])
 
669
    
 
670
    def new_module(self, basedir):
 
671
        """New module"""
 
672
        title = _("New module")
 
673
        filters = _("Python scripts")+" (*.py *.pyw *.ipy)"
 
674
        create_func = lambda fname: self.parent_widget.emit( \
 
675
                                     SIGNAL("create_module(QString)"), fname)
 
676
        self.create_new_file(basedir, title, filters, create_func)
 
677
        
 
678
    #----- VCS actions
 
679
    def vcs_command(self, fnames, action):
 
680
        """VCS action (commit, browse)"""
 
681
        try:
 
682
            for path in sorted(fnames):
 
683
                vcs.run_vcs_tool(path, action)
 
684
        except vcs.ActionToolNotFound as error:
 
685
            msg = _("For %s support, please install one of the<br/> "
 
686
                    "following tools:<br/><br/>  %s")\
 
687
                        % (error.vcsname, ', '.join(error.tools))
 
688
            QMessageBox.critical(self, _("Error"),
 
689
                _("""<b>Unable to find external program.</b><br><br>%s""")
 
690
                    % to_text_string(msg))
 
691
        
 
692
    #----- Settings
 
693
    def get_scrollbar_position(self):
 
694
        """Return scrollbar positions"""
 
695
        return (self.horizontalScrollBar().value(),
 
696
                self.verticalScrollBar().value())
 
697
        
 
698
    def set_scrollbar_position(self, position):
 
699
        """Set scrollbar positions"""
 
700
        # Scrollbars will be restored after the expanded state
 
701
        self._scrollbar_positions = position
 
702
        if self._to_be_loaded is not None and len(self._to_be_loaded) == 0:
 
703
            self.restore_scrollbar_positions()
 
704
            
 
705
    def restore_scrollbar_positions(self):
 
706
        """Restore scrollbar positions once tree is loaded"""
 
707
        hor, ver = self._scrollbar_positions
 
708
        self.horizontalScrollBar().setValue(hor)
 
709
        self.verticalScrollBar().setValue(ver)
 
710
        
 
711
    def get_expanded_state(self):
 
712
        """Return expanded state"""
 
713
        self.save_expanded_state()
 
714
        return self.__expanded_state
 
715
    
 
716
    def set_expanded_state(self, state):
 
717
        """Set expanded state"""
 
718
        self.__expanded_state = state
 
719
        self.restore_expanded_state()
 
720
    
 
721
    def save_expanded_state(self):
 
722
        """Save all items expanded state"""
 
723
        model = self.model()
 
724
        # If model is not installed, 'model' will be None: this happens when
 
725
        # using the Project Explorer without having selected a workspace yet
 
726
        if model is not None:
 
727
            self.__expanded_state = []
 
728
            for idx in model.persistentIndexList():
 
729
                if self.isExpanded(idx):
 
730
                    self.__expanded_state.append(self.get_filename(idx))
 
731
 
 
732
    def restore_directory_state(self, fname):
 
733
        """Restore directory expanded state"""
 
734
        root = osp.normpath(to_text_string(fname))
 
735
        if not osp.exists(root):
 
736
            # Directory has been (re)moved outside Spyder
 
737
            return
 
738
        for basename in os.listdir(root):
 
739
            path = osp.normpath(osp.join(root, basename))
 
740
            if osp.isdir(path) and path in self.__expanded_state:
 
741
                self.__expanded_state.pop(self.__expanded_state.index(path))
 
742
                if self._to_be_loaded is None:
 
743
                    self._to_be_loaded = []
 
744
                self._to_be_loaded.append(path)
 
745
                self.setExpanded(self.get_index(path), True)
 
746
        if not self.__expanded_state:
 
747
            self.disconnect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
 
748
                            self.restore_directory_state)
 
749
                
 
750
    def follow_directories_loaded(self, fname):
 
751
        """Follow directories loaded during startup"""
 
752
        if self._to_be_loaded is None:
 
753
            return
 
754
        path = osp.normpath(to_text_string(fname))
 
755
        if path in self._to_be_loaded:
 
756
            self._to_be_loaded.remove(path)
 
757
        if self._to_be_loaded is not None and len(self._to_be_loaded) == 0:
 
758
            self.disconnect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
 
759
                            self.follow_directories_loaded)
 
760
            if self._scrollbar_positions is not None:
 
761
                # The tree view need some time to render branches:
 
762
                QTimer.singleShot(50, self.restore_scrollbar_positions)
 
763
 
 
764
    def restore_expanded_state(self):
 
765
        """Restore all items expanded state"""
 
766
        if self.__expanded_state is not None:
 
767
            # In the old project explorer, the expanded state was a dictionnary:
 
768
            if isinstance(self.__expanded_state, list):
 
769
                self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
 
770
                             self.restore_directory_state)
 
771
                self.connect(self.fsmodel, SIGNAL('directoryLoaded(QString)'),
 
772
                             self.follow_directories_loaded)
 
773
 
 
774
 
 
775
class ProxyModel(QSortFilterProxyModel):
 
776
    """Proxy model: filters tree view"""
 
777
    def __init__(self, parent):
 
778
        super(ProxyModel, self).__init__(parent)
 
779
        self.root_path = None
 
780
        self.path_list = []
 
781
        self.setDynamicSortFilter(True)
 
782
        
 
783
    def setup_filter(self, root_path, path_list):
 
784
        """Setup proxy model filter parameters"""
 
785
        self.root_path = osp.normpath(to_text_string(root_path))
 
786
        self.path_list = [osp.normpath(to_text_string(p)) for p in path_list]
 
787
        self.invalidateFilter()
 
788
 
 
789
    def sort(self, column, order=Qt.AscendingOrder):
 
790
        """Reimplement Qt method"""
 
791
        self.sourceModel().sort(column, order)
 
792
        
 
793
    def filterAcceptsRow(self, row, parent_index):
 
794
        """Reimplement Qt method"""
 
795
        if self.root_path is None:
 
796
            return True
 
797
        index = self.sourceModel().index(row, 0, parent_index)
 
798
        path = osp.normpath(to_text_string(self.sourceModel().filePath(index)))
 
799
        if self.root_path.startswith(path):
 
800
            # This is necessary because parent folders need to be scanned
 
801
            return True
 
802
        else:
 
803
            for p in self.path_list:
 
804
                if path == p or path.startswith(p+os.sep):
 
805
                    return True
 
806
            else:
 
807
                return False
 
808
 
 
809
 
 
810
class FilteredDirView(DirView):
 
811
    """Filtered file/directory tree view"""
 
812
    def __init__(self, parent=None):
 
813
        super(FilteredDirView, self).__init__(parent)
 
814
        self.proxymodel = None
 
815
        self.setup_proxy_model()
 
816
        self.root_path = None
 
817
        
 
818
    #---- Model
 
819
    def setup_proxy_model(self):
 
820
        """Setup proxy model"""
 
821
        self.proxymodel = ProxyModel(self)
 
822
        self.proxymodel.setSourceModel(self.fsmodel)
 
823
        
 
824
    def install_model(self):
 
825
        """Install proxy model"""
 
826
        if self.root_path is not None:
 
827
            self.fsmodel.setNameFilters(self.name_filters)
 
828
            self.setModel(self.proxymodel)
 
829
        
 
830
    def set_root_path(self, root_path):
 
831
        """Set root path"""
 
832
        self.root_path = root_path
 
833
        self.install_model()
 
834
        index = self.fsmodel.setRootPath(root_path)
 
835
        self.setRootIndex(self.proxymodel.mapFromSource(index))
 
836
        
 
837
    def get_index(self, filename):
 
838
        """Return index associated with filename"""
 
839
        index = self.fsmodel.index(filename)
 
840
        if index.isValid() and index.model() is self.fsmodel:
 
841
            return self.proxymodel.mapFromSource(index)
 
842
        
 
843
    def set_folder_names(self, folder_names):
 
844
        """Set folder names"""
 
845
        assert self.root_path is not None
 
846
        path_list = [osp.join(self.root_path, dirname)
 
847
                     for dirname in folder_names]
 
848
        self.proxymodel.setup_filter(self.root_path, path_list)
 
849
        
 
850
    def get_filename(self, index):
 
851
        """Return filename from index"""
 
852
        if index:
 
853
            path = self.fsmodel.filePath(self.proxymodel.mapToSource(index))
 
854
            return osp.normpath(to_text_string(path))
 
855
 
 
856
 
 
857
class ExplorerTreeWidget(DirView):
 
858
    """File/directory explorer tree widget
 
859
    show_cd_only: Show current directory only
 
860
    (True/False: enable/disable the option
 
861
     None: enable the option and do not allow the user to disable it)"""
 
862
    def __init__(self, parent=None, show_cd_only=None):
 
863
        DirView.__init__(self, parent)
 
864
                
 
865
        self.history = []
 
866
        self.histindex = None
 
867
 
 
868
        self.show_cd_only = show_cd_only
 
869
        self.__original_root_index = None
 
870
        self.__last_folder = None
 
871
 
 
872
        self.menu = None
 
873
        self.common_actions = None
 
874
        
 
875
        # Enable drag events
 
876
        self.setDragEnabled(True)
 
877
        
 
878
    #---- Context menu
 
879
    def setup_common_actions(self):
 
880
        """Setup context menu common actions"""
 
881
        actions = super(ExplorerTreeWidget, self).setup_common_actions()
 
882
        if self.show_cd_only is None:
 
883
            # Enabling the 'show current directory only' option but do not
 
884
            # allow the user to disable it
 
885
            self.show_cd_only = True
 
886
        else:
 
887
            # Show current directory only
 
888
            cd_only_action = create_action(self,
 
889
                                           _("Show current directory only"),
 
890
                                           toggled=self.toggle_show_cd_only)
 
891
            cd_only_action.setChecked(self.show_cd_only)
 
892
            self.toggle_show_cd_only(self.show_cd_only)
 
893
            actions.append(cd_only_action)
 
894
        return actions
 
895
            
 
896
    def toggle_show_cd_only(self, checked):
 
897
        """Toggle show current directory only mode"""
 
898
        self.parent_widget.sig_option_changed.emit('show_cd_only', checked)
 
899
        self.show_cd_only = checked
 
900
        if checked:
 
901
            if self.__last_folder is not None:
 
902
                self.set_current_folder(self.__last_folder)
 
903
        elif self.__original_root_index is not None:
 
904
            self.setRootIndex(self.__original_root_index)
 
905
        
 
906
    #---- Refreshing widget
 
907
    def set_current_folder(self, folder):
 
908
        """Set current folder and return associated model index"""
 
909
        index = self.fsmodel.setRootPath(folder)
 
910
        self.__last_folder = folder
 
911
        if self.show_cd_only:
 
912
            if self.__original_root_index is None:
 
913
                self.__original_root_index = self.rootIndex()
 
914
            self.setRootIndex(index)
 
915
        return index
 
916
        
 
917
    def refresh(self, new_path=None, force_current=False):
 
918
        """Refresh widget
 
919
        force=False: won't refresh widget if path has not changed"""
 
920
        if new_path is None:
 
921
            new_path = getcwd()
 
922
        if force_current:
 
923
            index = self.set_current_folder(new_path)
 
924
            self.expand(index)
 
925
            self.setCurrentIndex(index)
 
926
        self.emit(SIGNAL("set_previous_enabled(bool)"),
 
927
                  self.histindex is not None and self.histindex > 0)
 
928
        self.emit(SIGNAL("set_next_enabled(bool)"),
 
929
                  self.histindex is not None and \
 
930
                  self.histindex < len(self.history)-1)
 
931
            
 
932
    #---- Events
 
933
    def directory_clicked(self, dirname):
 
934
        """Directory was just clicked"""
 
935
        self.chdir(directory=dirname)
 
936
        
 
937
    #---- Files/Directories Actions
 
938
    def go_to_parent_directory(self):
 
939
        """Go to parent directory"""
 
940
        self.chdir( osp.abspath(osp.join(getcwd(), os.pardir)) )
 
941
        
 
942
    def go_to_previous_directory(self):
 
943
        """Back to previous directory"""
 
944
        self.histindex -= 1
 
945
        self.chdir(browsing_history=True)
 
946
        
 
947
    def go_to_next_directory(self):
 
948
        """Return to next directory"""
 
949
        self.histindex += 1
 
950
        self.chdir(browsing_history=True)
 
951
        
 
952
    def update_history(self, directory):
 
953
        """Update browse history"""
 
954
        directory = osp.abspath(to_text_string(directory))
 
955
        if directory in self.history:
 
956
            self.histindex = self.history.index(directory)
 
957
        
 
958
    def chdir(self, directory=None, browsing_history=False):
 
959
        """Set directory as working directory"""
 
960
        if directory is not None:
 
961
            directory = osp.abspath(to_text_string(directory))
 
962
        if browsing_history:
 
963
            directory = self.history[self.histindex]
 
964
        elif directory in self.history:
 
965
            self.histindex = self.history.index(directory)
 
966
        else:
 
967
            if self.histindex is None:
 
968
                self.history = []
 
969
            else:
 
970
                self.history = self.history[:self.histindex+1]
 
971
            if len(self.history) == 0 or \
 
972
               (self.history and self.history[-1] != directory):
 
973
                self.history.append(directory)
 
974
            self.histindex = len(self.history)-1
 
975
        directory = to_text_string(directory)
 
976
        os.chdir(directory)
 
977
        self.parent_widget.emit(SIGNAL("open_dir(QString)"), directory)
 
978
        self.refresh(new_path=directory, force_current=True)
 
979
 
 
980
 
 
981
class ExplorerWidget(QWidget):
 
982
    """Explorer widget"""
 
983
    sig_option_changed = Signal(str, object)
 
984
    sig_open_file = Signal(str)
 
985
    sig_new_file = Signal(str)
 
986
    
 
987
    def __init__(self, parent=None, name_filters=['*.py', '*.pyw'],
 
988
                 valid_types=('.py', '.pyw'), show_all=False,
 
989
                 show_cd_only=None, show_toolbar=True, show_icontext=True):
 
990
        QWidget.__init__(self, parent)
 
991
        
 
992
        self.treewidget = ExplorerTreeWidget(self, show_cd_only=show_cd_only)
 
993
        self.treewidget.setup(name_filters=name_filters,
 
994
                              valid_types=valid_types, show_all=show_all)
 
995
        self.treewidget.chdir(getcwd())
 
996
        
 
997
        toolbar_action = create_action(self, _("Show toolbar"),
 
998
                                       toggled=self.toggle_toolbar)
 
999
        icontext_action = create_action(self, _("Show icons and text"),
 
1000
                                        toggled=self.toggle_icontext)
 
1001
        self.treewidget.common_actions += [None,
 
1002
                                           toolbar_action, icontext_action]
 
1003
        
 
1004
        # Setup toolbar
 
1005
        self.toolbar = QToolBar(self)
 
1006
        self.toolbar.setIconSize(QSize(16, 16))
 
1007
        
 
1008
        self.previous_action = create_action(self, text=_("Previous"),
 
1009
                            icon=get_icon('previous.png'),
 
1010
                            triggered=self.treewidget.go_to_previous_directory)
 
1011
        self.toolbar.addAction(self.previous_action)
 
1012
        self.previous_action.setEnabled(False)
 
1013
        self.connect(self.treewidget, SIGNAL("set_previous_enabled(bool)"),
 
1014
                     self.previous_action.setEnabled)
 
1015
        
 
1016
        self.next_action = create_action(self, text=_("Next"),
 
1017
                            icon=get_icon('next.png'),
 
1018
                            triggered=self.treewidget.go_to_next_directory)
 
1019
        self.toolbar.addAction(self.next_action)
 
1020
        self.next_action.setEnabled(False)
 
1021
        self.connect(self.treewidget, SIGNAL("set_next_enabled(bool)"),
 
1022
                     self.next_action.setEnabled)
 
1023
        
 
1024
        parent_action = create_action(self, text=_("Parent"),
 
1025
                            icon=get_icon('up.png'),
 
1026
                            triggered=self.treewidget.go_to_parent_directory)
 
1027
        self.toolbar.addAction(parent_action)
 
1028
 
 
1029
        options_action = create_action(self, text='', tip=_("Options"),
 
1030
                                       icon=get_icon('tooloptions.png'))
 
1031
        self.toolbar.addAction(options_action)
 
1032
        widget = self.toolbar.widgetForAction(options_action)
 
1033
        widget.setPopupMode(QToolButton.InstantPopup)
 
1034
        menu = QMenu(self)
 
1035
        add_actions(menu, self.treewidget.common_actions)
 
1036
        options_action.setMenu(menu)
 
1037
            
 
1038
        toolbar_action.setChecked(show_toolbar)
 
1039
        self.toggle_toolbar(show_toolbar)   
 
1040
        icontext_action.setChecked(show_icontext)
 
1041
        self.toggle_icontext(show_icontext)     
 
1042
        
 
1043
        vlayout = QVBoxLayout()
 
1044
        vlayout.addWidget(self.toolbar)
 
1045
        vlayout.addWidget(self.treewidget)
 
1046
        self.setLayout(vlayout)
 
1047
        
 
1048
    def toggle_toolbar(self, state):
 
1049
        """Toggle toolbar"""
 
1050
        self.sig_option_changed.emit('show_toolbar', state)
 
1051
        self.toolbar.setVisible(state)
 
1052
            
 
1053
    def toggle_icontext(self, state):
 
1054
        """Toggle icon text"""
 
1055
        self.sig_option_changed.emit('show_icontext', state)
 
1056
        for action in self.toolbar.actions():
 
1057
            widget = self.toolbar.widgetForAction(action)
 
1058
            if state:
 
1059
                widget.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
 
1060
            else:
 
1061
                widget.setToolButtonStyle(Qt.ToolButtonIconOnly)
 
1062
 
 
1063
 
 
1064
class FileExplorerTest(QWidget):
 
1065
    def __init__(self):
 
1066
        QWidget.__init__(self)
 
1067
        vlayout = QVBoxLayout()
 
1068
        self.setLayout(vlayout)
 
1069
        self.explorer = ExplorerWidget(self, show_cd_only=None)
 
1070
        vlayout.addWidget(self.explorer)
 
1071
        
 
1072
        hlayout1 = QHBoxLayout()
 
1073
        vlayout.addLayout(hlayout1)
 
1074
        label = QLabel("<b>Open file:</b>")
 
1075
        label.setAlignment(Qt.AlignRight)
 
1076
        hlayout1.addWidget(label)
 
1077
        self.label1 = QLabel()
 
1078
        hlayout1.addWidget(self.label1)
 
1079
        self.explorer.sig_open_file.connect(self.label1.setText)
 
1080
        
 
1081
        hlayout2 = QHBoxLayout()
 
1082
        vlayout.addLayout(hlayout2)
 
1083
        label = QLabel("<b>Open dir:</b>")
 
1084
        label.setAlignment(Qt.AlignRight)
 
1085
        hlayout2.addWidget(label)
 
1086
        self.label2 = QLabel()
 
1087
        hlayout2.addWidget(self.label2)
 
1088
        self.connect(self.explorer, SIGNAL("open_dir(QString)"),
 
1089
                     self.label2.setText)
 
1090
        
 
1091
        hlayout3 = QHBoxLayout()
 
1092
        vlayout.addLayout(hlayout3)
 
1093
        label = QLabel("<b>Option changed:</b>")
 
1094
        label.setAlignment(Qt.AlignRight)
 
1095
        hlayout3.addWidget(label)
 
1096
        self.label3 = QLabel()
 
1097
        hlayout3.addWidget(self.label3)
 
1098
        self.explorer.sig_option_changed.connect(
 
1099
           lambda x, y: self.label3.setText('option_changed: %r, %r' % (x, y)))
 
1100
 
 
1101
        self.connect(self.explorer, SIGNAL("open_parent_dir()"),
 
1102
                     lambda: self.explorer.listwidget.refresh('..'))
 
1103
 
 
1104
class ProjectExplorerTest(QWidget):
 
1105
    def __init__(self, parent=None):
 
1106
        QWidget.__init__(self, parent)
 
1107
        vlayout = QVBoxLayout()
 
1108
        self.setLayout(vlayout)
 
1109
        self.treewidget = FilteredDirView(self)
 
1110
        self.treewidget.setup_view()
 
1111
        self.treewidget.set_root_path(r'D:\Python')
 
1112
        self.treewidget.set_folder_names(['spyder', 'spyder-2.0'])
 
1113
        vlayout.addWidget(self.treewidget)
 
1114
 
 
1115
    
 
1116
if __name__ == "__main__":
 
1117
    from spyderlib.utils.qthelpers import qapplication
 
1118
    app = qapplication()
 
1119
    test = FileExplorerTest()
 
1120
#    test = ProjectExplorerTest()
 
1121
    test.resize(640, 480)
 
1122
    test.show()
 
1123
    sys.exit(app.exec_())
 
1124