# Copyright (c) 2014, Riverbank Computing Limited # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the following disclaimer in the documentation # and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. import fnmatch import os from PyQt5.QtCore import pyqtSignal, Qt from PyQt5.QtWidgets import (QGridLayout, QGroupBox, QPushButton, QTreeWidget, QTreeWidgetItem, QTreeWidgetItemIterator) from ..project import QrcDirectory, QrcFile class QrcPackageEditor(QGroupBox): """ A resource file system package editor. """ # Emitted when the package has changed. package_changed = pyqtSignal() def __init__(self, title, show_root=False, scan="Scan"): """ Initialise the editor. """ super().__init__(title) self._package = None self._project = None self._title = title self._show_root = show_root layout = QGridLayout() self._package_edit = QTreeWidget() self._package_edit.header().hide() self._package_edit.itemChanged.connect(self._package_changed) layout.addWidget(self._package_edit, 0, 0, 3, 1) layout.addWidget(QPushButton(scan, clicked=self._scan), 0, 1) self._remove_button = QPushButton("Remove all", clicked=self._remove_all, enabled=False) layout.addWidget(self._remove_button, 0, 2) self._include_button = QPushButton("Include all", clicked=self._include_all, enabled=False) layout.addWidget(self._include_button, 1, 1) self._exclude_button = QPushButton("Exclude all", clicked=self._exclude_all, enabled=False) layout.addWidget(self._exclude_button, 1, 2) self._exclusions_edit = QTreeWidget() self._exclusions_edit.setHeaderLabels(["Exclusions"]) self._exclusions_edit.setEditTriggers( QTreeWidget.DoubleClicked|QTreeWidget.SelectedClicked| QTreeWidget.EditKeyPressed) self._exclusions_edit.setRootIsDecorated(False) self._exclusions_edit.itemChanged.connect(self._exclusion_changed) layout.addWidget(self._exclusions_edit, 2, 1, 1, 2) self.setLayout(layout) def configure(self, package, project): """ Configure the editor with the contents of the given package and project. """ # Save the configuration. self._package = package self._project = project # Set the package itself. self._visualise() # Set the exclusions. self._exclusions_edit.clear() for exclude in package.exclusions: self._add_exclusion_item(exclude) # Add one to be edited to create a new entry. self._add_exclusion_item() def get_root_dir(self): """ Return the root directory to scan, or '' if there was an error or the user cancelled. """ raise NotImplementedError def filter(self, name): """ See if a scanned name should be discarded. """ # Include everything by default. return False def required(self, name): """ See if a scanned name is required. """ # Nothing is required by default. return False def _add_exclusion_item(self, exclude=''): """ Add a QTreeWidgetItem that holds an exclusion. """ itm = QTreeWidgetItem([exclude]) itm.setFlags( Qt.ItemIsSelectable|Qt.ItemIsEditable|Qt.ItemIsEnabled| Qt.ItemNeverHasChildren) self._exclusions_edit.addTopLevelItem(itm) def _exclusion_changed(self, itm, _): """ Invoked when an exclusion has changed. """ exc_edit = self._exclusions_edit new_exc = itm.data(0, Qt.DisplayRole).strip() itm_index = exc_edit.indexOfTopLevelItem(itm) if new_exc != '': # See if we have added a new one. if itm_index == exc_edit.topLevelItemCount() - 1: self._add_exclusion_item() else: # It is empty so remove it. exc_edit.takeTopLevelItem(itm_index) # Save the new exclusions. self._package.exclusions = [ exc_edit.topLevelItem(i).data(0, Qt.DisplayRole).strip() for i in range(exc_edit.topLevelItemCount() - 1)] self.package_changed.emit() def _get_items(self): """ Return an iterator over the tree widget items. """ it = QTreeWidgetItemIterator(self._package_edit) if self._show_root: it += 1 itm = it.value() while itm is not None: yield itm it += 1 itm = it.value() def _include_all(self, _): """ Invoked when the user clicks on the include all button. """ for itm in self._get_items(): itm.setCheckState(0, Qt.Checked) def _exclude_all(self, _): """ Invoked when the user clicks on the exclude all button. """ for itm in self._get_items(): if not itm.isDisabled(): itm.setCheckState(0, Qt.Unchecked) itm.setExpanded(False) def _remove_all(self, _): """ Invoked when the use clicks on the remove all button. """ blocked = self._package_edit.blockSignals(True) self._package_edit.clear() self._package_edit.blockSignals(blocked) self._enable_buttons() self.package_changed.emit() def _enable_buttons(self): """ Set the enabled state of those buttons that require content. """ enable = (len(list(self._get_items())) != 0) self._remove_button.setEnabled(enable) self._include_button.setEnabled(enable) self._exclude_button.setEnabled(enable) def _scan(self, _): """ Invoked when the user clicks on the scan button. """ project = self._project package = self._package # Get the root directory to scan. root = self.get_root_dir() if root == '': return # Save the included state of any existing contents so that they can be # restored after the scan. old_state = {} for itm in self._get_items(): rel_path = [itm.data(0, Qt.DisplayRole)] parent = itm.parent() while parent is not None: rel_path.append(parent.data(0, Qt.DisplayRole)) parent = parent.parent() rel_path.reverse() old_state[os.path.join(*rel_path)] = (itm.checkState(0) == Qt.Checked) # Walk the package. self._add_to_container(package, project.relative_path(root), os.listdir(root), [], old_state) self._visualise() self.package_changed.emit() def _add_to_container(self, container, path, path_contents, dir_stack, old_state): """ Add the files and directories of a package or sub-package to a container. """ # Make sure any filter is applied in a predictable order. path_contents.sort(key=str.lower) dir_stack.append(os.path.basename(path)) contents = [] for name in path_contents: # Apply any exclusions. for exc in self._package.exclusions: if fnmatch.fnmatch(name, exc): name = None break if name is None: continue # Apply any filter. if len(dir_stack) > 1: module_path = dir_stack[1:] module_path.append(name) path_name = '/'.join(module_path) else: path_name = name if self.filter(path_name): continue # See if we already know the included state. rel_path = os.path.join(os.path.join(*dir_stack), name) included = old_state.get(rel_path, False) # Add the content. full_name = os.path.join(path, name) if os.path.isdir(full_name): qrc = QrcDirectory(name, included) self._add_to_container(qrc, full_name, os.listdir(full_name), dir_stack, old_state) elif os.path.isfile(full_name): qrc = QrcFile(name, included) else: continue contents.append(qrc) container.contents = contents dir_stack.pop() def _visualise(self): """ Update the GUI with the package content. """ blocked = self._package_edit.blockSignals(True) self._package_edit.clear() if self._show_root: parent = QTreeWidgetItem([self._package.name]) self._package_edit.addTopLevelItem(parent) parent.setExpanded(True) else: parent = self._package_edit self._visualise_contents(self._package.contents, parent) self._package_edit.blockSignals(blocked) self._enable_buttons() def _visualise_contents(self, contents, parent): """ Visualise the contents for a parent. """ module_names = [''] p = parent while p is not None and isinstance(p, QTreeWidgetItem): module_names.insert(0, p.text(0)) p = p.parent() for content in contents: module_names[-1] = content.name required = self.required('/'.join(module_names)) itm = QTreeWidgetItem(parent, [content.name]) itm.setDisabled(required) if required: content.included = True p = parent while p is not None and isinstance(p, QTreeWidgetItem): p.setCheckState(0, Qt.Checked) p._qrc_item.included = True p = p.parent() itm.setCheckState(0, Qt.Checked if content.included else Qt.Unchecked) itm._qrc_item = content if isinstance(content, QrcDirectory): self._visualise_contents(content.contents, itm) def _package_changed(self, itm, column): """ Invoked when part of the package changes. """ itm._qrc_item.included = (itm.checkState(0) == Qt.Checked) self.package_changed.emit()