diff --git a/resource_sharing/gui/resource_sharing_dialog.py b/resource_sharing/gui/resource_sharing_dialog.py index 586a9fd1..10ae41b7 100644 --- a/resource_sharing/gui/resource_sharing_dialog.py +++ b/resource_sharing/gui/resource_sharing_dialog.py @@ -456,8 +456,41 @@ def install_finished(self): self.progress_dialog.hide() if self.installer_worker.install_status: self.reload_collections_model() - message = '%s is installed successfully' % ( + # Report what has been installed + message = '%s was successfully installed, containing:\n' else: message = self.installer_worker.error_message QMessageBox.information(self, 'Resource Sharing', message) diff --git a/resource_sharing/resource_handler/model_handler.py b/resource_sharing/resource_handler/model_handler.py index 874ba661..d4e8dcbc 100644 --- a/resource_sharing/resource_handler/model_handler.py +++ b/resource_sharing/resource_handler/model_handler.py @@ -35,7 +35,7 @@ def install(self): Resource Sharing collection to the user's processing model directory and refresh the provider. """ - # Check if the dir exists, return silently if it doesn't + # Return silently if the directory does not exist # if Path(self.resource_dir).exists(): if not os.path.exists(self.resource_dir): return @@ -58,6 +58,7 @@ def install(self): str(model_file) + "':\n" + str(e)) if valid > 0: self.refresh_Model_provider() + self.collection[MODELS] = valid def uninstall(self): """Uninstall the models from processing toolbox.""" diff --git a/resource_sharing/resource_handler/processing_handler.py b/resource_sharing/resource_handler/processing_handler.py index 9e0309c3..1dc4a4f1 100644 --- a/resource_sharing/resource_handler/processing_handler.py +++ b/resource_sharing/resource_handler/processing_handler.py @@ -9,6 +9,7 @@ from qgis.core import QgsApplication LOGGER = logging.getLogger('QGIS Resource Sharing') +PROCESSING = 'processing' class ProcessingScriptHandler(BaseResourceHandler): @@ -21,15 +22,15 @@ def __init__(self, collection_id): @classmethod def dir_name(cls): - return 'processing' + return PROCESSING def install(self): - """Install the processing scripts in the collection. + """Install the processing scripts of the collection. - We copy the processing scripts exist in the processing dir to the - user's processing scripts directory and refresh the provider. + We copy the processing scripts to the user's processing + scripts directory, and refresh the provider. """ - # Check if the dir exists, pass installing silently if it doesn't exist + # Pass silently if the directory does not exist if not os.path.exists(self.resource_dir): return @@ -42,25 +43,38 @@ def install(self): valid = 0 for processing_file in processing_files: - # Install silently the processing file - try: - shutil.copy(processing_file, self.scripts_folder()) - valid += 1 - except OSError as e: - LOGGER.error("Could not copy script '" + + # Install the processing file silently + try: + shutil.copy(processing_file, self.scripts_folder()) + valid += 1 + except OSError as e: + LOGGER.error("Could not copy script '" + str(processing_file) + "'\n" + str(e)) if valid > 0: self.refresh_script_provider() + self.collection[PROCESSING] = valid def uninstall(self): """Uninstall the processing scripts from processing toolbox.""" - # Remove the script files containing substring collection_id - for item in os.listdir(self.scripts_folder()): - if fnmatch.fnmatch(item, '*%s*' % self.collection_id): + # if not Path(self.resource_dir).exists(): + if not os.path.exists(self.resource_dir): + return + # Remove the processing script files that are present in this + # collection + for item in os.listdir(self.resource_dir): + # file_path = self.resource_dir / item) + file_path = os.path.join(self.resource_dir, item) + if fnmatch.fnmatch(file_path, '*%s*' % self.collection_id): script_path = os.path.join(self.scripts_folder(), item) if os.path.exists(script_path): os.remove(script_path) + #for item in os.listdir(self.scripts_folder()): + # if fnmatch.fnmatch(item, '*%s*' % self.collection_id): + # script_path = os.path.join(self.scripts_folder(), item) + # if os.path.exists(script_path): + # os.remove(script_path) + self.refresh_script_provider() def refresh_script_provider(self): diff --git a/resource_sharing/resource_handler/r_handler.py b/resource_sharing/resource_handler/r_handler.py index fe2cadee..dc9b2b79 100644 --- a/resource_sharing/resource_handler/r_handler.py +++ b/resource_sharing/resource_handler/r_handler.py @@ -63,9 +63,10 @@ def install(self): "'\n" + str(e)) if valid > 0: self.refresh_Rscript_provider() + self.collection[RSCRIPTS_FOLDER] = valid def uninstall(self): - """Uninstall the r scripts from processing toolbox.""" + """Uninstall the R scripts from processing toolbox.""" # if not Path(self.resource_dir).exists(): if not os.path.exists(self.resource_dir): return @@ -77,7 +78,6 @@ def uninstall(self): script_path = os.path.join(self.RScripts_folder(), item) if os.path.exists(script_path): os.remove(script_path) - self.refresh_Rscript_provider() def refresh_Rscript_provider(self): diff --git a/resource_sharing/resource_handler/style_handler.py b/resource_sharing/resource_handler/style_handler.py index f95385bd..f604817c 100644 --- a/resource_sharing/resource_handler/style_handler.py +++ b/resource_sharing/resource_handler/style_handler.py @@ -1,14 +1,17 @@ # coding=utf-8 import os import fnmatch +import logging from resource_sharing.resource_handler.base import BaseResourceHandler from resource_sharing.resource_handler.symbol_resolver_mixin import \ SymbolResolverMixin +LOGGER = logging.getLogger('QGIS Resource Sharing') +STYLE = 'style' class StyleResourceHandler(BaseResourceHandler, SymbolResolverMixin): - """Concrete class of the style handler.""" + """Style handler class.""" IS_DISABLED = False def __init__(self, collection_id): @@ -17,12 +20,12 @@ def __init__(self, collection_id): @classmethod def dir_name(cls): - return 'style' + return STYLE def install(self): """Install the style. - We just resolve the symbol svg/image path in the qml file + We just resolve the symbol SVG/image path in the QML file """ # Check if the dir exists, pass installing silently if it doesn't exist if not os.path.exists(self.resource_dir): @@ -35,14 +38,19 @@ def install(self): if fnmatch.fnmatch(file_path, '*.qml'): style_files.append(file_path) - # If there's no symbol files don't do anything + # Nothing to do if there are no symbol files if len(style_files) == 0: return + valid = 0 for style_file in style_files: - # Modify the style + # Try to fix image and PNG paths in the QML file self.resolve_dependency(style_file) + valid += 1 + if valid >= 0: + self.collection[STYLE] = valid def uninstall(self): - """Uninstall the style from QGIS.""" + """Uninstall the style.""" + # The style is not installed, so do nothing. pass diff --git a/resource_sharing/resource_handler/svg_handler.py b/resource_sharing/resource_handler/svg_handler.py index 277e813a..1dc08c76 100644 --- a/resource_sharing/resource_handler/svg_handler.py +++ b/resource_sharing/resource_handler/svg_handler.py @@ -1,5 +1,7 @@ # coding=utf-8 import os +import fnmatch +import logging from qgis.PyQt.QtCore import QSettings from qgis.core import QgsSettings @@ -11,18 +13,20 @@ from resource_sharing.resource_handler.base import BaseResourceHandler from resource_sharing.utilities import local_collection_path +SVG = 'svg' +LOGGER = logging.getLogger('QGIS Resource Sharing') class SVGResourceHandler(BaseResourceHandler): - """Concrete class of the SVG resource handler.""" + """The SVG resource handler class.""" IS_DISABLED = False def __init__(self, collection_id): - """Constructor of the base class.""" + """Base class constructor.""" BaseResourceHandler.__init__(self, collection_id) @classmethod def svg_search_paths(cls): - """Return a list of SVG paths read from settings""" + """Read the SVG paths from settings""" # settings = QSettings() settings = QgsSettings() search_paths_str = settings.value('svg/searchPathsForSVG') @@ -39,7 +43,7 @@ def svg_search_paths(cls): @classmethod def set_svg_search_paths(cls, paths): - """Set a list of SVG paths read from settings""" + """Write the list of SVG paths to settings""" # settings = QSettings() settings = QgsSettings() if Qgis.QGIS_VERSION_INT < 29900: @@ -49,18 +53,17 @@ def set_svg_search_paths(cls, paths): @classmethod def dir_name(cls): - return 'svg' + return SVG def install(self): - """Install the SVGs from this collection in to QGIS. + """Install the SVGs from this collection. - We simply just add the path to the collection root directory to search - path in QGIS. + We just add the collection root directory path to the + SVG search path. """ # Check if the dir exists, pass installing silently if it doesn't exist if not os.path.exists(self.resource_dir): return - # Add to the search paths for SVG search_paths = self.svg_search_paths() @@ -69,9 +72,19 @@ def install(self): self.set_svg_search_paths(search_paths) + # Count the SVGs + valid = 0 + for item in os.listdir(self.resource_dir): + # file_path = self.resource_dir / item) + file_path = os.path.join(self.resource_dir, item) + if fnmatch.fnmatch(file_path, '*.svg'): + valid += 1 + if valid >= 0: + self.collection[SVG] = valid + def uninstall(self): """Uninstall the SVGs from QGIS.""" - # Remove from the searchPaths if the dir empty of collection + # Remove from the SVG search paths if the directory is empty search_paths = self.svg_search_paths() collection_directories = os.listdir(local_collection_path()) diff --git a/resource_sharing/resource_handler/symbol_handler.py b/resource_sharing/resource_handler/symbol_handler.py index e17250b8..c4074d11 100644 --- a/resource_sharing/resource_handler/symbol_handler.py +++ b/resource_sharing/resource_handler/symbol_handler.py @@ -1,6 +1,7 @@ # coding=utf-8 import os import fnmatch +import logging try: from qgis.core import QgsStyleV2 as QgsStyle @@ -12,6 +13,8 @@ from resource_sharing.resource_handler.symbol_resolver_mixin import \ SymbolResolverMixin +LOGGER = logging.getLogger('QGIS Resource Sharing') +SYMBOL = 'symbol' class SymbolResourceHandler(BaseResourceHandler, SymbolResolverMixin): """Concrete class of the Symbol handler.""" @@ -20,154 +23,187 @@ class SymbolResourceHandler(BaseResourceHandler, SymbolResolverMixin): def __init__(self, collection_id): """Constructor of the base class.""" BaseResourceHandler.__init__(self, collection_id) - # Init the default style + # Initialize with the default style self.style = QgsStyle.defaultStyle() @classmethod def dir_name(self): - return 'symbol' + return SYMBOL def _get_parent_group_or_tag(self): - """Retrieve or create the parent group (QGIS2) or tag (QGIS3) for - the styles returns the group or tag id.""" + """Retrieve or create the parent group (for QGIS2) + or parent tag (for QGIS3) for the styles. + Returns the id of the (existing or new) group or tag.""" parent_group_name = '%s (%s)' % ( self.collection['name'], self.collection_id) try: + # QGIS 2 group = self.style.groupId(parent_group_name) if group != 0: return group return self.style.addGroup(parent_group_name) except AttributeError: - tag = self.style.tagId(parent_group_name) - if tag != 0: - return tag - return self.style.addTag(parent_group_name) + # not QGIS 2, so hopefully QGIS 3 + #tag = self.style.tagId(parent_group_name) + #if tag != 0: + # return tag + #return self.style.addTag(parent_group_name) + # We don't want to create an empty "parent" tag + return None def _get_child_group_tag(self, group_or_tag_id, file_name): - """Retrieve or create the child group (QGIS2) or the parent tag for - (QGIS3, no hierarchy) for the styles, returns the group or parent tag - id with a slash and the file_name as a way to fake a tree within QGIS3. + """Retrieve or create the child group (for QGIS2) or the tag + (QGIS3, no hierarchy) for the styles. + Returns the id of the group or tag id. + Use a slash and the file_name as a way of simulating a tree + in QGIS3. """ try: + # QGIS 2 group = self.style.groupId(file_name) if group != 0: return group return self.style.addGroup(file_name, group_or_tag_id) except AttributeError: - tag_name = self.style.tag(group_or_tag_id) + '/' + file_name + # not QGIS 2, so hopefully QGIS 3 + #tag_name = self.style.tag(group_or_tag_id) + '/' + file_name + tag_name = ('%s (%s)/') % (self.collection['name'], self.collection_id) + file_name tag = self.style.tagId(tag_name) if tag != 0: return tag return self.style.addTag(tag_name) def _get_child_groups_tags_ids(self): - """Retrieve child groups (QGIS2) or tags (QGIS3) ids.""" + """Retrieve ids for the child groups (for QGIS2) or tags (for QGIS3).""" parent_group_name = '%s (%s)' % ( self.collection['name'], self.collection_id) try: + # QGIS 2 return [self.style.groupId(n) for n in self.style.childGroupNames(parent_group_name)] except AttributeError: + # not QGIS 2, so hopefully QGIS 3 return [self.style.tagId(tag) for tag in self.style.tags() if tag.find(parent_group_name) == 0] def _get_symbols_for_group_or_tag(self, symbol_type, child_group_or_tag_id): - """Return all symbols names in the group id (QGIS2) or tag id - (QGIS3).""" + """Return all the symbols for the given group (QGIS2) or tag + (QGIS3) id.""" try: + # QGIS 2 return self.style.symbolsOfGroup( QgsStyle.SymbolEntity, child_group_or_tag_id) except AttributeError: + # not QGIS 2, so hopefully QGIS 3 return self.style.symbolsWithTag( QgsStyle.SymbolEntity, child_group_or_tag_id) def _group_or_tag(self, symbol_type, symbol_name, tag_or_group): - """Add to group (QGIS2) or tag (QGIS3).""" + """Add the symbol to a group (QGIS2) or tag the symbol (QGIS3).""" try: + # QGIS 2 self.style.group(QgsStyle.SymbolEntity, symbol_name, tag_or_group) except AttributeError: + # not QGIS 2, so hopefully QGIS 3 self.style.tagSymbol(QgsStyle.SymbolEntity, symbol_name, [self.style.tag(tag_or_group)]) def _group_or_tag_remove(self, group_or_tag_id): - """Remove a group or tag.""" + """Remove a group (QGIS 2) or tag (QGIS 3).""" try: + # QGIS 2 self.style.remove(QgsStyle.GroupEntity, group_or_tag_id) except AttributeError: - self.style.remove(QgsStyle.TagEntity, group_or_tag_id) + # not QGIS 2, so hopefully QGIS 3 + if group_or_tag_id is not None: + self.style.remove(QgsStyle.TagEntity, group_or_tag_id) def install(self): - """Install the symbol and collection from this collection into QGIS. - - We create a group with the name of the collection, a child group for - each xml file and save all the symbols and colorramp defined that xml - file into that child group. + """Install the symbols from this collection in the QGIS default style. + + For QGIS 2, a group with the name of the collection is created, + and for each of the XML files in the collection, a child group + is created, where the symbols and colorramps defined in the XML + file are stored. + For QGIS 3, for each of the XML files in the collection, a tag + is created, and the tag is used for the symbols and colorramps + defined in that XML file. """ - # Check if the dir exists, pass installing silently if it doesn't exist + # Skip installation if the directory does not exist if not os.path.exists(self.resource_dir): return - # Uninstall first in case of reinstalling + # Uninstall first (in case it is a reinstall) self.uninstall() - # Get all the symbol xml files under resource dirs + # Get all the symbol XML files in the collection symbol_files = [] + valid = 0 for item in os.listdir(self.resource_dir): file_path = os.path.join(self.resource_dir, item) if fnmatch.fnmatch(file_path, '*.xml'): symbol_files.append(file_path) + valid += 1 + #LOGGER.info('Symbol file: ' + file_path) - # If there's no symbol files don't do anything + # If there are no symbol files, there is nothing to do if len(symbol_files) == 0: return + # Only relevant for QGIS 2! group_or_tag_id = self._get_parent_group_or_tag() + #LOGGER.info('ID: ' + str(group_or_tag_id)) for symbol_file in symbol_files: file_name = os.path.splitext(os.path.basename(symbol_file))[0] - # FIXME: no groups in QGIS3!!! - - child_id = self._get_child_group_tag(group_or_tag_id, file_name) - # Modify the symbol file first + # Groups in QGIS2, tags in QGIS3... + groupOrTag_id = self._get_child_group_tag(group_or_tag_id, file_name) + #LOGGER.info('groupOrTag_id: ' + str(groupOrTag_id)) + # Modify the symbol file to fix image and SVG paths self.resolve_dependency(symbol_file) - # Add all symbols and colorramps and group it + # Add all symbols and colorramps and group / tag them symbol_xml_extractor = SymbolXMLExtractor(symbol_file) - for symbol in symbol_xml_extractor.symbols: symbol_name = '%s (%s)' % (symbol['name'], self.collection_id) - # self.resolve_dependency(symbol['symbol']) - if self.style.addSymbol(symbol_name, symbol['symbol'], True): + #LOGGER.info('symbol_name: ' + symbol_name) + # self.resolve_dependency(symbol[SYMBOL]) + if self.style.addSymbol(symbol_name, symbol[SYMBOL], True): self._group_or_tag(QgsStyle.SymbolEntity, symbol_name, - child_id) - + groupOrTag_id) for colorramp in symbol_xml_extractor.colorramps: colorramp_name = '%s (%s)' % ( colorramp['name'], self.collection_id) + #LOGGER.info('colorramp_name: ' + colorramp_name) if self.style.addColorRamp( colorramp_name, colorramp['colorramp'], True): self._group_or_tag( - QgsStyle.ColorrampEntity, colorramp_name, child_id) + QgsStyle.ColorrampEntity, colorramp_name, groupOrTag_id) + #LOGGER.info('XML file: ' + file_name + ' Symbols: ' + str(len(symbol_xml_extractor.symbols)) + ' ColorRamps: ' + str(len(symbol_xml_extractor.colorramps))) + + if valid >= 0: + self.collection[SYMBOL] = valid def uninstall(self): """Uninstall the symbols from QGIS.""" - # Get the parent group id + # Get the parent group id (not relevant for QGIS 3) group_or_tag_id = self._get_parent_group_or_tag() + # Get the ids of the groups / tags that contain symbols child_groups_or_tags_ids = self._get_child_groups_tags_ids() for child_group_id in child_groups_or_tags_ids: - # Get all the symbol from this child group and remove them + # Get all the symbol from this tag / child group and remove them symbols = self._get_symbols_for_group_or_tag( QgsStyle.SymbolEntity, child_group_id) for symbol in symbols: self.style.removeSymbol(symbol) - # Get all the colorramps and remove them + # Get all the colorramps for this tag / child group and remove them colorramps = self._get_symbols_for_group_or_tag( QgsStyle.ColorrampEntity, child_group_id) for colorramp in colorramps: self.style.removeColorRamp(colorramp) - # Remove this child group + # Remove this tag / child group self._group_or_tag_remove(child_group_id) - # Remove parent group: + # Remove parent group (only relevant for QGIS 3): self._group_or_tag_remove(group_or_tag_id) diff --git a/resource_sharing/resource_handler/symbol_resolver_mixin.py b/resource_sharing/resource_handler/symbol_resolver_mixin.py index 970da22e..6d0ef1e7 100644 --- a/resource_sharing/resource_handler/symbol_resolver_mixin.py +++ b/resource_sharing/resource_handler/symbol_resolver_mixin.py @@ -9,18 +9,20 @@ class SymbolResolverMixin(object): - """Mixin for Resources Handler that need to resolve image symbol path.""" + """Mixin for Resources Handler that need to resolve SVG + and image symbol paths.""" def resolve_dependency(self, xml_path): - """Modify the xml and resolve the resources dependency. + """Modify the XML and resolve dependencies. - We need to update any path dependency of downloaded symbol so that - the path points to the right path after it's installed. + Update paths to downloaded symbol so that the paths + point to the right location. - For now, we only update the svg/image path to the svg/ directory of - the collection if the svg exists. + For now, we only update the svg/image paths to the + svg directory of the collection if the SVG file exists + there. - :param xml_path: The path to the style xml file. + :param xml_path: The path to the XML style file. :type xml_path: str """ with open(xml_path, 'rb') as xml_file: @@ -36,14 +38,15 @@ def resolve_dependency(self, xml_path): def fix_xml_node(xml, collection_path, search_paths): """Loop through the XML nodes to resolve the SVG and image paths. - :param xml: The xml string of the symbol (or full xml symbols definition) + :param xml: The XML string of the symbol (or full XML symbol definition) :type xml: str - :param collection_path: The downloaded collection path in local so we - know where to lookup the image/svg inside the collection. + :param collection_path: The downloaded collection local file + system path, where can lookup the images/SVGs of the + collection. :type collection_path: str - :param search_paths: List of paths to search the image/svg path. + :param search_paths: List of paths to search for images/SVGs. :type search_paths: str """ root = ET.fromstring(xml) @@ -61,27 +64,27 @@ def fix_xml_node(xml, collection_path, search_paths): def resolve_path(path, collection_path, search_paths): """Try to resolve the SVG and image path. - This is the procedure to check it by order: - * It might be a full local path, check if it exists - * It might be a url (either local file system or http(s)) - * Check in the 'svg' collection path - * Check in the 'image' collection path + This is the procedure to check it: + * It might be a complete local file system path, check if it exists + * It might be a URL (either local file system or http(s)) + * Check in the 'svg' directory of the collection + * Check in the 'image' directory of the collection * Check in the search_paths :param path: The original path. :type path: str - :param collection_path: The downloaded collection path in local. + :param collection_path: The local file system path for the collection. :type collection_path: str - :param search_paths: List of paths to search the image/svg path + :param search_paths: List of paths to search for images/SVGs. :type search_paths: str """ - # It might be a full path + # It might be a complete local file system path if QFile(path).exists(): return QFileInfo(path).canonicalFilePath() - # It might be a url + # It might be a URL if '://' in path: url = QUrl(path) if url.isValid() and url.scheme() != '': @@ -91,21 +94,21 @@ def resolve_path(path, collection_path, search_paths): if QFile(path).exists(): return QFileInfo(path).canonicalFilePath() else: - # URL to pointing to online resource + # URL pointing to online resource return path - # Check in the svg collection path + # Check in the 'svg' directory of the collection file_name = path_leaf(path) svg_collection_path = os.path.join(collection_path, 'svg', file_name) if QFile(svg_collection_path).exists(): return QFileInfo(svg_collection_path).canonicalFilePath() - # Check in the image collection path + # Check in the 'image' directory of the collection image_collection_path = os.path.join(collection_path, 'image', file_name) if QFile(image_collection_path).exists(): return QFileInfo(image_collection_path).canonicalFilePath() - # Still not found, check in the search_paths + # Still not found, check the search_paths for search_path in search_paths: local_path = os.path.join(search_path, path) if QFile(local_path).exists():