[{"data":1,"prerenderedAt":2349},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-locator-filter-to-qgis-plugin":3},{"id":4,"title":5,"body":6,"description":2338,"extension":2339,"meta":2340,"navigation":324,"path":2345,"seo":2346,"stem":2347,"__hash__":2348},"docs\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-locator-filter-to-qgis-plugin\u002Findex.md","Add a Locator Filter to a QGIS Plugin",{"type":7,"value":8,"toc":2325},"minimark",[9,13,17,31,208,213,243,247,261,985,1020,1083,1087,1090,1215,1232,1236,1242,1783,1803,1876,1880,1906,1910,1913,2105,2131,2135,2160,2164,2214,2218,2236,2240,2249,2255,2267,2286,2290,2321],[10,11,5],"h1",{"id":12},"add-a-locator-filter-to-a-qgis-plugin",[14,15,16],"p",{},"The search box in the bottom-left corner of QGIS — the locator, opened with Ctrl+K — finds layers, features, actions, settings and bookmarks as you type. Plugins can add their own searches to it. An asset register becomes searchable by asset number, a gazetteer by place name, an internal API by job reference, all without a dialog, a dock widget or a toolbar button: type a short prefix, a few characters, press Enter, and the map jumps there.",[14,18,19,20,25,26,30],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","QGIS Plugin Boilerplate & Structure",". It writes a ",[27,28,29],"code",{},"QgsLocatorFilter"," that searches a project layer, handles the background thread the locator runs searches in, acts on the chosen result, adds a filter backed by a web API, and registers the filters from a plugin.",[14,32,33],{},[34,35,40,44,48,55,72,81,90,95,102,112,118,123,127,131,136,141,146,154,160,164,168,173,180,186,190,196,199,204],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 300","img","How a locator search runs: the user types a prefix and query, QGIS clones the filter into a background thread, fetchResults emits results as they are found, results appear grouped under the filter name, and choosing one calls triggerResult on the main thread","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"Type, search in the background, act on the main thread",[45,46,47],"desc",{},"The user types asset 1043 in the locator bar. QGIS matches the prefix asset to the plugin's filter, clones the filter and calls fetchResults in a worker thread with the query 1043. The clone emits resultFetched for each match, such as TR-10432 Oak Mill Lane and TR-10433. Results appear in the locator drop-down under the filter's display name. When the user presses Enter, triggerResult runs on the main thread with the chosen result and zooms the canvas to the feature.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","300","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"locFlowArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Two threads, two methods",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"24","52","220","44","#fffdf7","#59645f","stroke-width:2",[73,91,94],{"x":86,"y":92,"style":93,"fill":71},"79","font-size:11px;font-family:monospace","asset 1043▏",[96,97],"line",{"x1":98,"y1":99,"x2":100,"y2":99,"stroke":71,"style":101},"244","74","286","stroke-width:1.8;marker-end:url(#locFlowArrow)",[49,103],{"x":104,"y":105,"width":106,"height":107,"rx":108,"fill":109,"stroke":110,"style":111},"294","46","200","104","10","#eff3ff","#2563eb","stroke-width:2.5",[73,113,117],{"x":114,"y":115,"style":116,"fill":110,"textAnchor":79},"394","70","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","worker thread",[73,119,122],{"x":114,"y":120,"style":121,"fill":71,"textAnchor":79},"96","text-anchor:middle;font-size:10px;font-family:monospace","clone()",[73,124,126],{"x":114,"y":125,"style":121,"fill":71,"textAnchor":79},"116","fetchResults(\"1043\")",[73,128,130],{"x":114,"y":129,"style":121,"fill":71,"textAnchor":79},"136","resultFetched.emit(…)",[96,132],{"x1":133,"y1":134,"x2":135,"y2":134,"stroke":71,"style":101},"494","98","536",[49,137],{"x":138,"y":105,"width":139,"height":140,"rx":108,"fill":87,"stroke":88,"style":89},"544","192","130",[73,142,145],{"x":143,"y":115,"style":144,"fill":88},"560","font-size:10px;font-weight:bold;font-family:sans-serif","Asset register",[49,147],{"x":148,"y":149,"width":150,"height":151,"rx":152,"fill":153},"556","80","170","26","4","#dbeafe",[73,155,159],{"x":156,"y":157,"style":158,"fill":71},"566","97","font-size:10px;font-family:sans-serif","TR-10432 · Oak, Mill Lane",[73,161,163],{"x":156,"y":162,"style":158,"fill":71},"126","TR-10433 · Lime, Mill Lane",[73,165,167],{"x":156,"y":166,"style":158,"fill":71},"152","TR-10436 · Ash, Park Rd",[96,169],{"x1":170,"y1":171,"x2":170,"y2":172,"stroke":71,"style":101},"640","176","206",[49,174],{"x":133,"y":175,"width":176,"height":177,"rx":108,"fill":178,"stroke":179,"style":111},"214","242","64","#e8efe6","#15803d",[73,181,185],{"x":182,"y":183,"style":116,"fill":184,"textAnchor":79},"615","240","#166534","main thread",[73,187,189],{"x":182,"y":188,"style":121,"fill":71,"textAnchor":79},"262","triggerResult → zoom, select",[49,191],{"x":83,"y":192,"width":193,"height":134,"rx":108,"fill":194,"stroke":195,"style":89},"180","440","#fdf2e2","#b45309",[73,197,198],{"x":98,"y":172,"style":116,"fill":195,"textAnchor":79},"in fetchResults, never touch",[73,200,203],{"x":98,"y":201,"style":202,"fill":71,"textAnchor":79},"230","text-anchor:middle;font-size:10px;font-family:sans-serif","iface, the canvas, widgets or live layers",[73,205,207],{"x":98,"y":206,"style":202,"fill":71,"textAnchor":79},"254","read from what clone() prepared",[209,210,212],"h2",{"id":211},"prerequisites","Prerequisites",[214,215,216,224,240],"ul",{},[217,218,219,223],"li",{},[220,221,222],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[217,225,226,227,230,231,234,235,239],{},"A plugin skeleton with ",[27,228,229],{},"initGui"," and ",[27,232,233],{},"unload",", as in ",[21,236,238],{"href":237},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fcreate-qgis-plugin-with-plugin-builder\u002F","creating a QGIS plugin with Plugin Builder",".",[217,241,242],{},"Something worth searching: a layer, a lookup table or an API.",[209,244,246],{"id":245},"a-filter-that-searches-a-layer","A filter that searches a layer",[14,248,249,250,252,253,256,257,260],{},"A locator filter subclasses ",[27,251,29],{}," and implements a handful of methods. The two that matter are ",[27,254,255],{},"fetchResults",", which runs in a background thread and emits results, and ",[27,258,259],{},"triggerResult",", which runs on the main thread when a result is chosen.",[262,263,268],"pre",{"className":264,"code":265,"language":266,"meta":267,"style":267},"language-python shiki shiki-themes github-dark","from qgis.core import (\n    QgsLocatorFilter, QgsLocatorResult, QgsFeatureRequest, QgsProject,\n    QgsVectorLayerFeatureSource, QgsExpression,\n)\nfrom qgis.utils import iface\n\n\nclass AssetFilter(QgsLocatorFilter):\n    LAYER_NAME = \"street_trees\"\n\n    def __init__(self, source=None, layer_id=None):\n        super().__init__()\n        self.source = source\n        self.layer_id = layer_id\n\n    def name(self):\n        return \"asset_register\"\n\n    def displayName(self):\n        return \"Asset register\"\n\n    def prefix(self):\n        return \"asset\"\n\n    def priority(self):\n        return QgsLocatorFilter.Priority.High\n\n    def clone(self):\n        layers = QgsProject.instance().mapLayersByName(self.LAYER_NAME)\n        if not layers:\n            return AssetFilter()\n        layer = layers[0]\n        return AssetFilter(QgsVectorLayerFeatureSource(layer), layer.id())\n\n    def fetchResults(self, string, context, feedback):\n        if self.source is None or len(string) \u003C 3:\n            return\n        pattern = QgsExpression.quotedString(f\"%{string}%\")\n        request = (QgsFeatureRequest()\n                   .setFilterExpression(f'\"asset_id\" ILIKE {pattern} OR \"street\" ILIKE {pattern}')\n                   .setLimit(30))\n        for feature in self.source.getFeatures(request):\n            if feedback.isCanceled():\n                return\n            result = QgsLocatorResult()\n            result.filter = self\n            result.displayString = f'{feature[\"asset_id\"]} · {feature[\"species\"]}, {feature[\"street\"]}'\n            result.setUserData({\"layer_id\": self.layer_id, \"fid\": feature.id()})\n            self.resultFetched.emit(result)\n\n    def triggerResult(self, result):\n        data = result.userData()\n        layer = QgsProject.instance().mapLayer(data[\"layer_id\"])\n        if layer is None:\n            return\n        layer.selectByIds([data[\"fid\"]])\n        iface.mapCanvas().zoomToSelected(layer)\n        iface.mapCanvas().flashFeatureIds(layer, [data[\"fid\"]])\n","python","",[27,269,270,288,294,300,306,319,326,331,349,363,368,395,410,424,437,442,453,462,467,477,485,490,500,508,513,523,531,536,546,567,579,588,604,612,617,628,662,668,699,710,742,754,771,780,786,797,808,865,888,897,902,913,924,939,953,958,969,975],{"__ignoreMap":267},[271,272,274,278,282,285],"span",{"class":96,"line":273},1,[271,275,277],{"class":276},"snl16","from",[271,279,281],{"class":280},"s95oV"," qgis.core ",[271,283,284],{"class":276},"import",[271,286,287],{"class":280}," (\n",[271,289,291],{"class":96,"line":290},2,[271,292,293],{"class":280},"    QgsLocatorFilter, QgsLocatorResult, QgsFeatureRequest, QgsProject,\n",[271,295,297],{"class":96,"line":296},3,[271,298,299],{"class":280},"    QgsVectorLayerFeatureSource, QgsExpression,\n",[271,301,303],{"class":96,"line":302},4,[271,304,305],{"class":280},")\n",[271,307,309,311,314,316],{"class":96,"line":308},5,[271,310,277],{"class":276},[271,312,313],{"class":280}," qgis.utils ",[271,315,284],{"class":276},[271,317,318],{"class":280}," iface\n",[271,320,322],{"class":96,"line":321},6,[271,323,325],{"emptyLinePlaceholder":324},true,"\n",[271,327,329],{"class":96,"line":328},7,[271,330,325],{"emptyLinePlaceholder":324},[271,332,334,337,341,344,346],{"class":96,"line":333},8,[271,335,336],{"class":276},"class",[271,338,340],{"class":339},"svObZ"," AssetFilter",[271,342,343],{"class":280},"(",[271,345,29],{"class":339},[271,347,348],{"class":280},"):\n",[271,350,352,356,359],{"class":96,"line":351},9,[271,353,355],{"class":354},"sDLfK","    LAYER_NAME",[271,357,358],{"class":276}," =",[271,360,362],{"class":361},"sU2Wk"," \"street_trees\"\n",[271,364,366],{"class":96,"line":365},10,[271,367,325],{"emptyLinePlaceholder":324},[271,369,371,374,377,380,383,386,389,391,393],{"class":96,"line":370},11,[271,372,373],{"class":276},"    def",[271,375,376],{"class":354}," __init__",[271,378,379],{"class":280},"(self, source",[271,381,382],{"class":276},"=",[271,384,385],{"class":354},"None",[271,387,388],{"class":280},", layer_id",[271,390,382],{"class":276},[271,392,385],{"class":354},[271,394,348],{"class":280},[271,396,398,401,404,407],{"class":96,"line":397},12,[271,399,400],{"class":354},"        super",[271,402,403],{"class":280},"().",[271,405,406],{"class":354},"__init__",[271,408,409],{"class":280},"()\n",[271,411,413,416,419,421],{"class":96,"line":412},13,[271,414,415],{"class":354},"        self",[271,417,418],{"class":280},".source ",[271,420,382],{"class":276},[271,422,423],{"class":280}," source\n",[271,425,427,429,432,434],{"class":96,"line":426},14,[271,428,415],{"class":354},[271,430,431],{"class":280},".layer_id ",[271,433,382],{"class":276},[271,435,436],{"class":280}," layer_id\n",[271,438,440],{"class":96,"line":439},15,[271,441,325],{"emptyLinePlaceholder":324},[271,443,445,447,450],{"class":96,"line":444},16,[271,446,373],{"class":276},[271,448,449],{"class":339}," name",[271,451,452],{"class":280},"(self):\n",[271,454,456,459],{"class":96,"line":455},17,[271,457,458],{"class":276},"        return",[271,460,461],{"class":361}," \"asset_register\"\n",[271,463,465],{"class":96,"line":464},18,[271,466,325],{"emptyLinePlaceholder":324},[271,468,470,472,475],{"class":96,"line":469},19,[271,471,373],{"class":276},[271,473,474],{"class":339}," displayName",[271,476,452],{"class":280},[271,478,480,482],{"class":96,"line":479},20,[271,481,458],{"class":276},[271,483,484],{"class":361}," \"Asset register\"\n",[271,486,488],{"class":96,"line":487},21,[271,489,325],{"emptyLinePlaceholder":324},[271,491,493,495,498],{"class":96,"line":492},22,[271,494,373],{"class":276},[271,496,497],{"class":339}," prefix",[271,499,452],{"class":280},[271,501,503,505],{"class":96,"line":502},23,[271,504,458],{"class":276},[271,506,507],{"class":361}," \"asset\"\n",[271,509,511],{"class":96,"line":510},24,[271,512,325],{"emptyLinePlaceholder":324},[271,514,516,518,521],{"class":96,"line":515},25,[271,517,373],{"class":276},[271,519,520],{"class":339}," priority",[271,522,452],{"class":280},[271,524,526,528],{"class":96,"line":525},26,[271,527,458],{"class":276},[271,529,530],{"class":280}," QgsLocatorFilter.Priority.High\n",[271,532,534],{"class":96,"line":533},27,[271,535,325],{"emptyLinePlaceholder":324},[271,537,539,541,544],{"class":96,"line":538},28,[271,540,373],{"class":276},[271,542,543],{"class":339}," clone",[271,545,452],{"class":280},[271,547,549,552,554,557,560,562,565],{"class":96,"line":548},29,[271,550,551],{"class":280},"        layers ",[271,553,382],{"class":276},[271,555,556],{"class":280}," QgsProject.instance().mapLayersByName(",[271,558,559],{"class":354},"self",[271,561,239],{"class":280},[271,563,564],{"class":354},"LAYER_NAME",[271,566,305],{"class":280},[271,568,570,573,576],{"class":96,"line":569},30,[271,571,572],{"class":276},"        if",[271,574,575],{"class":276}," not",[271,577,578],{"class":280}," layers:\n",[271,580,582,585],{"class":96,"line":581},31,[271,583,584],{"class":276},"            return",[271,586,587],{"class":280}," AssetFilter()\n",[271,589,591,594,596,599,601],{"class":96,"line":590},32,[271,592,593],{"class":280},"        layer ",[271,595,382],{"class":276},[271,597,598],{"class":280}," layers[",[271,600,51],{"class":354},[271,602,603],{"class":280},"]\n",[271,605,607,609],{"class":96,"line":606},33,[271,608,458],{"class":276},[271,610,611],{"class":280}," AssetFilter(QgsVectorLayerFeatureSource(layer), layer.id())\n",[271,613,615],{"class":96,"line":614},34,[271,616,325],{"emptyLinePlaceholder":324},[271,618,620,622,625],{"class":96,"line":619},35,[271,621,373],{"class":276},[271,623,624],{"class":339}," fetchResults",[271,626,627],{"class":280},"(self, string, context, feedback):\n",[271,629,631,633,636,638,641,644,647,650,653,656,659],{"class":96,"line":630},36,[271,632,572],{"class":276},[271,634,635],{"class":354}," self",[271,637,418],{"class":280},[271,639,640],{"class":276},"is",[271,642,643],{"class":354}," None",[271,645,646],{"class":276}," or",[271,648,649],{"class":354}," len",[271,651,652],{"class":280},"(string) ",[271,654,655],{"class":276},"\u003C",[271,657,658],{"class":354}," 3",[271,660,661],{"class":280},":\n",[271,663,665],{"class":96,"line":664},37,[271,666,667],{"class":276},"            return\n",[271,669,671,674,676,679,682,685,688,691,694,697],{"class":96,"line":670},38,[271,672,673],{"class":280},"        pattern ",[271,675,382],{"class":276},[271,677,678],{"class":280}," QgsExpression.quotedString(",[271,680,681],{"class":276},"f",[271,683,684],{"class":361},"\"%",[271,686,687],{"class":354},"{",[271,689,690],{"class":280},"string",[271,692,693],{"class":354},"}",[271,695,696],{"class":361},"%\"",[271,698,305],{"class":280},[271,700,702,705,707],{"class":96,"line":701},39,[271,703,704],{"class":280},"        request ",[271,706,382],{"class":276},[271,708,709],{"class":280}," (QgsFeatureRequest()\n",[271,711,713,716,718,721,723,726,728,731,733,735,737,740],{"class":96,"line":712},40,[271,714,715],{"class":280},"                   .setFilterExpression(",[271,717,681],{"class":276},[271,719,720],{"class":361},"'\"asset_id\" ILIKE ",[271,722,687],{"class":354},[271,724,725],{"class":280},"pattern",[271,727,693],{"class":354},[271,729,730],{"class":361}," OR \"street\" ILIKE ",[271,732,687],{"class":354},[271,734,725],{"class":280},[271,736,693],{"class":354},[271,738,739],{"class":361},"'",[271,741,305],{"class":280},[271,743,745,748,751],{"class":96,"line":744},41,[271,746,747],{"class":280},"                   .setLimit(",[271,749,750],{"class":354},"30",[271,752,753],{"class":280},"))\n",[271,755,757,760,763,766,768],{"class":96,"line":756},42,[271,758,759],{"class":276},"        for",[271,761,762],{"class":280}," feature ",[271,764,765],{"class":276},"in",[271,767,635],{"class":354},[271,769,770],{"class":280},".source.getFeatures(request):\n",[271,772,774,777],{"class":96,"line":773},43,[271,775,776],{"class":276},"            if",[271,778,779],{"class":280}," feedback.isCanceled():\n",[271,781,783],{"class":96,"line":782},44,[271,784,785],{"class":276},"                return\n",[271,787,789,792,794],{"class":96,"line":788},45,[271,790,791],{"class":280},"            result ",[271,793,382],{"class":276},[271,795,796],{"class":280}," QgsLocatorResult()\n",[271,798,800,803,805],{"class":96,"line":799},46,[271,801,802],{"class":280},"            result.filter ",[271,804,382],{"class":276},[271,806,807],{"class":354}," self\n",[271,809,811,814,816,819,821,823,826,829,832,834,837,839,841,844,846,848,851,853,855,858,860,862],{"class":96,"line":810},47,[271,812,813],{"class":280},"            result.displayString ",[271,815,382],{"class":276},[271,817,818],{"class":276}," f",[271,820,739],{"class":361},[271,822,687],{"class":354},[271,824,825],{"class":280},"feature[",[271,827,828],{"class":361},"\"asset_id\"",[271,830,831],{"class":280},"]",[271,833,693],{"class":354},[271,835,836],{"class":361}," · ",[271,838,687],{"class":354},[271,840,825],{"class":280},[271,842,843],{"class":361},"\"species\"",[271,845,831],{"class":280},[271,847,693],{"class":354},[271,849,850],{"class":361},", ",[271,852,687],{"class":354},[271,854,825],{"class":280},[271,856,857],{"class":361},"\"street\"",[271,859,831],{"class":280},[271,861,693],{"class":354},[271,863,864],{"class":361},"'\n",[271,866,868,871,874,877,879,882,885],{"class":96,"line":867},48,[271,869,870],{"class":280},"            result.setUserData({",[271,872,873],{"class":361},"\"layer_id\"",[271,875,876],{"class":280},": ",[271,878,559],{"class":354},[271,880,881],{"class":280},".layer_id, ",[271,883,884],{"class":361},"\"fid\"",[271,886,887],{"class":280},": feature.id()})\n",[271,889,891,894],{"class":96,"line":890},49,[271,892,893],{"class":354},"            self",[271,895,896],{"class":280},".resultFetched.emit(result)\n",[271,898,900],{"class":96,"line":899},50,[271,901,325],{"emptyLinePlaceholder":324},[271,903,905,907,910],{"class":96,"line":904},51,[271,906,373],{"class":276},[271,908,909],{"class":339}," triggerResult",[271,911,912],{"class":280},"(self, result):\n",[271,914,916,919,921],{"class":96,"line":915},52,[271,917,918],{"class":280},"        data ",[271,920,382],{"class":276},[271,922,923],{"class":280}," result.userData()\n",[271,925,927,929,931,934,936],{"class":96,"line":926},53,[271,928,593],{"class":280},[271,930,382],{"class":276},[271,932,933],{"class":280}," QgsProject.instance().mapLayer(data[",[271,935,873],{"class":361},[271,937,938],{"class":280},"])\n",[271,940,942,944,947,949,951],{"class":96,"line":941},54,[271,943,572],{"class":276},[271,945,946],{"class":280}," layer ",[271,948,640],{"class":276},[271,950,643],{"class":354},[271,952,661],{"class":280},[271,954,956],{"class":96,"line":955},55,[271,957,667],{"class":276},[271,959,961,964,966],{"class":96,"line":960},56,[271,962,963],{"class":280},"        layer.selectByIds([data[",[271,965,884],{"class":361},[271,967,968],{"class":280},"]])\n",[271,970,972],{"class":96,"line":971},57,[271,973,974],{"class":280},"        iface.mapCanvas().zoomToSelected(layer)\n",[271,976,978,981,983],{"class":96,"line":977},58,[271,979,980],{"class":280},"        iface.mapCanvas().flashFeatureIds(layer, [data[",[271,982,884],{"class":361},[271,984,968],{"class":280},[14,986,987,990,991,993,994,996,997,1000,1001,1004,1005,1008,1009,1012,1013,1016,1017,1019],{},[220,988,989],{},"Breakdown:"," QGIS calls ",[27,992,122],{}," on the main thread before each search and runs ",[27,995,255],{}," on the copy in a worker thread, so ",[27,998,999],{},"clone"," is where anything thread-unsafe is turned into something safe: a ",[27,1002,1003],{},"QgsVectorLayerFeatureSource"," is a snapshot of the layer's provider that can be iterated from another thread, whereas the layer itself must not be. The prefix lets users type ",[27,1006,1007],{},"asset 1043"," to search only this filter; without a prefix the filter also runs for unprefixed searches if enabled in the locator settings. Checking ",[27,1010,1011],{},"feedback.isCanceled()"," matters because every keystroke cancels the previous search. ",[27,1014,1015],{},"setUserData"," stores what ",[27,1018,259],{}," needs — the layer id and feature id — rather than the feature itself. Flashing the feature after zooming draws the eye to it, the same effect the built-in feature search uses.",[14,1021,1022],{},[34,1023,1026,1029,1032,1035,1038,1041,1046,1050,1054,1058,1062,1066,1070,1074,1077,1080],{"viewBox":1024,"role":37,"ariaLabel":1025,"xmlns":39},"0 0 760 256","What clone must prepare for the worker thread: snapshots such as QgsVectorLayerFeatureSource and plain Python data are safe to use in fetchResults, while live layers, the map canvas, iface and widgets must only be used in triggerResult on the main thread",[41,1027,1028],{},"Safe in the worker, main thread only",[45,1030,1031],{},"Left column, safe in fetchResults: a QgsVectorLayerFeatureSource created in clone, plain Python lists and dictionaries copied in clone, QgsBlockingNetworkRequest for HTTP, and emitting resultFetched. Right column, main thread only: QgsVectorLayer objects, iface and the map canvas, message bar and widgets, and changing selections. Violating this crashes QGIS intermittently rather than raising an error.",[49,1033],{"x":51,"y":51,"width":52,"height":1034,"fill":54},"256",[73,1036,1037],{"x":75,"y":151,"style":77,"fill":78,"textAnchor":79},"Crashes here are intermittent, so get it right up front",[49,1039],{"x":83,"y":86,"width":1040,"height":139,"rx":108,"fill":178,"stroke":179,"style":111},"344",[73,1042,1045],{"x":1043,"y":115,"style":1044,"fill":184,"textAnchor":79},"196","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","fine in fetchResults",[73,1047,1049],{"x":84,"y":107,"style":1048,"fill":71},"font-size:10.5px;font-family:sans-serif","✓ QgsVectorLayerFeatureSource",[73,1051,1053],{"x":84,"y":1052,"style":1048,"fill":71},"132","✓ plain data copied in clone()",[73,1055,1057],{"x":84,"y":1056,"style":1048,"fill":71},"160","✓ QgsBlockingNetworkRequest",[73,1059,1061],{"x":84,"y":1060,"style":1048,"fill":71},"188","✓ resultFetched.emit",[49,1063],{"x":1064,"y":86,"width":1040,"height":139,"rx":108,"fill":194,"stroke":1065,"style":111},"392","#b91c1c",[73,1067,1069],{"x":1068,"y":115,"style":1044,"fill":1065,"textAnchor":79},"564","triggerResult only",[73,1071,1073],{"x":1072,"y":107,"style":1048,"fill":71},"420","✗ QgsVectorLayer objects",[73,1075,1076],{"x":1072,"y":1052,"style":1048,"fill":71},"✗ iface, map canvas",[73,1078,1079],{"x":1072,"y":1056,"style":1048,"fill":71},"✗ message bar, widgets",[73,1081,1082],{"x":1072,"y":1060,"style":1048,"fill":71},"✗ selections, edits",[209,1084,1086],{"id":1085},"register-the-filter-from-the-plugin","Register the filter from the plugin",[14,1088,1089],{},"Filters are registered with the interface when the plugin loads and must be deregistered when it unloads.",[262,1091,1093],{"className":264,"code":1092,"language":266,"meta":267,"style":267},"class AssetSearchPlugin:\n    def __init__(self, iface):\n        self.iface = iface\n        self.filters = []\n\n    def initGui(self):\n        for f in (AssetFilter(), JobFilter()):\n            self.iface.registerLocatorFilter(f)\n            self.filters.append(f)\n\n    def unload(self):\n        for f in self.filters:\n            self.iface.deregisterLocatorFilter(f)\n        self.filters.clear()\n",[27,1094,1095,1104,1113,1124,1136,1140,1149,1161,1168,1175,1179,1188,1201,1208],{"__ignoreMap":267},[271,1096,1097,1099,1102],{"class":96,"line":273},[271,1098,336],{"class":276},[271,1100,1101],{"class":339}," AssetSearchPlugin",[271,1103,661],{"class":280},[271,1105,1106,1108,1110],{"class":96,"line":290},[271,1107,373],{"class":276},[271,1109,376],{"class":354},[271,1111,1112],{"class":280},"(self, iface):\n",[271,1114,1115,1117,1120,1122],{"class":96,"line":296},[271,1116,415],{"class":354},[271,1118,1119],{"class":280},".iface ",[271,1121,382],{"class":276},[271,1123,318],{"class":280},[271,1125,1126,1128,1131,1133],{"class":96,"line":302},[271,1127,415],{"class":354},[271,1129,1130],{"class":280},".filters ",[271,1132,382],{"class":276},[271,1134,1135],{"class":280}," []\n",[271,1137,1138],{"class":96,"line":308},[271,1139,325],{"emptyLinePlaceholder":324},[271,1141,1142,1144,1147],{"class":96,"line":321},[271,1143,373],{"class":276},[271,1145,1146],{"class":339}," initGui",[271,1148,452],{"class":280},[271,1150,1151,1153,1156,1158],{"class":96,"line":328},[271,1152,759],{"class":276},[271,1154,1155],{"class":280}," f ",[271,1157,765],{"class":276},[271,1159,1160],{"class":280}," (AssetFilter(), JobFilter()):\n",[271,1162,1163,1165],{"class":96,"line":333},[271,1164,893],{"class":354},[271,1166,1167],{"class":280},".iface.registerLocatorFilter(f)\n",[271,1169,1170,1172],{"class":96,"line":351},[271,1171,893],{"class":354},[271,1173,1174],{"class":280},".filters.append(f)\n",[271,1176,1177],{"class":96,"line":365},[271,1178,325],{"emptyLinePlaceholder":324},[271,1180,1181,1183,1186],{"class":96,"line":370},[271,1182,373],{"class":276},[271,1184,1185],{"class":339}," unload",[271,1187,452],{"class":280},[271,1189,1190,1192,1194,1196,1198],{"class":96,"line":397},[271,1191,759],{"class":276},[271,1193,1155],{"class":280},[271,1195,765],{"class":276},[271,1197,635],{"class":354},[271,1199,1200],{"class":280},".filters:\n",[271,1202,1203,1205],{"class":96,"line":412},[271,1204,893],{"class":354},[271,1206,1207],{"class":280},".iface.deregisterLocatorFilter(f)\n",[271,1209,1210,1212],{"class":96,"line":426},[271,1211,415],{"class":354},[271,1213,1214],{"class":280},".filters.clear()\n",[14,1216,1217,1219,1220,1223,1224,1226,1227,1231],{},[220,1218,989],{}," ",[27,1221,1222],{},"registerLocatorFilter"," takes ownership on the C++ side, but the Python object must stay referenced — keeping the filters in a list on the plugin does that and gives ",[27,1225,233],{}," something to deregister. Forgetting to deregister leaves a filter that points at unloaded plugin code, which crashes QGIS the next time someone types in the locator. Users can enable, disable and change the prefix of every registered filter in ",[1228,1229,1230],"em",{},"Settings → Options → Locator",", so pick a short, memorable default and a clear display name.",[209,1233,1235],{"id":1234},"a-filter-backed-by-a-web-api","A filter backed by a web API",[14,1237,1238,1239,1241],{},"Locator filters are a natural front end for remote lookups — a job system, a geocoder, a register kept outside GIS. Network requests in ",[27,1240,255],{}," should use QGIS's blocking request, which is safe in the worker thread and respects proxy and authentication settings.",[262,1243,1245],{"className":264,"code":1244,"language":266,"meta":267,"style":267},"import json\nfrom qgis.PyQt.QtCore import QUrl, QUrlQuery\nfrom qgis.PyQt.QtNetwork import QNetworkRequest\nfrom qgis.core import (\n    QgsBlockingNetworkRequest, QgsCoordinateReferenceSystem, QgsCoordinateTransform,\n    QgsPointXY, QgsRectangle,\n)\n\n\nclass JobFilter(QgsLocatorFilter):\n    URL = \"https:\u002F\u002Fjobs.example.internal\u002Fapi\u002Fsearch\"\n\n    def __init__(self):\n        super().__init__()\n        self.setUseWithoutPrefix(False)\n\n    def name(self): return \"job_search\"\n    def displayName(self): return \"Works jobs\"\n    def prefix(self): return \"job\"\n    def clone(self): return JobFilter()\n\n    def fetchResults(self, string, context, feedback):\n        if len(string) \u003C 4:\n            return\n        url = QUrl(self.URL)\n        query = QUrlQuery()\n        query.addQueryItem(\"q\", string)\n        url.setQuery(query)\n        request = QgsBlockingNetworkRequest()\n        request.setAuthCfg(\"jobs001\")\n        if request.get(QNetworkRequest(url), False, feedback) != QgsBlockingNetworkRequest.NoError:\n            return\n        for job in json.loads(bytes(request.reply().content()))[\"results\"][:20]:\n            if feedback.isCanceled():\n                return\n            result = QgsLocatorResult()\n            result.filter = self\n            result.displayString = f'{job[\"ref\"]} — {job[\"title\"]}'\n            result.description = job.get(\"status\", \"\")\n            result.setUserData({\"lon\": job[\"lon\"], \"lat\": job[\"lat\"]})\n            self.resultFetched.emit(result)\n\n    def triggerResult(self, result):\n        data = result.userData()\n        canvas = iface.mapCanvas()\n        to_canvas = QgsCoordinateTransform(QgsCoordinateReferenceSystem(\"EPSG:4326\"),\n                                           canvas.mapSettings().destinationCrs(),\n                                           QgsProject.instance())\n        point = to_canvas.transform(QgsPointXY(data[\"lon\"], data[\"lat\"]))\n        canvas.setCenter(point)\n        canvas.zoomScale(1500)\n        canvas.refresh()\n",[27,1246,1247,1254,1266,1278,1288,1293,1298,1302,1306,1310,1323,1333,1337,1345,1355,1367,1371,1386,1399,1412,1425,1429,1437,1452,1456,1475,1485,1496,1501,1510,1520,1538,1542,1572,1578,1582,1590,1598,1636,1656,1681,1687,1691,1699,1707,1717,1733,1738,1743,1763,1768,1778],{"__ignoreMap":267},[271,1248,1249,1251],{"class":96,"line":273},[271,1250,284],{"class":276},[271,1252,1253],{"class":280}," json\n",[271,1255,1256,1258,1261,1263],{"class":96,"line":290},[271,1257,277],{"class":276},[271,1259,1260],{"class":280}," qgis.PyQt.QtCore ",[271,1262,284],{"class":276},[271,1264,1265],{"class":280}," QUrl, QUrlQuery\n",[271,1267,1268,1270,1273,1275],{"class":96,"line":296},[271,1269,277],{"class":276},[271,1271,1272],{"class":280}," qgis.PyQt.QtNetwork ",[271,1274,284],{"class":276},[271,1276,1277],{"class":280}," QNetworkRequest\n",[271,1279,1280,1282,1284,1286],{"class":96,"line":302},[271,1281,277],{"class":276},[271,1283,281],{"class":280},[271,1285,284],{"class":276},[271,1287,287],{"class":280},[271,1289,1290],{"class":96,"line":308},[271,1291,1292],{"class":280},"    QgsBlockingNetworkRequest, QgsCoordinateReferenceSystem, QgsCoordinateTransform,\n",[271,1294,1295],{"class":96,"line":321},[271,1296,1297],{"class":280},"    QgsPointXY, QgsRectangle,\n",[271,1299,1300],{"class":96,"line":328},[271,1301,305],{"class":280},[271,1303,1304],{"class":96,"line":333},[271,1305,325],{"emptyLinePlaceholder":324},[271,1307,1308],{"class":96,"line":351},[271,1309,325],{"emptyLinePlaceholder":324},[271,1311,1312,1314,1317,1319,1321],{"class":96,"line":365},[271,1313,336],{"class":276},[271,1315,1316],{"class":339}," JobFilter",[271,1318,343],{"class":280},[271,1320,29],{"class":339},[271,1322,348],{"class":280},[271,1324,1325,1328,1330],{"class":96,"line":370},[271,1326,1327],{"class":354},"    URL",[271,1329,358],{"class":276},[271,1331,1332],{"class":361}," \"https:\u002F\u002Fjobs.example.internal\u002Fapi\u002Fsearch\"\n",[271,1334,1335],{"class":96,"line":397},[271,1336,325],{"emptyLinePlaceholder":324},[271,1338,1339,1341,1343],{"class":96,"line":412},[271,1340,373],{"class":276},[271,1342,376],{"class":354},[271,1344,452],{"class":280},[271,1346,1347,1349,1351,1353],{"class":96,"line":426},[271,1348,400],{"class":354},[271,1350,403],{"class":280},[271,1352,406],{"class":354},[271,1354,409],{"class":280},[271,1356,1357,1359,1362,1365],{"class":96,"line":439},[271,1358,415],{"class":354},[271,1360,1361],{"class":280},".setUseWithoutPrefix(",[271,1363,1364],{"class":354},"False",[271,1366,305],{"class":280},[271,1368,1369],{"class":96,"line":444},[271,1370,325],{"emptyLinePlaceholder":324},[271,1372,1373,1375,1377,1380,1383],{"class":96,"line":455},[271,1374,373],{"class":276},[271,1376,449],{"class":339},[271,1378,1379],{"class":280},"(self): ",[271,1381,1382],{"class":276},"return",[271,1384,1385],{"class":361}," \"job_search\"\n",[271,1387,1388,1390,1392,1394,1396],{"class":96,"line":464},[271,1389,373],{"class":276},[271,1391,474],{"class":339},[271,1393,1379],{"class":280},[271,1395,1382],{"class":276},[271,1397,1398],{"class":361}," \"Works jobs\"\n",[271,1400,1401,1403,1405,1407,1409],{"class":96,"line":469},[271,1402,373],{"class":276},[271,1404,497],{"class":339},[271,1406,1379],{"class":280},[271,1408,1382],{"class":276},[271,1410,1411],{"class":361}," \"job\"\n",[271,1413,1414,1416,1418,1420,1422],{"class":96,"line":479},[271,1415,373],{"class":276},[271,1417,543],{"class":339},[271,1419,1379],{"class":280},[271,1421,1382],{"class":276},[271,1423,1424],{"class":280}," JobFilter()\n",[271,1426,1427],{"class":96,"line":487},[271,1428,325],{"emptyLinePlaceholder":324},[271,1430,1431,1433,1435],{"class":96,"line":492},[271,1432,373],{"class":276},[271,1434,624],{"class":339},[271,1436,627],{"class":280},[271,1438,1439,1441,1443,1445,1447,1450],{"class":96,"line":502},[271,1440,572],{"class":276},[271,1442,649],{"class":354},[271,1444,652],{"class":280},[271,1446,655],{"class":276},[271,1448,1449],{"class":354}," 4",[271,1451,661],{"class":280},[271,1453,1454],{"class":96,"line":510},[271,1455,667],{"class":276},[271,1457,1458,1461,1463,1466,1468,1470,1473],{"class":96,"line":515},[271,1459,1460],{"class":280},"        url ",[271,1462,382],{"class":276},[271,1464,1465],{"class":280}," QUrl(",[271,1467,559],{"class":354},[271,1469,239],{"class":280},[271,1471,1472],{"class":354},"URL",[271,1474,305],{"class":280},[271,1476,1477,1480,1482],{"class":96,"line":525},[271,1478,1479],{"class":280},"        query ",[271,1481,382],{"class":276},[271,1483,1484],{"class":280}," QUrlQuery()\n",[271,1486,1487,1490,1493],{"class":96,"line":533},[271,1488,1489],{"class":280},"        query.addQueryItem(",[271,1491,1492],{"class":361},"\"q\"",[271,1494,1495],{"class":280},", string)\n",[271,1497,1498],{"class":96,"line":538},[271,1499,1500],{"class":280},"        url.setQuery(query)\n",[271,1502,1503,1505,1507],{"class":96,"line":548},[271,1504,704],{"class":280},[271,1506,382],{"class":276},[271,1508,1509],{"class":280}," QgsBlockingNetworkRequest()\n",[271,1511,1512,1515,1518],{"class":96,"line":569},[271,1513,1514],{"class":280},"        request.setAuthCfg(",[271,1516,1517],{"class":361},"\"jobs001\"",[271,1519,305],{"class":280},[271,1521,1522,1524,1527,1529,1532,1535],{"class":96,"line":581},[271,1523,572],{"class":276},[271,1525,1526],{"class":280}," request.get(QNetworkRequest(url), ",[271,1528,1364],{"class":354},[271,1530,1531],{"class":280},", feedback) ",[271,1533,1534],{"class":276},"!=",[271,1536,1537],{"class":280}," QgsBlockingNetworkRequest.NoError:\n",[271,1539,1540],{"class":96,"line":590},[271,1541,667],{"class":276},[271,1543,1544,1546,1549,1551,1554,1557,1560,1563,1566,1569],{"class":96,"line":606},[271,1545,759],{"class":276},[271,1547,1548],{"class":280}," job ",[271,1550,765],{"class":276},[271,1552,1553],{"class":280}," json.loads(",[271,1555,1556],{"class":354},"bytes",[271,1558,1559],{"class":280},"(request.reply().content()))[",[271,1561,1562],{"class":361},"\"results\"",[271,1564,1565],{"class":280},"][:",[271,1567,1568],{"class":354},"20",[271,1570,1571],{"class":280},"]:\n",[271,1573,1574,1576],{"class":96,"line":614},[271,1575,776],{"class":276},[271,1577,779],{"class":280},[271,1579,1580],{"class":96,"line":619},[271,1581,785],{"class":276},[271,1583,1584,1586,1588],{"class":96,"line":630},[271,1585,791],{"class":280},[271,1587,382],{"class":276},[271,1589,796],{"class":280},[271,1591,1592,1594,1596],{"class":96,"line":664},[271,1593,802],{"class":280},[271,1595,382],{"class":276},[271,1597,807],{"class":354},[271,1599,1600,1602,1604,1606,1608,1610,1613,1616,1618,1620,1623,1625,1627,1630,1632,1634],{"class":96,"line":670},[271,1601,813],{"class":280},[271,1603,382],{"class":276},[271,1605,818],{"class":276},[271,1607,739],{"class":361},[271,1609,687],{"class":354},[271,1611,1612],{"class":280},"job[",[271,1614,1615],{"class":361},"\"ref\"",[271,1617,831],{"class":280},[271,1619,693],{"class":354},[271,1621,1622],{"class":361}," — ",[271,1624,687],{"class":354},[271,1626,1612],{"class":280},[271,1628,1629],{"class":361},"\"title\"",[271,1631,831],{"class":280},[271,1633,693],{"class":354},[271,1635,864],{"class":361},[271,1637,1638,1641,1643,1646,1649,1651,1654],{"class":96,"line":701},[271,1639,1640],{"class":280},"            result.description ",[271,1642,382],{"class":276},[271,1644,1645],{"class":280}," job.get(",[271,1647,1648],{"class":361},"\"status\"",[271,1650,850],{"class":280},[271,1652,1653],{"class":361},"\"\"",[271,1655,305],{"class":280},[271,1657,1658,1660,1663,1666,1668,1671,1674,1676,1678],{"class":96,"line":712},[271,1659,870],{"class":280},[271,1661,1662],{"class":361},"\"lon\"",[271,1664,1665],{"class":280},": job[",[271,1667,1662],{"class":361},[271,1669,1670],{"class":280},"], ",[271,1672,1673],{"class":361},"\"lat\"",[271,1675,1665],{"class":280},[271,1677,1673],{"class":361},[271,1679,1680],{"class":280},"]})\n",[271,1682,1683,1685],{"class":96,"line":744},[271,1684,893],{"class":354},[271,1686,896],{"class":280},[271,1688,1689],{"class":96,"line":756},[271,1690,325],{"emptyLinePlaceholder":324},[271,1692,1693,1695,1697],{"class":96,"line":773},[271,1694,373],{"class":276},[271,1696,909],{"class":339},[271,1698,912],{"class":280},[271,1700,1701,1703,1705],{"class":96,"line":782},[271,1702,918],{"class":280},[271,1704,382],{"class":276},[271,1706,923],{"class":280},[271,1708,1709,1712,1714],{"class":96,"line":788},[271,1710,1711],{"class":280},"        canvas ",[271,1713,382],{"class":276},[271,1715,1716],{"class":280}," iface.mapCanvas()\n",[271,1718,1719,1722,1724,1727,1730],{"class":96,"line":799},[271,1720,1721],{"class":280},"        to_canvas ",[271,1723,382],{"class":276},[271,1725,1726],{"class":280}," QgsCoordinateTransform(QgsCoordinateReferenceSystem(",[271,1728,1729],{"class":361},"\"EPSG:4326\"",[271,1731,1732],{"class":280},"),\n",[271,1734,1735],{"class":96,"line":810},[271,1736,1737],{"class":280},"                                           canvas.mapSettings().destinationCrs(),\n",[271,1739,1740],{"class":96,"line":867},[271,1741,1742],{"class":280},"                                           QgsProject.instance())\n",[271,1744,1745,1748,1750,1753,1755,1758,1760],{"class":96,"line":890},[271,1746,1747],{"class":280},"        point ",[271,1749,382],{"class":276},[271,1751,1752],{"class":280}," to_canvas.transform(QgsPointXY(data[",[271,1754,1662],{"class":361},[271,1756,1757],{"class":280},"], data[",[271,1759,1673],{"class":361},[271,1761,1762],{"class":280},"]))\n",[271,1764,1765],{"class":96,"line":899},[271,1766,1767],{"class":280},"        canvas.setCenter(point)\n",[271,1769,1770,1773,1776],{"class":96,"line":904},[271,1771,1772],{"class":280},"        canvas.zoomScale(",[271,1774,1775],{"class":354},"1500",[271,1777,305],{"class":280},[271,1779,1780],{"class":96,"line":915},[271,1781,1782],{"class":280},"        canvas.refresh()\n",[14,1784,1785,1787,1788,1791,1792,1795,1796,1798,1799,239],{},[220,1786,989],{}," Passing the locator's ",[27,1789,1790],{},"feedback"," into the network request aborts the HTTP call when the user keeps typing, so a slow API does not queue up stale requests. A minimum query length of four characters keeps the API from being hit for every keystroke. ",[27,1793,1794],{},"description"," adds a second line under the result, useful for status. Coordinates from the API are transformed into the canvas CRS in ",[27,1797,259],{},", on the main thread, where touching the canvas is safe. The HTTP pattern is covered in more depth in ",[21,1800,1802],{"href":1801},"\u002Fspatial-data-processing-automation\u002Fweb-services-and-remote-data\u002Fmake-http-requests-with-qgsnetworkaccessmanager-pyqgis\u002F","making HTTP requests with QgsNetworkAccessManager",[14,1804,1805],{},[34,1806,1809,1812,1815,1818,1821,1825,1828,1832,1836,1840,1846,1850,1854,1858,1861,1865,1868,1870,1873],{"viewBox":1807,"role":37,"ariaLabel":1808,"xmlns":39},"0 0 760 236","Locator filter settings users control: enabled state, default inclusion in unprefixed searches, and the prefix, all configurable in the options dialog, so a plugin should choose sensible defaults and a unique name",[41,1810,1811],{},"Defaults the user can change",[45,1813,1814],{},"The locator options list each filter with a checkbox for enabled, a checkbox for default so it runs without a prefix, and an editable prefix. The plugin sets the initial values: a unique name used as the settings key, a short prefix like asset, and default inclusion off for slow filters such as web APIs so that typing in the locator stays fast.",[49,1816],{"x":51,"y":51,"width":52,"height":1817,"fill":54},"236",[73,1819,1820],{"x":75,"y":151,"style":77,"fill":78,"textAnchor":79},"Pick defaults that keep the locator fast",[49,1822],{"x":83,"y":86,"width":1823,"height":1824,"rx":108,"fill":87,"stroke":88,"style":89},"712","172",[73,1826,1827],{"x":150,"y":115,"style":116,"fill":78,"textAnchor":79},"filter",[73,1829,1831],{"x":1830,"y":115,"style":116,"fill":78,"textAnchor":79},"360","prefix",[73,1833,1835],{"x":1834,"y":115,"style":116,"fill":78,"textAnchor":79},"490","enabled",[73,1837,1839],{"x":1838,"y":115,"style":116,"fill":78,"textAnchor":79},"630","in unprefixed search",[96,1841],{"x1":1842,"y1":149,"x2":1843,"y2":149,"stroke":1844,"style":1845},"40","720","#d9d3c4","stroke-width:1.5",[73,1847,145],{"x":150,"y":1848,"style":1849,"fill":71,"textAnchor":79},"110","text-anchor:middle;font-size:10.5px;font-family:sans-serif",[73,1851,1853],{"x":1830,"y":1848,"style":1852,"fill":71,"textAnchor":79},"text-anchor:middle;font-size:10.5px;font-family:monospace","asset",[73,1855,1857],{"x":1834,"y":1848,"style":1856,"fill":184,"textAnchor":79},"text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","yes",[73,1859,1860],{"x":1838,"y":1848,"style":1856,"fill":184,"textAnchor":79},"yes — local, fast",[73,1862,1864],{"x":150,"y":1863,"style":1849,"fill":71,"textAnchor":79},"146","Works jobs",[73,1866,1867],{"x":1830,"y":1863,"style":1852,"fill":71,"textAnchor":79},"job",[73,1869,1857],{"x":1834,"y":1863,"style":1856,"fill":184,"textAnchor":79},[73,1871,1872],{"x":1838,"y":1863,"style":1856,"fill":195,"textAnchor":79},"no — network",[73,1874,1875],{"x":75,"y":139,"style":202,"fill":88,"textAnchor":79},"name() is the settings key — never change it between plugin versions",[209,1877,1879],{"id":1878},"defaults-prefixes-and-performance","Defaults, prefixes and performance",[14,1881,1882,1883,1886,1887,1890,1891,1894,1895,1898,1899,1901,1902,1905],{},"Every registered filter appears in ",[1228,1884,1885],{},"Options → Locator",", where users can disable it, change its prefix and choose whether it runs for searches typed without a prefix. The plugin sets the starting point: ",[27,1888,1889],{},"name()"," is the key under which those settings are stored, so it must be unique and stable across versions; ",[27,1892,1893],{},"prefix()"," should be short and unlikely to collide with built-in prefixes such as ",[27,1896,1897],{},"l"," for layers or ",[27,1900,681],{}," for features; and filters that call networks or scan large tables should not run on every unprefixed keystroke. Call ",[27,1903,1904],{},"setUseWithoutPrefix(False)"," in their constructor, as the job filter does, and keep local filters fast by limiting results and requiring a minimum query length.",[209,1907,1909],{"id":1908},"test-the-filter-without-the-locator","Test the filter without the locator",[14,1911,1912],{},"Because the search logic lives in two ordinary methods, a filter can be exercised directly — in the Python console while developing, or in a pytest suite in CI — without typing into the locator at all.",[262,1914,1916],{"className":264,"code":1915,"language":266,"meta":267,"style":267},"from qgis.core import QgsFeedback, QgsLocatorContext\n\ndef run_filter(filter_, query):\n    worker = filter_.clone()\n    found = []\n    worker.resultFetched.connect(found.append)\n    worker.fetchResults(query, QgsLocatorContext(), QgsFeedback())\n    return found\n\nresults = run_filter(AssetFilter(), \"1043\")\nprint(len(results), \"results\")\nfor r in results[:5]:\n    print(\" \", r.displayString, r.userData())\n\nassert all(len(r.displayString) \u003C 80 for r in results), \"labels too long for the drop-down\"\nassert run_filter(AssetFilter(), \"10\") == [], \"short queries should not search\"\n",[27,1917,1918,1929,1933,1944,1954,1963,1968,1973,1981,1985,2000,2017,2034,2047,2051,2084],{"__ignoreMap":267},[271,1919,1920,1922,1924,1926],{"class":96,"line":273},[271,1921,277],{"class":276},[271,1923,281],{"class":280},[271,1925,284],{"class":276},[271,1927,1928],{"class":280}," QgsFeedback, QgsLocatorContext\n",[271,1930,1931],{"class":96,"line":290},[271,1932,325],{"emptyLinePlaceholder":324},[271,1934,1935,1938,1941],{"class":96,"line":296},[271,1936,1937],{"class":276},"def",[271,1939,1940],{"class":339}," run_filter",[271,1942,1943],{"class":280},"(filter_, query):\n",[271,1945,1946,1949,1951],{"class":96,"line":302},[271,1947,1948],{"class":280},"    worker ",[271,1950,382],{"class":276},[271,1952,1953],{"class":280}," filter_.clone()\n",[271,1955,1956,1959,1961],{"class":96,"line":308},[271,1957,1958],{"class":280},"    found ",[271,1960,382],{"class":276},[271,1962,1135],{"class":280},[271,1964,1965],{"class":96,"line":321},[271,1966,1967],{"class":280},"    worker.resultFetched.connect(found.append)\n",[271,1969,1970],{"class":96,"line":328},[271,1971,1972],{"class":280},"    worker.fetchResults(query, QgsLocatorContext(), QgsFeedback())\n",[271,1974,1975,1978],{"class":96,"line":333},[271,1976,1977],{"class":276},"    return",[271,1979,1980],{"class":280}," found\n",[271,1982,1983],{"class":96,"line":351},[271,1984,325],{"emptyLinePlaceholder":324},[271,1986,1987,1990,1992,1995,1998],{"class":96,"line":365},[271,1988,1989],{"class":280},"results ",[271,1991,382],{"class":276},[271,1993,1994],{"class":280}," run_filter(AssetFilter(), ",[271,1996,1997],{"class":361},"\"1043\"",[271,1999,305],{"class":280},[271,2001,2002,2005,2007,2010,2013,2015],{"class":96,"line":370},[271,2003,2004],{"class":354},"print",[271,2006,343],{"class":280},[271,2008,2009],{"class":354},"len",[271,2011,2012],{"class":280},"(results), ",[271,2014,1562],{"class":361},[271,2016,305],{"class":280},[271,2018,2019,2022,2025,2027,2030,2032],{"class":96,"line":397},[271,2020,2021],{"class":276},"for",[271,2023,2024],{"class":280}," r ",[271,2026,765],{"class":276},[271,2028,2029],{"class":280}," results[:",[271,2031,64],{"class":354},[271,2033,1571],{"class":280},[271,2035,2036,2039,2041,2044],{"class":96,"line":412},[271,2037,2038],{"class":354},"    print",[271,2040,343],{"class":280},[271,2042,2043],{"class":361},"\" \"",[271,2045,2046],{"class":280},", r.displayString, r.userData())\n",[271,2048,2049],{"class":96,"line":426},[271,2050,325],{"emptyLinePlaceholder":324},[271,2052,2053,2056,2059,2061,2063,2066,2068,2071,2074,2076,2078,2081],{"class":96,"line":439},[271,2054,2055],{"class":276},"assert",[271,2057,2058],{"class":354}," all",[271,2060,343],{"class":280},[271,2062,2009],{"class":354},[271,2064,2065],{"class":280},"(r.displayString) ",[271,2067,655],{"class":276},[271,2069,2070],{"class":354}," 80",[271,2072,2073],{"class":276}," for",[271,2075,2024],{"class":280},[271,2077,765],{"class":276},[271,2079,2080],{"class":280}," results), ",[271,2082,2083],{"class":361},"\"labels too long for the drop-down\"\n",[271,2085,2086,2088,2090,2093,2096,2099,2102],{"class":96,"line":444},[271,2087,2055],{"class":276},[271,2089,1994],{"class":280},[271,2091,2092],{"class":361},"\"10\"",[271,2094,2095],{"class":280},") ",[271,2097,2098],{"class":276},"==",[271,2100,2101],{"class":280}," [], ",[271,2103,2104],{"class":361},"\"short queries should not search\"\n",[14,2106,2107,2109,2110,2112,2113,2115,2116,2119,2120,2123,2124,2126,2127,239],{},[220,2108,989],{}," Calling ",[27,2111,122],{}," first mirrors exactly what QGIS does, so a filter whose ",[27,2114,999],{}," forgets to prepare its feature source fails here the same way it would in the locator — silently, with no results. Connecting ",[27,2117,2118],{},"resultFetched"," to a list's ",[27,2121,2122],{},"append"," collects results synchronously, because ",[27,2125,255],{}," emits them on the calling thread when called directly. The assertions encode the filter's contract: labels short enough to read in the drop-down, and no search for queries below the minimum length. With a small GeoPackage fixture holding a dozen test assets, this runs in milliseconds and catches regressions in the expression or the display format, as in ",[21,2128,2130],{"href":2129},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","unit testing a QGIS plugin with pytest",[209,2132,2134],{"id":2133},"qgis-version-compatibility","QGIS version compatibility",[14,2136,2137,230,2139,2141,2142,230,2145,2148,2149,2152,2153,2156,2157,2159],{},[27,2138,29],{},[27,2140,1222],{}," have been available since QGIS 3.0. ",[27,2143,2144],{},"QgsLocatorResult.setUserData",[27,2146,2147],{},"userData()"," replaced direct attribute access in 3.18. ",[27,2150,2151],{},"QgsLocatorFilter.Priority.High"," is the scoped enum form required on the QGIS 4 series, as is ",[27,2154,2155],{},"QgsBlockingNetworkRequest.ErrorCode.NoError",". ",[27,2158,1003],{}," is the stable way to read layers from background threads on every version.",[209,2161,2163],{"id":2162},"troubleshooting","Troubleshooting",[214,2165,2166,2183,2189,2195,2203],{},[217,2167,2168,1219,2171,2173,2174,2177,2178,2180,2181,239],{},[220,2169,2170],{},"QGIS crashes when typing in the locator.",[27,2172,255],{}," touched a layer or ",[27,2175,2176],{},"iface","; move that into ",[27,2179,999],{}," or ",[27,2182,259],{},[217,2184,2185,2188],{},[220,2186,2187],{},"No results appear."," The filter was not registered, is disabled in the options, or the prefix differs from what you typed.",[217,2190,2191,2194],{},[220,2192,2193],{},"Stale results flash up."," Cancellation is not checked inside the loop.",[217,2196,2197,2200,2201,239],{},[220,2198,2199],{},"Crash after reloading the plugin."," The filter was not deregistered in ",[27,2202,233],{},[217,2204,2205,1219,2208,2210,2211,2213],{},[220,2206,2207],{},"Results work once then stop.",[27,2209,999],{}," returns ",[27,2212,559],{}," instead of a new filter.",[209,2215,2217],{"id":2216},"conclusion","Conclusion",[14,2219,2220,2221,2223,2224,2226,2227,2229,2230,2232,2233,2235],{},"Implement ",[27,2222,999],{}," to prepare thread-safe snapshots, ",[27,2225,255],{}," to search them in the background with cancellation checks, and ",[27,2228,259],{}," to act on the map from the main thread. Store identifiers in the result's user data, register filters in ",[27,2231,229],{}," and deregister them in ",[27,2234,233],{},", and choose a stable name, a short prefix and conservative defaults so the locator stays fast.",[209,2237,2239],{"id":2238},"frequently-asked-questions","Frequently Asked Questions",[14,2241,2242,2245,2246,239],{},[220,2243,2244],{},"Can a filter add actions to a result, such as \"open in browser\"?","\nYes. Recent releases support result actions: set them on the result and handle ",[27,2247,2248],{},"triggerResultFromAction",[14,2250,2251,2254],{},[220,2252,2253],{},"Does the filter work in QGIS Server or scripts?","\nNo. The locator is a desktop interface feature.",[14,2256,2257,2260,2261,2263,2264,239],{},[220,2258,2259],{},"Can one filter search several layers?","\nYes — prepare a feature source for each in ",[27,2262,999],{},", and group results by setting ",[27,2265,2266],{},"result.group",[14,2268,2269,2272,2273,2275,2276,2278,2279,2282,2283,2285],{},[220,2270,2271],{},"How do I test a locator filter?","\nCall ",[27,2274,122],{}," and then ",[27,2277,255],{}," directly with a ",[27,2280,2281],{},"QgsFeedback",", connecting ",[27,2284,2118],{}," to a list, in a headless test.",[209,2287,2289],{"id":2288},"related","Related",[214,2291,2292,2297,2303,2309,2315],{},[217,2293,2294,2296],{},[21,2295,24],{"href":23}," — the guide this recipe belongs to",[217,2298,2299],{},[21,2300,2302],{"href":2301},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-toolbar-button-to-qgis-plugin\u002F","Add a Toolbar Button to a QGIS Plugin",[217,2304,2305],{},[21,2306,2308],{"href":2307},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis\u002F","Add a Layer Context Menu Action in PyQGIS",[217,2310,2311],{},[21,2312,2314],{"href":2313},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Frun-background-task-with-qgstask-pyqgis\u002F","Run a Long Task with QgsTask in PyQGIS",[217,2316,2317],{},[21,2318,2320],{"href":2319},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002F","Speed Up Feature Iteration with QgsFeatureRequest",[2322,2323,2324],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":267,"searchDepth":290,"depth":290,"links":2326},[2327,2328,2329,2330,2331,2332,2333,2334,2335,2336,2337],{"id":211,"depth":290,"text":212},{"id":245,"depth":290,"text":246},{"id":1085,"depth":290,"text":1086},{"id":1234,"depth":290,"text":1235},{"id":1878,"depth":290,"text":1879},{"id":1908,"depth":290,"text":1909},{"id":2133,"depth":290,"text":2134},{"id":2162,"depth":290,"text":2163},{"id":2216,"depth":290,"text":2217},{"id":2238,"depth":290,"text":2239},{"id":2288,"depth":290,"text":2289},"Make plugin data searchable from the QGIS locator bar with QgsLocatorFilter — a prefix, fetchResults running in a background thread, clone for thread safety, zooming and selecting on triggerResult, searching a web API, and registering and deregistering the filter.","md",{"slug":2341,"type":2342,"breadcrumb":2343,"datePublished":2344,"dateModified":2344},"add-locator-filter-to-qgis-plugin","article","Add a Locator Filter","2026-09-17","\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-locator-filter-to-qgis-plugin",{"title":5,"description":2338},"qgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-locator-filter-to-qgis-plugin\u002Findex","iQtMKGmkSI_fwn2xme0OYxGxy-sIuM_HZe1IjAx5nA4",1789632907500]