[QGIS-Developer] Difficulties with custom bad layer handler in plugin code
Nyall Dawson
nyall.dawson at gmail.com
Mon Jul 22 16:05:32 PDT 2019
On Tue, 23 Jul 2019 at 00:58, Áron Gergely <aron.gergely at rasterra.nl> wrote:
>
>
>
> On 22/07/2019 01:11, Nyall Dawson wrote:
> > On Sun, 21 Jul 2019 at 21:16, Áron Gergely<aron.gergely at rasterra.nl> wrote:
> >
> >> This actually works, for the 1st time when I open a project file with bad layers. Then the 2nd time, QGIS always crashes.
> >> It seems once I replaced the bad layer handler during project start, QGIS is not able to open a subsequent new project properly.
> > Couldn't you achieve the same result by setting your bad layer handler
> > directly in initGui?
> No, for some strange reason the bad layer handler would not get assigned
> when I tried within initGui.
> I tried to find out why by looking at debug messages on qgis launch,
> reading the source code,
> but it is a lot to take in with my rusty C skills so I have not found
> the answer.
Ehhhh- right. The built-in handler is set AFTER plugins are loaded, so
it wipes out any custom handler implemented by a plugin! I think this
is a bug -- as it makes that API call rather useless. PR inbound.
> But I reckoned it may be due to the plugin being loaded before a
> QgsProject singleton would be initialized.
> >> # TODO: after this, call the GUI file dialog as in the default bad layer handler, to allow the user to fix
> >> # any other broken layers... not sure how to do this
> > The bad news is -- unfortunately you cannot. The inbuilt bad layer
> > handler isn't exposed to Python in any way, and as soon as you set a
> > new bad layer
> > handler method the existing inbuilt one is completely deleted.
> Understood - if that part is not exposed to the python API then I will
> not pursue the above. Thanks for letting me know!
Well, it's possible we could work around this by NOT deleting the
inbuilt one when a new one is set. But honestly, I think your use case
is better server by my proposed API addition.
> Thanks for your insight Nyall!
> Sounds bad with that IT Department. :(
>
> But yes, I think that would be helpful. (I wonder who else hit a wall
> with the broken paths? )
I suspect lots of people. Personally, as a user, everytime I see that
dialog popup a little bit dies inside of me...
> Although my problem (or rather, our problem with Raymond) is similar but
> not exactly the same:
Nope - it IS the same ;) You can see it in
https://github.com/qgis/QGIS/pull/30842
> We created a plugin that helps to achieve consistent map layouts: the
> plugin itself ships with the correct resources (logos, symbols, and a
> world map layer)
> for specific map layouting tasks and has a simple workflow via dialogs
> that creates the right layouts which the users would fill in or adapt to
> meet their needs.
>
> The problem is that since we ship the resources with the plugin, the
> resource paths are on the plugin path which is different across machines.
> So when users create map layouts, save the project file and pass it to
> another user to work on they see missing items and broken layer path,
> even though they have the plugin installed.
So what you would do here is implement a custom pre-processor which
detects some hardcoded path to the plugin resource, and swaps it out
with the actual path to the python plugin. E.g.
def my_processor(path):
if path.endswith('my_plugins_super_north_arrow.svg'):
return os.path.join(os.path.dirname(__file__),
'resources/my_plugins_super_north_arrow.svg')
return path
QgsPathResolver.setPathPreprocessor(my_processor)
Basically - whenever a path ending in
'my_plugins_super_north_arrow.svg' is requested, it gets (silently)
modified to point to a subfolder off your plugin's actual runtime
location. You'd just need to make sure the intercepted resources have
a distinct enough name to not clash with other (non-plugin) paths!
(e.g. don't just check for "north_arrow.svg"!).
Nyall
> On the longer term we believe it would be good to have a resource
> sharing system that is native to QGIS.
> That would make it very easy to share resources for plugins as well,
> without needing to instruct the users to get the resource sharing
> plugin, connect to resource repo X and pull resources a, b, c,... and so on.
>
> I had discussions about this with Raymond and we are looking to write a
> QEP about 'native' resource sharing.
>
> We might as well be able to live without a solution for now, as native
> resource sharing would answer our needs best.
>
> Best regards,
> Aron
> >> Here is my example in code:
> >>
> >> class MyBadLayerHandler(QgsProjectBadLayerHandler):
> >> """
> >> This is a custom bad layer handler that would work as the default one, except it would automatically fix
> >> a specific layer that is broken.
> >> """
> >>
> >> def __init__(self, path):
> >> QgsProjectBadLayerHandler.__init__(self)
> >> self.validLayerPath = path
> >>
> >> def handleBadLayers(self, domNodes):
> >> # Some logic to look for a specific DomNode to fix, oversimplified here:
> >> for dom in domNodes:
> >> dataSource = self.dataSource(dom)
> >>
> >> if dataSource is 'the broken path to the layer I want to fix':
> >> # set correct data source then force re-read
> >> self.setDataSource(domNodes, self.validLayerPath)
> >> QgsProject.instance().readLayer(dom)
> >>
> >> # TODO: after this, call the GUI file dialog as in the default bad layer handler, to allow the user to fix
> >> # any other broken layers... not sure how to do this
> >>
> >>
> >> class MyPlugin:
> >> """My plugin"""
> >>
> >> def __init__(self):
> >> self.validPath = 'valid path to layer'
> >> self.badLayerHandler = None
> >>
> >> def hackyAndUglyReplacer(self, i, n):
> >> """
> >> This hacky ugly function is to replace the bad layer handler early on, before any layers would be loaded.
> >> it is meant to be connected to the `layerLoaded` signal of `QgsProject`
> >> """
> >> # do not run further if there were other layers loaded before (e.g. the signal was emitted before)
> >> if i != 0:
> >> return
> >>
> >> if not self.badLayerHandler:
> >> self.badLayerHandler = MyBadLayerHandler(self.validPath)
> >> QgsProject.instance().setBadLayerHandler(self.badLayerHandler)
> >>
> >> def initGui(self):
> >> # start-up code here...
> >>
> >> #connect to signal
> >> QgsProject.instance().layerLoaded.connect(self.hackyAndUglyReplacer)
> >>
> >> def unload(self):
> >> try:
> >> QgsProject.instance().layerLoaded.disconnect()
> >> except Exception:
> >> pass
> >>
> >>
> >> Does anyone know what am I doing wrong here? Have I missed something?
> >> Why does QGIS crash every 2nd time?
> >>
> >> I would be happy to pass the bad layer handler a better way than with the current signal above. But the other signals I found are emitted 'too late' already.
> >> Perhaps here I am also doing something wrong?
> >>
> >> I tried looking for help but the web barely has any trace of documentation on this.
> >>
> >> I also tried to see how this is done in other plugins e.g. the changeDataSource plugin, but the authors seem to have removed the code when they ported the plugin to QGIS 3.x
> >> (perhaps a bad sign)
> >>
> >> In general it seems to me this part was also overhauled in QGIS3 but there aren't many leads to follow on what is the current way of using custom bad layer handlers.
> >>
> >> Maybe if we could put the story together on how to do this correctly, I could document it and put it on the web for others to refer to.
> >>
> >> Hope you are having / had a great Sunday!
> >>
> >> Best regards,
> >> Aron
> >>
> >> _______________________________________________
> >> QGIS-Developer mailing list
> >> QGIS-Developer at lists.osgeo.org
> >> List info:https://lists.osgeo.org/mailman/listinfo/qgis-developer
> >> Unsubscribe:https://lists.osgeo.org/mailman/listinfo/qgis-developer
>
More information about the QGIS-Developer
mailing list