Projet

Général

Profil

Anomalie #284 » menu_builder_dialog.py

Leslie Lemaire, 22/06/2020 18:22

 
# -*- coding: utf-8 -*-
"""
MenuBuilder - Create your own menus with your favorite layers

copyright : (C) 2015 by Oslandia
email : infos@oslandia.com

/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
import os
import re
import json
from contextlib import contextmanager
from collections import defaultdict
from functools import wraps, partial

import psycopg2

from PyQt5.QtCore import Qt, QRect, QSortFilterProxyModel
from PyQt5.QtWidgets import (
QAction, QMessageBox, QDialog, QMenu, QTreeView,
QAbstractItemView, QDockWidget, QWidget, QVBoxLayout,
QSizePolicy, QLineEdit, QDialogButtonBox
)

from PyQt5.QtGui import (
QIcon, QStandardItem,
QStandardItemModel
)
from qgis.core import (
QgsProject, QgsBrowserModel, QgsDataSourceUri, QgsSettings,
QgsCredentials, QgsVectorLayer, QgsMimeDataUtils, QgsRasterLayer
)

from .menu_builder_dialog_base import Ui_Dialog

QGIS_MIMETYPE = 'application/x-vnd.qgis.qgis.uri'


ICON_MAPPER = {
'postgres': ":/plugins/MenuBuilder/resources/postgis.svg",
'WMS': ":/plugins/MenuBuilder/resources/wms.svg",
'WFS': ":/plugins/MenuBuilder/resources/wfs.svg",
'OWS': ":/plugins/MenuBuilder/resources/ows.svg",
'spatialite': ":/plugins/MenuBuilder/resources/spatialite.svg",
'mssql': ":/plugins/MenuBuilder/resources/mssql.svg",
'gdal': ":/plugins/MenuBuilder/resources/gdal.svg",
'ogr': ":/plugins/MenuBuilder/resources/ogr.svg",
}


class MenuBuilderDialog(QDialog, Ui_Dialog):

def __init__(self, uiparent):
super().__init__()

self.setupUi(self)

# reference to caller
self.uiparent = uiparent

self.combo_profile.lineEdit().setPlaceholderText(self.tr("Profile name"))

# add icons
self.button_add_menu.setIcon(QIcon(":/plugins/MenuBuilder/resources/plus.svg"))
self.button_delete_profile.setIcon(QIcon(":/plugins/MenuBuilder/resources/delete.svg"))

# custom qtreeview
self.target = CustomQtTreeView(self)
self.target.setGeometry(QRect(440, 150, 371, 451))
self.target.setAcceptDrops(True)
self.target.setDragEnabled(True)
self.target.setDragDropMode(QAbstractItemView.DragDrop)
self.target.setObjectName("target")
self.target.setDropIndicatorShown(True)
self.target.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.target.setHeaderHidden(True)
sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.target.sizePolicy().hasHeightForWidth())
self.target.setSizePolicy(sizePolicy)
self.target.setAutoFillBackground(True)
self.verticalLayout_2.addWidget(self.target)
self.menumodel = MenuTreeModel(self)
self.target.setModel(self.menumodel)
self.target.setAnimated(True)
self.target.setDefaultDropAction(Qt.MoveAction)

self.browser = QgsBrowserModel()
self.browser.initialize()
self.source.setModel(self.browser)
self.source.setHeaderHidden(True)
self.source.setDragEnabled(True)
self.source.setSelectionMode(QAbstractItemView.ExtendedSelection)

# add a dock widget
self.dock_widget = QDockWidget("Menus")
self.dock_widget.resize(400, 300)
self.dock_widget.setFloating(True)
self.dock_widget.setObjectName(self.tr("Menu Tree"))
self.dock_widget_content = QWidget()
self.dock_widget.setWidget(self.dock_widget_content)
dock_layout = QVBoxLayout()
self.dock_widget_content.setLayout(dock_layout)
self.dock_view = DockQtTreeView(self.dock_widget_content)
self.dock_view.setDragDropMode(QAbstractItemView.DragOnly)
self.dock_menu_filter = QLineEdit()
self.dock_menu_filter.setPlaceholderText(self.tr("Filter by table description (postgis only)"))
dock_layout.addWidget(self.dock_menu_filter)
dock_layout.addWidget(self.dock_view)
self.dock_view.setHeaderHidden(True)
self.dock_view.setDragEnabled(True)
self.dock_view.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.dock_view.setAnimated(True)
self.dock_view.setObjectName("treeView")
self.proxy_model = LeafFilterProxyModel(self)
self.proxy_model.setFilterRole(Qt.ToolTipRole)
self.proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)

self.profile_list = []
self.table = 'qgis_menubuilder_metadata'

self.layer_handler = {
'vector': self.load_vector,
'raster': self.load_raster
}

# connect signals and handlers
self.combo_database.activated.connect(partial(self.set_connection, dbname=None))
self.combo_schema.activated.connect(self.update_profile_list)
self.combo_profile.activated.connect(partial(self.update_model_idx, self.menumodel))
self.button_add_menu.released.connect(self.add_menu)
self.button_delete_profile.released.connect(self.delete_profile)
self.dock_menu_filter.textEdited.connect(self.filter_update)
self.dock_view.doubleClicked.connect(self.load_from_index)

self.buttonBox.rejected.connect(self.reject)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.button(QDialogButtonBox.Apply).clicked.connect(self.apply)

def filter_update(self):
text = self.dock_menu_filter.displayText()
self.proxy_model.setFilterRegExp(text)

def show_dock(self, state, profile=None, schema=None):
if not state:
# just hide widget
self.dock_widget.setVisible(state)
return
# dock must be read only and deepcopy of model is not supported (c++ inside!)
self.dock_model = MenuTreeModel(self)
if profile:
# bypass combobox
self.update_model(self.dock_model, schema, profile)
else:
self.update_model_idx(self.dock_model, self.combo_profile.currentIndex())
self.dock_model.setHorizontalHeaderLabels(["Menus"])
self.dock_view.setEditTriggers(QAbstractItemView.NoEditTriggers)
self.proxy_model.setSourceModel(self.dock_model)
self.dock_view.setModel(self.proxy_model)
self.dock_widget.setVisible(state)

def show_menus(self, state, profile=None, schema=None):
if state:
self.load_menus(profile=profile, schema=schema)
return
# remove menus
for menu in self.uiparent.menus:
self.uiparent.iface.mainWindow().menuBar().removeAction(menu.menuAction())

def add_menu(self):
"""
Add a menu inside qtreeview
"""
item = QStandardItem('NewMenu')
item.setIcon(QIcon(':/plugins/MenuBuilder/resources/menu.svg'))
# select current index selected and insert as a sibling
brother = self.target.selectedIndexes()

if not brother or not brother[0].parent():
# no selection, add menu at the top level
self.menumodel.insertRow(self.menumodel.rowCount(), item)
return

parent = self.menumodel.itemFromIndex(brother[0].parent())
if not parent:
self.menumodel.insertRow(self.menumodel.rowCount(), item)
return
parent.appendRow(item)

def update_database_list(self):
"""update list of defined postgres connections"""
settings = QgsSettings()
settings.beginGroup("/PostgreSQL/connections")
keys = settings.childGroups()
self.combo_database.clear()
self.combo_schema.clear()
self.menumodel.clear()
self.combo_database.addItems(keys)
self.combo_database.setCurrentIndex(-1)
settings.endGroup()
# clear profile list
self.combo_profile.clear()
self.combo_profile.setCurrentIndex(-1)

def set_connection(self, databaseidx, dbname=None):
"""
Connect to selected postgresql database
"""
selected = self.combo_database.itemText(databaseidx) or dbname
if not selected:
return

settings = QgsSettings()
settings.beginGroup("/PostgreSQL/connections/{}".format(selected))

if not settings.contains("database"):
# no entry?
QMessageBox.critical(self, "Error", "There is no defined database connection")
return

uri = QgsDataSourceUri()

settingsList = ["service", "host", "port", "database", "username", "password"]
service, host, port, database, username, password = map(
lambda x: settings.value(x, "", type=str), settingsList)

useEstimatedMetadata = settings.value("estimatedMetadata", False, type=bool)
sslmode = settings.enumValue("sslmode", uri.SslPrefer)

settings.endGroup()

if service:
uri.setConnection(service, database, username, password, sslmode)
else:
uri.setConnection(host, port, database, username, password, sslmode)

uri.setUseEstimatedMetadata(useEstimatedMetadata)

# connect to db
try:
self.connect_to_uri(uri)
except self.pg_error_types():
QMessageBox.warning(
self,
"Plugin MenuBuilder: Message",
self.tr("The database containing Menu's configuration is unavailable"),
QMessageBox.Ok,
)
# connection not available
return False

# update schema list
self.update_schema_list()
return True

@contextmanager
def transaction(self):
try:
yield
self.connection.commit()
except self.pg_error_types() as e:
self.connection.rollback()
raise e

def check_connected(func):
"""
Decorator that checks if a database connection is active before executing function
"""
@wraps(func)
def wrapped(inst, *args, **kwargs):
if not getattr(inst, 'connection', False):
QMessageBox(
QMessageBox.Warning,
"Menu Builder",
inst.tr("Not connected to any database, please select one"),
QMessageBox.Ok,
inst
)
return
if inst.connection.closed:
QMessageBox(
QMessageBox.Warning,
"Menu Builder",
inst.tr("Not connected to any database, please select one"),
QMessageBox.Ok,
inst
)
return
return func(inst, *args, **kwargs)
return wrapped

def connect_to_uri(self, uri):
self.close_connection()
self.host = uri.host() or os.environ.get('PGHOST')
self.port = uri.port() or os.environ.get('PGPORT')

username = uri.username() or os.environ.get('PGUSER') or os.environ.get('USER')
password = uri.password() or os.environ.get('PGPASSWORD')
conninfo = uri.connectionInfo()

while True:
try:
self.connection = psycopg2.connect(uri.connectionInfo(), application_name="QGIS:MenuBuilder")
break
except self.pg_error_types() as e:
err = str(e) or "Erreur d'authentification. Vérifiez les informations saisies."
ok, username, password = QgsCredentials.instance().get(
conninfo, username, password, err)
if not ok:
raise e
if username:
uri.setUsername(username)
if password:
uri.setPassword(password)
QgsCredentials.instance().put(conninfo, username, password)
self.pgencoding = self.connection.encoding

return True

def pg_error_types(self):
return (
psycopg2.InterfaceError,
psycopg2.OperationalError,
psycopg2.ProgrammingError
)

@check_connected
def update_schema_list(self):
self.combo_schema.clear()
with self.transaction():
cur = self.connection.cursor()
cur.execute("""
select nspname
from pg_namespace
where nspname not ilike 'pg_%'
and nspname not in ('pg_catalog', 'information_schema')
""")
schemas = [row[0] for row in cur.fetchall()]
self.combo_schema.addItems(schemas)

@check_connected
def update_profile_list(self, schemaidx):
"""
update profile list from database
"""
schema = self.combo_schema.itemText(schemaidx)
with self.transaction():
cur = self.connection.cursor()
cur.execute("""
select 1
from pg_tables
where schemaname = '{0}'
and tablename = '{1}'
union
select 1
from pg_matviews
where schemaname = '{0}'
and matviewname = '{1}'
union
select 1
from pg_views
where schemaname = '{0}'
and viewname = '{1}'
""".format(schema, self.table))
tables = cur.fetchone()
if not tables:
box = QMessageBox(
QMessageBox.Warning,
"Menu Builder",
self.tr("Table '{}.{}' not found in this database, "
"would you like to create it now ?")
.format(schema, self.table),
QMessageBox.Cancel | QMessageBox.Yes,
self
)
ret = box.exec_()
if ret == QMessageBox.Cancel:
return False
elif ret == QMessageBox.Yes:
cur.execute("""
create table {}.{} (
id serial,
name varchar,
profile varchar,
model_index varchar,
datasource_uri text
)
""".format(schema, self.table))
self.connection.commit()
return False

cur.execute("""
select distinct(profile) from {}.{}
""".format(schema, self.table))
profiles = [row[0] for row in cur.fetchall()]
saved_profile = self.combo_profile.currentText()
self.combo_profile.clear()
self.combo_profile.addItems(profiles)
self.combo_profile.setCurrentIndex(self.combo_profile.findText(saved_profile))

@check_connected
def delete_profile(self):
"""
Delete profile currently selected
"""
idx = self.combo_profile.currentIndex()
schema = self.combo_schema.currentText()
profile = self.combo_profile.itemText(idx)
box = QMessageBox(
QMessageBox.Warning,
"Menu Builder",
self.tr("Delete '{}' profile ?").format(profile),
QMessageBox.Cancel | QMessageBox.Yes,
self
)
ret = box.exec_()
if ret == QMessageBox.Cancel:
return False
elif ret == QMessageBox.Yes:
self.combo_profile.removeItem(idx)
with self.transaction():
cur = self.connection.cursor()
cur.execute("""
delete from {}.{}
where profile = '{}'
""".format(schema, self.table, profile))
self.menumodel.clear()
self.combo_profile.setCurrentIndex(-1)

def update_model_idx(self, model, profile_index):
"""
wrapper that checks combobox
"""
profile = self.combo_profile.itemText(profile_index)
schema = self.combo_schema.currentText()
self.update_model(model, schema, profile)

def sortby_modelindex(self, rows):
return sorted(
rows,
key=lambda line: '/'.join(
['{:04}'.format(elem[0]) for elem in json.loads(line[2])]
))

@check_connected
def update_model(self, model, schema, profile):
"""
Update the model by retrieving the profile given in database
"""
menudict = {}

with self.transaction():
cur = self.connection.cursor()
select = """
select name, profile, model_index, datasource_uri
from {}.{}
where profile = '{}'
""".format(schema, self.table, profile)
cur.execute(select)
rows = cur.fetchall()
model.clear()
for name, profile, model_index, datasource_uri in self.sortby_modelindex(rows):
menu = model.invisibleRootItem()
indexes = json.loads(model_index)
parent = ''
for idx, subname in indexes[:-1]:
parent += '{}-{}/'.format(idx, subname)
if parent in menudict:
# already created entry
menu = menudict[parent]
continue
# create menu
item = QStandardItem(subname)
uri_struct = QgsMimeDataUtils.Uri(datasource_uri)
item.setData(uri_struct)
item.setIcon(QIcon(':/plugins/MenuBuilder/resources/menu.svg'))
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable |
Qt.ItemIsEnabled | Qt.ItemIsDropEnabled |
Qt.ItemIsEditable)
item.setWhatsThis("menu")
menu.appendRow(item)
menudict[parent] = item
# set current menu to the new created item
menu = item

# add leaf (layer item)
item = QStandardItem(name)
uri_struct = QgsMimeDataUtils.Uri(datasource_uri)
# fix layer name instead of table name
# usefull when the layer has been renamed in menu
uri_struct.name = name
if uri_struct.providerKey in ICON_MAPPER:
item.setIcon(QIcon(ICON_MAPPER[uri_struct.providerKey]))
item.setData(uri_struct)
# avoid placing dragged layers on it
item.setDropEnabled(False)
if uri_struct.providerKey == 'postgres':
# set tooltip to postgres comment
comment = self.get_table_comment(uri_struct.uri)
item.setToolTip(comment)
menudict[parent].appendRow(item)

@check_connected
def save_changes(self, save_to_db=True):
"""
Save changes in the postgres table
"""
schema = self.combo_schema.currentText()
profile = self.combo_profile.currentText()
if not profile:
QMessageBox(
QMessageBox.Warning,
"Menu Builder",
self.tr("Profile cannot be empty"),
QMessageBox.Ok,
self
).exec_()
return False

if save_to_db:
try:
with self.transaction():
cur = self.connection.cursor()
cur.execute("delete from {}.{} where profile = '{}'".format(
schema, self.table, profile))
for item, data in self.target.iteritems():
if not data:
continue
cur.execute("""
insert into {}.{} (name,profile,model_index,datasource_uri)
values (%s, %s, %s, %s)
""".format(schema, self.table), (
item[-1][1],
profile,
json.dumps(item),
data.data())
)
except Exception as exc:
QMessageBox(
QMessageBox.Warning,
"Menu Builder",
exc.message.decode(self.pgencoding),
QMessageBox.Ok,
self
).exec_()
return False

self.save_session(
self.combo_database.currentText(),
schema,
profile,
self.activate_dock.isChecked(),
self.activate_menubar.isChecked()
)
self.update_profile_list(self.combo_schema.currentIndex())
self.show_dock(self.activate_dock.isChecked())
self.show_menus(self.activate_menubar.isChecked())
return True

@check_connected
def load_menus(self, profile=None, schema=None):
"""
Load menus in the main windows qgis bar
"""
if not schema:
schema = self.combo_schema.currentText()
if not profile:
profile = self.combo_profile.currentText()
# remove previous menus
for menu in self.uiparent.menus:
self.uiparent.iface.mainWindow().menuBar().removeAction(menu.menuAction())

with self.transaction():
cur = self.connection.cursor()
select = """
select name, profile, model_index, datasource_uri
from {}.{}
where profile = '{}'
""".format(schema, self.table, profile)
cur.execute(select)
rows = cur.fetchall()
# item accessor ex: '0-menu/0-submenu/1-item/'
menudict = {}
# reference to parent item
parent = ''
# reference to qgis main menu bar
menubar = self.uiparent.iface.mainWindow().menuBar()

for name, profile, model_index, datasource_uri in self.sortby_modelindex(rows):
uri_struct = QgsMimeDataUtils.Uri(datasource_uri)
indexes = json.loads(model_index)
# root menu
parent = '{}-{}/'.format(indexes[0][0], indexes[0][1])
if parent not in menudict:
menu = QMenu(self.uiparent.iface.mainWindow())
self.uiparent.menus.append(menu)
menu.setObjectName(indexes[0][1])
menu.setTitle(indexes[0][1])
menubar.insertMenu(
self.uiparent.iface.firstRightStandardMenu().menuAction(),
menu)
menudict[parent] = menu
else:
# menu already there
menu = menudict[parent]

for idx, subname in indexes[1:-1]:
# intermediate submenus
parent += '{}-{}/'.format(idx, subname)
if parent not in menudict:
submenu = menu.addMenu(subname)
submenu.setObjectName(subname)
submenu.setTitle(subname)
menu = submenu
# store it for later use
menudict[parent] = menu
continue
# already treated
menu = menudict[parent]

# last item = layer
layer = QAction(name, self)

if uri_struct.providerKey in ICON_MAPPER:
layer.setIcon(QIcon(ICON_MAPPER[uri_struct.providerKey]))

if uri_struct.providerKey == 'postgres':
# set tooltip to postgres comment
comment = self.get_table_comment(uri_struct.uri)
layer.setStatusTip(comment)
layer.setToolTip(comment)

layer.setData(uri_struct.uri)
layer.setWhatsThis(uri_struct.providerKey)
layer.triggered.connect(self.layer_handler[uri_struct.layerType])
menu.addAction(layer)

def get_table_comment(self, uri):
schema, table = re.match(r'.*table="(.*"\.".*)"', uri) \
.group(1) \
.strip() \
.split('"."')

with self.transaction():
cur = self.connection.cursor()
select = """
select description from pg_description
join pg_class on pg_description.objoid = pg_class.oid
join pg_namespace on pg_class.relnamespace = pg_namespace.oid
where relname = '{}' and nspname='{}'
""".format(table, schema)
cur.execute(select)
row = cur.fetchone()
if row:
return row[0]
return ''

def load_from_index(self, index):
"""Load layers from selected item index"""
item = self.dock_model.itemFromIndex(self.proxy_model.mapToSource(index))
if item.whatsThis() == 'menu':
return
if item.data().layerType == 'vector':
layer = QgsVectorLayer(
item.data().uri, # uri
item.text(), # layer name
item.data().providerKey # provider name
)
elif item.data().layerType == 'raster':
layer = QgsRasterLayer(
item.data().uri, # uri
item.text(), # layer name
item.data().providerKey # provider name
)
if not layer:
return
QgsProject.instance().addMapLayer(layer)

def load_vector(self):
action = self.sender()

layer = QgsVectorLayer(
action.data(), # uri
action.text(), # layer name
action.whatsThis() # provider name
)
QgsProject.instance().addMapLayer(layer)

def load_raster(self):
action = self.sender()
layer = QgsRasterLayer(
action.data(), # uri
action.text(), # layer name
action.whatsThis() # provider name
)
QgsProject.instance().addMapLayer(layer)

def accept(self):
if self.save_changes():
QDialog.reject(self)
self.close_connection()

def apply(self):
if self.save_changes(save_to_db=False):
QDialog.reject(self)
self.close_connection()

def reject(self):
self.close_connection()
QDialog.reject(self)

def close_connection(self):
"""close current pg connection if exists"""
if getattr(self, 'connection', False):
if self.connection.closed:
return
self.connection.close()

def save_session(self, database, schema, profile, dock, menubar):
"""save current profile for next session"""
settings = QgsSettings()
settings.setValue("MenuBuilder/database", database)
settings.setValue("MenuBuilder/schema", schema)
settings.setValue("MenuBuilder/profile", profile)
settings.setValue("MenuBuilder/dock", dock)
settings.setValue("MenuBuilder/menubar", menubar)

def restore_session(self):
settings = QgsSettings()
database = settings.value("MenuBuilder/database", False)
schema = settings.value("MenuBuilder/schema", 'public')
profile = settings.value("MenuBuilder/profile", False)
dock = settings.value("MenuBuilder/dock", False)
menubar = settings.value("MenuBuilder/menubar", False)
if not any([database, profile]):
return

connected = self.set_connection(0, dbname=database)
if not connected:
# don't try to continue
return

self.show_dock(bool(dock), profile=profile, schema=schema)
if bool(dock):
self.uiparent.iface.addDockWidget(Qt.LeftDockWidgetArea, self.dock_widget)
self.show_menus(bool(menubar), profile=profile, schema=schema)
self.close_connection()


class CustomQtTreeView(QTreeView):

def dragEnterEvent(self, event):
if not event.mimeData():
# don't drag menu entry
return False
# refuse if it's not a qgis mimetype
if event.mimeData().hasFormat(QGIS_MIMETYPE):
event.acceptProposedAction()

def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete:
model = self.selectionModel().model()
parents = defaultdict(list)
for idx in self.selectedIndexes():
parents[idx.parent()].append(idx)
for parent, idx_list in parents.items():
for diff, index in enumerate(idx_list):
model.removeRow(index.row() - diff, parent)
elif event.key() == Qt.Key_Return:
pass
else:
super().keyPressEvent(event)

def iteritems(self, level=0):
"""
Dump model to store in database.
Generates each level recursively
"""
rowcount = self.model().rowCount()
for itemidx in range(rowcount):
# iterate over parents
parent = self.model().itemFromIndex(self.model().index(itemidx, 0))
for item, uri in self.traverse_tree(parent, []):
yield item, uri

def traverse_tree(self, parent, identifier):
"""
Iterate over childs, recursively
"""
identifier.append([parent.row(), parent.text()])
for row in range(parent.rowCount()):
child = parent.child(row)
if child.hasChildren():
# child is a menu ?
for item in self.traverse_tree(child, identifier):
yield item
identifier.pop()
else:
# add leaf
sibling = list(identifier)
sibling.append([child.row(), child.text()])
yield sibling, child.data()


class DockQtTreeView(CustomQtTreeView):

def keyPressEvent(self, event):
"""override keyevent to avoid deletion of items in the dock"""
pass


class MenuTreeModel(QStandardItemModel):

def dropMimeData(self, mimedata, action, row, column, parentIndex):
"""
Handles the dropping of an item onto the model.
De-serializes the data and inserts it into the model.
"""
# decode data using qgis helpers
uri_list = QgsMimeDataUtils.decodeUriList(mimedata)
if not uri_list:
return False
# find parent item
parent_item = self.itemFromIndex(parentIndex)
if not parent_item:
return False

items = []
for uri in uri_list:
item = QStandardItem(uri.name)
item.setData(uri)
# avoid placing dragged layers on it
item.setDropEnabled(False)
if uri.providerKey in ICON_MAPPER:
item.setIcon(QIcon(ICON_MAPPER[uri.providerKey]))
items.append(item)

if row == -1:
# dropped on a Menu
# add as a child at the end
parent_item.appendRows(items)
else:
# add items at the separator shown
parent_item.insertRows(row, items)

return True

def mimeData(self, indexes):
"""
Used to serialize data
"""
if not indexes:
return 0
items = [self.itemFromIndex(idx) for idx in indexes]
if not items:
return 0
if not all(it.data() for it in items):
return 0
# reencode items
mimedata = QgsMimeDataUtils.encodeUriList([item.data() for item in items])
return mimedata

def mimeTypes(self):
return [QGIS_MIMETYPE]

def supportedDropActions(self):
return Qt.CopyAction | Qt.MoveAction


class LeafFilterProxyModel(QSortFilterProxyModel):
"""
Class to override the following behaviour:
If a parent item doesn't match the filter,
none of its children will be shown.

This Model matches items which are descendants
or ascendants of matching items.
"""

def filterAcceptsRow(self, row_num, source_parent):
"""Overriding the parent function"""

# Check if the current row matches
if self.filter_accepts_row_itself(row_num, source_parent):
return True

# Traverse up all the way to root and check if any of them match
if self.filter_accepts_any_parent(source_parent):
return True

# Finally, check if any of the children match
return self.has_accepted_children(row_num, source_parent)

def filter_accepts_row_itself(self, row_num, parent):
return super(LeafFilterProxyModel, self).filterAcceptsRow(row_num, parent)

def filter_accepts_any_parent(self, parent):
"""
Traverse to the root node and check if any of the
ancestors match the filter
"""
while parent.isValid():
if self.filter_accepts_row_itself(parent.row(), parent.parent()):
return True
parent = parent.parent()
return False

def has_accepted_children(self, row_num, parent):
"""
Starting from the current node as root, traverse all
the descendants and test if any of the children match
"""
model = self.sourceModel()
source_index = model.index(row_num, 0, parent)

children_count = model.rowCount(source_index)
for i in range(children_count):
if self.filterAcceptsRow(i, source_index):
return True
return False
    (1-1/1)