def __init__(self, directory, freezed=True): QAbstractItemModel.__init__(self) self.directory = directory #self.icon_for = icon_factory self.prompter = lambda *args: FSAction.PROMPT_CANCEL self.update_func = lambda *args: None self.color_directories = False self.copyStarting = Callback() self.moveStarting = Callback() self.deleteStarting = Callback() self.onDrop = Callback() self.preFSOp = Callback() self.modelReset = Callback() if not self.CACHING: self.data = self._data self.icon_mapping = {} self.overrides = {} self.cache = {}
class DirModel(QAbstractItemModel): """ :ivar directory: A `directory`-like object :ivar prompter: A callable which is called when a user interaction is needed by an FS Operation (copy, move), defaults to lambda *args: FSAction.PROMPT_CANCEL :ivar icon_mapping: .ext -> icon_name :cvar columns: a dict column_number -> column_name :cvar CACHING: if true `data`s return values are cached """ # pylint: disable-msg=C0103 columns = dict(enumerate(('Filename', 'Size', 'Modified'))) directoryChanged = pyqtSignal(object) preDirectoryChanged = pyqtSignal(object) CACHING = True def __init__(self, directory, freezed=True): QAbstractItemModel.__init__(self) self.directory = directory #self.icon_for = icon_factory self.prompter = lambda *args: FSAction.PROMPT_CANCEL self.update_func = lambda *args: None self.color_directories = False self.copyStarting = Callback() self.moveStarting = Callback() self.deleteStarting = Callback() self.onDrop = Callback() self.preFSOp = Callback() self.modelReset = Callback() if not self.CACHING: self.data = self._data self.icon_mapping = {} self.overrides = {} self.cache = {} def index(self, row, column, parent): if parent.isValid() or not self.hasIndex(row, column, parent): return QModelIndex() idx = self.createIndex(row, column, self.directory[row]) return idx def path2index(self, pathobj): row = self.directory.files.index(pathobj) return self.index(row, 0, QModelIndex()) def rowCount(self, parent): #print "RC", len(self.directory), len(self.directory.files) if not parent.isValid(): return len(self.directory) else: return 0 def columnCount(self, parent): return len(self.columns) def data(self, index, role): try: return self.cache[index.row(), index.column(), role] except KeyError: pass try: value = self._data(index, role) except OSError as exception: # File list invalid if exception.errno == errno.ENOENT: #print "NOENT" self.changeDirectory() else: raise else: self.cache[index.row(), index.column(), role] = value return value def _data(self, index, role): row = index.row() column = index.column() column_name = self.columns[column] filepath = index.internalPointer() obj = self.overrides.get((filepath, column, role), None) if obj is not None: return obj if filepath is None: import sys logger['DirModel'].error( "Fatal model error, requested index '%s' not found", row ) QApplication.instance().exit() sys.exit() # Handle broken symlinks if not filepath.realpath().exists(): if column_name == 'Filename': if role == Qt.DisplayRole: return self.formatFilename(filepath.name) elif role == Qt.DecorationRole: return self.icon_for(filepath) elif role == Qt.EditRole: return filepath.name elif column_name == 'Modified' and role == Qt.DisplayRole: return "0000-00-00 00:00" elif column_name == 'Size' and role == Qt.DisplayRole: return "0" if role == Qt.BackgroundRole and self.color_directories: if filepath.isdir(): return QPalette().color(QPalette.AlternateBase) if column_name == 'Filename': if role == Qt.DisplayRole: return self.formatFilename(filepath.name) elif role == Qt.DecorationRole: return self.icon_for(filepath) elif role == Qt.EditRole: return filepath.name elif column_name == 'Size' and not filepath.isdir(): if role == Qt.DisplayRole: try: size_str = "%.2f%s" % format_size(filepath.size) except OSError: size_str = "" return size_str elif column_name == 'Modified': if role == Qt.DisplayRole: date_obj = datetime.fromtimestamp(filepath.mtime) date_str = date_obj.strftime('%Y-%m-%d %H:%M') return date_str return QVariant() def setData(self, idx, data, role): new_path = self.directory.path / unicode(data.toString()) old_path = idx.internalPointer() old_path.rename(new_path) try: self.cache.pop((idx.row(), idx.column(), role)) except KeyError: pass self.changeDirectory() return True def formatFilename(self, name): return name def flags(self, index): flags = QAbstractItemModel.flags(self, index) if index.isValid() and index.internalPointer().name != u'..': if index.column() == 0: flags |= Qt.ItemIsEditable return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | flags else: return Qt.ItemIsDropEnabled | flags def parent(self, index): return QModelIndex() def hasChildren(self, index): if index.row() == index.column() == -1: return True return False def headerData(self, section, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole or orientation == Qt.Vertical: return QVariant() return self.columns.get(section, QVariant()) def sort(self, column, order=Qt.AscendingOrder): try: sort_attr = {0: 'name', 1: 'size', 2: 'mtime'}[int(column)] except ValueError: sort_attr = column self.directory.sort(sort_attr, order != Qt.AscendingOrder) self.reset() def supportedDropActions(self): return Qt.CopyAction def dropMimeData(self, data, action, row, column, parent): #print "DMD", data, action, row, column, parent.row(), parent.column() #print data.text(), data.urls(), hasattr(data, 'source_model') if hasattr(data, 'source_model'): filenames = [ data.source_model.directory.path_for(url.toLocalFile()) for url in data.urls() ] relpath = data.source_model.directory.path else: filenames = [path(url.toLocalFile()) for url in data.urls()] relpath = filenames[0].parent row = parent.row() if row == -1: destination = self.directory.path else: destination = self.index(row, 0, QModelIndex()).internalPointer() if not destination.isdir(): destination = self.directory.path #relpath = data.source_model.directory.path event_handled = self.onDrop.call_chain( self, data, action, row, column, parent, #filenames, destination, relpath ) if event_handled: return True menu = QMenu() copy = menu.addAction('Copy files') move = menu.addAction('Move files') cancel = menu.addAction('Cancel') choice = menu.exec_(QCursor.pos()) if choice != cancel and choice is not None: event_handled = self.preFSOp.call_chain( self, data, action, row, column, parent, {copy: 'copy', move: 'move'}[choice], filenames, destination, relpath ) if event_handled: return event_handled thread = { copy: self.copy, move: self.move, }[choice](filenames, relpath, destination) try: thread.finished.connect(data.source_model.changeDirectory) except AttributeError: pass thread.finished.connect(self.changeDirectory) thread.start() else: return False return True def mimeTypes(self): return ['text/uri-list'] def mimeData(self, indexes): data = QMimeData() urls = [] for index in indexes: if index.column() == 0: urls.append(QUrl.fromLocalFile(index.internalPointer())) data.setUrls(urls) data.source_model = self return data def reset(self): self.cache.clear() return QAbstractItemModel.reset(self) def changeDirectory(self, newpath=None): """ Changes the model's current directory to `newpath` If `newpath` is None it refreshs the contents of the current dir """ def reset_model(): self.reset() self.directoryChanged.emit(newpath) self.preDirectoryChanged.emit(self.directory.path) sync, defer = self.directory.chdir(newpath) if sync: reset_model() else: defer.connect(reset_model) @defer.connect_errback def errback(exception): print exception def setFilter(self, func): def filterwrapper(p): if func: return func(filters.make_path_props(p)) else: return True if func: filterwrapper.text = func.text else: filterwrapper.text = '' self.directory.set_filter(filterwrapper) self.reset() def copy(self, filenames, relpath, destination): thread = FSOpThread(self, self.directory.copy( relpath, filenames, destination, self.prompter, self.update_func ) ) self.copyStarting.emit(filenames, destination, thread) return thread def move(self, filenames, relpath, destination): thread = FSOpThread(self, self.directory.move( relpath, filenames, destination, self.prompter, self.update_func ) ) self.moveStarting.emit(filenames, destination, thread) return thread def delete(self, indexes): filenames = [idx.internalPointer() for idx in indexes] relpath = self.directory.path print filenames thread = FSOpThread(self, self.directory.delete( relpath, filenames, self.prompter, self.update_func, ) ) self.deleteStarting.emit(filenames, thread) thread.finished.connect( self.changeDirectory ) thread.start() def mkdir(self, dirname): fullname = self.directory.path / dirname fullname.mkdir() self.changeDirectory() def stats(self, index): pass def icon_for(self, filename): return QImage(self.icon_path_for(filename)) def icon_path_for(self, filename): def splitext(filename): parts = filename.split('.') if len(parts) == 1: return ".%s" % parts[0] elif parts[-2] == 'tar': return ".%s.%s" % (parts[-2], parts[-1]) else: return ".%s" % parts[-1] basepath = path('gfx/mimetypes') if filename.name == u"..": return basepath / u"up.png" if filename.isdir(): return basepath / u"folder.png" ext = splitext(filename) try: return basepath / self.icon_mapping[ext.lower()] except KeyError: return basepath / self.icon_mapping['']