[{"data":1,"prerenderedAt":2028},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis":3},{"id":4,"title":5,"body":6,"description":2017,"extension":2018,"meta":2019,"navigation":332,"path":2024,"seo":2025,"stem":2026,"__hash__":2027},"docs\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis\u002Findex.md","Add a Layer Context Menu Action in PyQGIS",{"type":7,"value":8,"toc":2005},"minimark",[9,13,26,40,228,233,260,264,274,728,749,753,764,850,1075,1095,1099,1105,1365,1379,1449,1453,1456,1816,1829,1832,1836,1864,1868,1911,1915,1926,1930,1936,1952,1962,1968,1972,2001],[10,11,5],"h1",{"id":12},"add-a-layer-context-menu-action-in-pyqgis",[14,15,16,17,21,22,25],"p",{},"A toolbar button is the default home for a plugin command, and often the wrong one. Commands that act on ",[18,19,20],"em",{},"a layer"," — export it to the corporate format, check its schema, publish it — belong where the user already has the layer in hand: its right-click menu in the Layers panel. Commands that act on ",[18,23,24],{},"features"," — open the asset record, create a work order from these three pipes — belong in the feature menus of the attribute table and the identify results. QGIS provides extension points for both, and for small status icons beside layer names.",[14,27,28,29,34,35,39],{},"This recipe belongs to ",[30,31,33],"a",{"href":32},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002F","Custom Map Tools and Canvas Interaction",". It adds a layer-type action to the Layers panel context menu, restricts actions to specific layers, registers feature actions with ",[36,37,38],"code",{},"QgsMapLayerAction",", adds a layer tree indicator, and removes everything on unload.",[14,41,42],{},[43,44,49,53,57,64,73,82,88,97,104,112,118,122,129,135,140,144,147,151,157,163,166,170,175,180,184,189,193,197,201,208,212,218,222,225],"svg",{"viewBox":45,"role":46,"ariaLabel":47,"xmlns":48},"0 0 760 300","img","Three extension points for plugin commands: the Layers panel context menu for layer commands, the attribute table and identify results for feature commands, and indicators beside layer names for status","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[50,51,52],"title",{},"Where layer and feature commands live",[54,55,56],"desc",{},"Left: a Layers panel with a right-click menu on the layer water_mains showing standard entries and a plugin submenu with Export to asset system and Check schema, added with addCustomActionForLayerType. Middle: an attribute table row with an actions menu entry Create work order, added with QgsMapLayerAction and the map layer action registry. Right: a layer name with a small warning icon indicator whose tooltip says schema check failed, added with a layer tree view indicator.",[58,59],"rect",{"x":60,"y":60,"width":61,"height":62,"fill":63},"0","760","300","#f6f3ea",[65,66,72],"text",{"x":67,"y":68,"style":69,"fill":70,"textAnchor":71},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Put the command next to the thing it acts on",[58,74],{"x":75,"y":76,"width":77,"height":77,"rx":78,"fill":79,"stroke":80,"style":81},"24","48","230","10","#fffdf7","#59645f","stroke-width:2",[65,83,87],{"x":84,"y":85,"style":86,"fill":70,"textAnchor":71},"139","72","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","Layers panel menu",[58,89],{"x":90,"y":91,"width":92,"height":75,"rx":93,"fill":94,"stroke":95,"style":96},"44","86","190","4","#dbeafe","#2563eb","stroke-width:1",[65,98,103],{"x":99,"y":100,"style":101,"fill":102},"56","103","font-size:10px;font-family:sans-serif","#2f3b35","water_mains",[58,105],{"x":106,"y":107,"width":108,"height":109,"rx":93,"fill":79,"stroke":110,"style":111},"70","116","170","140","#9aa39d","stroke-width:1.2",[65,113,117],{"x":114,"y":115,"style":116,"fill":80},"82","136","font-size:9.5px;font-family:sans-serif","Zoom to Layer",[65,119,121],{"x":114,"y":120,"style":116,"fill":80},"156","Open Attribute Table",[123,124],"line",{"x1":125,"y1":126,"x2":127,"y2":126,"stroke":128,"style":96},"78","166","232","#d9d3c4",[65,130,134],{"x":114,"y":131,"style":132,"fill":133},"184","font-size:9.5px;font-weight:bold;font-family:sans-serif","#0f766e","Asset tools ▸",[65,136,139],{"x":137,"y":138,"style":116,"fill":102},"94","206","Export to asset system",[65,141,143],{"x":137,"y":142,"style":116,"fill":102},"226","Check schema",[58,145],{"x":146,"y":76,"width":77,"height":77,"rx":78,"fill":79,"stroke":80,"style":81},"266",[65,148,150],{"x":149,"y":85,"style":86,"fill":70,"textAnchor":71},"381","feature menus",[58,152],{"x":153,"y":154,"width":92,"height":155,"fill":156},"286","90","22","#e8e4d8",[65,158,162],{"x":159,"y":160,"style":161,"fill":102},"296","105","font-size:9.5px;font-family:monospace","id  material  dn",[58,164],{"x":153,"y":165,"width":92,"height":155,"fill":94},"112",[65,167,169],{"x":159,"y":168,"style":161,"fill":102},"127","41  PE        180",[58,171],{"x":172,"y":173,"width":174,"height":99,"rx":93,"fill":79,"stroke":110,"style":111},"316","142","160",[65,176,179],{"x":177,"y":178,"style":132,"fill":95},"328","164","Create work order",[65,181,183],{"x":177,"y":182,"style":116,"fill":102},"186","Open asset record",[65,185,188],{"x":149,"y":186,"style":187,"fill":80,"textAnchor":71},"240","text-anchor:middle;font-size:9.5px;font-family:sans-serif","attribute table, identify",[58,190],{"x":191,"y":76,"width":192,"height":77,"rx":78,"fill":79,"stroke":80,"style":81},"508","228",[65,194,196],{"x":195,"y":85,"style":86,"fill":70,"textAnchor":71},"622","indicators",[65,198,103],{"x":199,"y":200,"style":101,"fill":102},"540","108",[202,203],"circle",{"cx":204,"cy":205,"r":206,"fill":207},"690","104","8","#b45309",[65,209,211],{"x":204,"y":200,"style":210,"fill":79,"textAnchor":71},"text-anchor:middle;font-size:10px;font-weight:bold;font-family:sans-serif","!",[58,213],{"x":214,"y":215,"width":108,"height":216,"rx":93,"fill":217,"stroke":207,"style":96},"548","120","30","#fdf2e2",[65,219,221],{"x":220,"y":84,"style":187,"fill":102,"textAnchor":71},"633","schema check failed",[65,223,224],{"x":199,"y":182,"style":101,"fill":102},"valves",[65,226,227],{"x":195,"y":186,"style":187,"fill":80,"textAnchor":71},"status beside the name",[229,230,232],"h2",{"id":231},"prerequisites","Prerequisites",[234,235,236,244],"ul",{},[237,238,239,243],"li",{},[240,241,242],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[237,245,246,247,250,251,254,255,259],{},"A plugin with ",[36,248,249],{},"initGui"," and ",[36,252,253],{},"unload",", as described in ",[30,256,258],{"href":257},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002F","QGIS plugin boilerplate and structure",". The console works for experiments, but actions added there must be removed by hand.",[229,261,263],{"id":262},"add-an-action-to-every-layer-of-a-type","Add an action to every layer of a type",[14,265,266,269,270,273],{},[36,267,268],{},"iface.addCustomActionForLayerType"," adds a ",[36,271,272],{},"QAction"," to the Layers panel context menu for all layers of one type, optionally inside a named submenu.",[275,276,281],"pre",{"className":277,"code":278,"language":279,"meta":280,"style":280},"language-python shiki shiki-themes github-dark","from qgis.PyQt.QtGui import QIcon\nfrom qgis.PyQt.QtWidgets import QAction\nfrom qgis.core import Qgis, QgsMapLayer\n\n\nclass AssetToolsPlugin:\n    MENU = \"Asset tools\"\n\n    def __init__(self, iface):\n        self.iface = iface\n        self.layer_actions = []\n\n    def initGui(self):\n        export = QAction(QIcon(\":\u002Fplugins\u002Fasset_tools\u002Fexport.svg\"),\n                         \"Export to asset system\", self.iface.mainWindow())\n        export.triggered.connect(self.export_current_layer)\n\n        check = QAction(\"Check schema\", self.iface.mainWindow())\n        check.triggered.connect(self.check_current_layer)\n\n        for action in (export, check):\n            self.iface.addCustomActionForLayerType(\n                action, self.MENU, Qgis.LayerType.Vector, True)\n            self.layer_actions.append(action)\n\n    def export_current_layer(self):\n        layer = self.iface.layerTreeView().currentLayer()\n        if layer is None:\n            return\n        self.iface.messageBar().pushInfo(\"Asset tools\", f\"Exporting {layer.name()} …\")\n\n    def check_current_layer(self):\n        layer = self.iface.layerTreeView().currentLayer()\n        ...\n\n    def unload(self):\n        for action in self.layer_actions:\n            self.iface.removeCustomActionForLayerType(action)\n        self.layer_actions.clear()\n","python","",[36,282,283,301,314,327,334,339,352,366,371,383,398,411,416,427,444,459,470,475,495,506,511,526,535,558,566,571,581,595,612,618,651,656,666,677,683,688,698,712,720],{"__ignoreMap":280},[284,285,287,291,295,298],"span",{"class":123,"line":286},1,[284,288,290],{"class":289},"snl16","from",[284,292,294],{"class":293},"s95oV"," qgis.PyQt.QtGui ",[284,296,297],{"class":289},"import",[284,299,300],{"class":293}," QIcon\n",[284,302,304,306,309,311],{"class":123,"line":303},2,[284,305,290],{"class":289},[284,307,308],{"class":293}," qgis.PyQt.QtWidgets ",[284,310,297],{"class":289},[284,312,313],{"class":293}," QAction\n",[284,315,317,319,322,324],{"class":123,"line":316},3,[284,318,290],{"class":289},[284,320,321],{"class":293}," qgis.core ",[284,323,297],{"class":289},[284,325,326],{"class":293}," Qgis, QgsMapLayer\n",[284,328,330],{"class":123,"line":329},4,[284,331,333],{"emptyLinePlaceholder":332},true,"\n",[284,335,337],{"class":123,"line":336},5,[284,338,333],{"emptyLinePlaceholder":332},[284,340,342,345,349],{"class":123,"line":341},6,[284,343,344],{"class":289},"class",[284,346,348],{"class":347},"svObZ"," AssetToolsPlugin",[284,350,351],{"class":293},":\n",[284,353,355,359,362],{"class":123,"line":354},7,[284,356,358],{"class":357},"sDLfK","    MENU",[284,360,361],{"class":289}," =",[284,363,365],{"class":364},"sU2Wk"," \"Asset tools\"\n",[284,367,369],{"class":123,"line":368},8,[284,370,333],{"emptyLinePlaceholder":332},[284,372,374,377,380],{"class":123,"line":373},9,[284,375,376],{"class":289},"    def",[284,378,379],{"class":357}," __init__",[284,381,382],{"class":293},"(self, iface):\n",[284,384,386,389,392,395],{"class":123,"line":385},10,[284,387,388],{"class":357},"        self",[284,390,391],{"class":293},".iface ",[284,393,394],{"class":289},"=",[284,396,397],{"class":293}," iface\n",[284,399,401,403,406,408],{"class":123,"line":400},11,[284,402,388],{"class":357},[284,404,405],{"class":293},".layer_actions ",[284,407,394],{"class":289},[284,409,410],{"class":293}," []\n",[284,412,414],{"class":123,"line":413},12,[284,415,333],{"emptyLinePlaceholder":332},[284,417,419,421,424],{"class":123,"line":418},13,[284,420,376],{"class":289},[284,422,423],{"class":347}," initGui",[284,425,426],{"class":293},"(self):\n",[284,428,430,433,435,438,441],{"class":123,"line":429},14,[284,431,432],{"class":293},"        export ",[284,434,394],{"class":289},[284,436,437],{"class":293}," QAction(QIcon(",[284,439,440],{"class":364},"\":\u002Fplugins\u002Fasset_tools\u002Fexport.svg\"",[284,442,443],{"class":293},"),\n",[284,445,447,450,453,456],{"class":123,"line":446},15,[284,448,449],{"class":364},"                         \"Export to asset system\"",[284,451,452],{"class":293},", ",[284,454,455],{"class":357},"self",[284,457,458],{"class":293},".iface.mainWindow())\n",[284,460,462,465,467],{"class":123,"line":461},16,[284,463,464],{"class":293},"        export.triggered.connect(",[284,466,455],{"class":357},[284,468,469],{"class":293},".export_current_layer)\n",[284,471,473],{"class":123,"line":472},17,[284,474,333],{"emptyLinePlaceholder":332},[284,476,478,481,483,486,489,491,493],{"class":123,"line":477},18,[284,479,480],{"class":293},"        check ",[284,482,394],{"class":289},[284,484,485],{"class":293}," QAction(",[284,487,488],{"class":364},"\"Check schema\"",[284,490,452],{"class":293},[284,492,455],{"class":357},[284,494,458],{"class":293},[284,496,498,501,503],{"class":123,"line":497},19,[284,499,500],{"class":293},"        check.triggered.connect(",[284,502,455],{"class":357},[284,504,505],{"class":293},".check_current_layer)\n",[284,507,509],{"class":123,"line":508},20,[284,510,333],{"emptyLinePlaceholder":332},[284,512,514,517,520,523],{"class":123,"line":513},21,[284,515,516],{"class":289},"        for",[284,518,519],{"class":293}," action ",[284,521,522],{"class":289},"in",[284,524,525],{"class":293}," (export, check):\n",[284,527,529,532],{"class":123,"line":528},22,[284,530,531],{"class":357},"            self",[284,533,534],{"class":293},".iface.addCustomActionForLayerType(\n",[284,536,538,541,543,546,549,552,555],{"class":123,"line":537},23,[284,539,540],{"class":293},"                action, ",[284,542,455],{"class":357},[284,544,545],{"class":293},".",[284,547,548],{"class":357},"MENU",[284,550,551],{"class":293},", Qgis.LayerType.Vector, ",[284,553,554],{"class":357},"True",[284,556,557],{"class":293},")\n",[284,559,561,563],{"class":123,"line":560},24,[284,562,531],{"class":357},[284,564,565],{"class":293},".layer_actions.append(action)\n",[284,567,569],{"class":123,"line":568},25,[284,570,333],{"emptyLinePlaceholder":332},[284,572,574,576,579],{"class":123,"line":573},26,[284,575,376],{"class":289},[284,577,578],{"class":347}," export_current_layer",[284,580,426],{"class":293},[284,582,584,587,589,592],{"class":123,"line":583},27,[284,585,586],{"class":293},"        layer ",[284,588,394],{"class":289},[284,590,591],{"class":357}," self",[284,593,594],{"class":293},".iface.layerTreeView().currentLayer()\n",[284,596,598,601,604,607,610],{"class":123,"line":597},28,[284,599,600],{"class":289},"        if",[284,602,603],{"class":293}," layer ",[284,605,606],{"class":289},"is",[284,608,609],{"class":357}," None",[284,611,351],{"class":293},[284,613,615],{"class":123,"line":614},29,[284,616,617],{"class":289},"            return\n",[284,619,621,623,626,629,631,634,637,640,643,646,649],{"class":123,"line":620},30,[284,622,388],{"class":357},[284,624,625],{"class":293},".iface.messageBar().pushInfo(",[284,627,628],{"class":364},"\"Asset tools\"",[284,630,452],{"class":293},[284,632,633],{"class":289},"f",[284,635,636],{"class":364},"\"Exporting ",[284,638,639],{"class":357},"{",[284,641,642],{"class":293},"layer.name()",[284,644,645],{"class":357},"}",[284,647,648],{"class":364}," …\"",[284,650,557],{"class":293},[284,652,654],{"class":123,"line":653},31,[284,655,333],{"emptyLinePlaceholder":332},[284,657,659,661,664],{"class":123,"line":658},32,[284,660,376],{"class":289},[284,662,663],{"class":347}," check_current_layer",[284,665,426],{"class":293},[284,667,669,671,673,675],{"class":123,"line":668},33,[284,670,586],{"class":293},[284,672,394],{"class":289},[284,674,591],{"class":357},[284,676,594],{"class":293},[284,678,680],{"class":123,"line":679},34,[284,681,682],{"class":357},"        ...\n",[284,684,686],{"class":123,"line":685},35,[284,687,333],{"emptyLinePlaceholder":332},[284,689,691,693,696],{"class":123,"line":690},36,[284,692,376],{"class":289},[284,694,695],{"class":347}," unload",[284,697,426],{"class":293},[284,699,701,703,705,707,709],{"class":123,"line":700},37,[284,702,516],{"class":289},[284,704,519],{"class":293},[284,706,522],{"class":289},[284,708,591],{"class":357},[284,710,711],{"class":293},".layer_actions:\n",[284,713,715,717],{"class":123,"line":714},38,[284,716,531],{"class":357},[284,718,719],{"class":293},".iface.removeCustomActionForLayerType(action)\n",[284,721,723,725],{"class":123,"line":722},39,[284,724,388],{"class":357},[284,726,727],{"class":293},".layer_actions.clear()\n",[14,729,730,733,734,737,738,741,742,745,746,748],{},[240,731,732],{},"Breakdown:"," The second argument names a submenu; passing an empty string puts the action at the top level of the menu, which is best reserved for a single, very common command. The last argument, ",[36,735,736],{},"allLayers=True",", shows the action for every vector layer. Right-clicking a layer makes it the current layer in the tree view, so ",[36,739,740],{},"currentLayer()"," inside the handler is the layer the user clicked — the handler receives no layer argument of its own. ",[36,743,744],{},"removeCustomActionForLayerType"," in ",[36,747,253],{}," is essential: without it, reloading the plugin during development adds a second copy of every entry, and uninstalling leaves dead actions behind until QGIS restarts.",[229,750,752],{"id":751},"show-an-action-only-for-suitable-layers","Show an action only for suitable layers",[14,754,755,756,759,760,763],{},"Many commands only make sense for some layers — the export only for layers that have the asset schema. Passing ",[36,757,758],{},"allLayers=False"," registers the action for no layers, and ",[36,761,762],{},"addCustomActionForLayer"," then enables it layer by layer.",[14,765,766],{},[43,767,770,773,776,779,783,788,794,798,804,807,809,812,816,820,822,827,831,834,837,839,841,843,846,848],{"viewBox":768,"role":46,"ariaLabel":769,"xmlns":48},"0 0 760 256","Targeting context menu actions: allLayers true shows the action on every vector layer, allLayers false plus addCustomActionForLayer shows it only on layers that pass a check, and new layers are checked when they are added to the project",[50,771,772],{},"Every layer, or only the right ones",[54,774,775],{},"Left: with allLayers true, the Export to asset system action appears on water_mains, valves, parcels and a basemap, including layers where it would fail. Right: with allLayers false, the plugin checks each layer's fields when it is added to the project and calls addCustomActionForLayer only for water_mains and valves, which have an asset_id field. The action simply does not appear on parcels or the basemap.",[58,777],{"x":60,"y":60,"width":61,"height":778,"fill":63},"256",[65,780,782],{"x":67,"y":781,"style":69,"fill":70,"textAnchor":71},"26","Hide commands that cannot work on a layer",[58,784],{"x":75,"y":90,"width":785,"height":786,"rx":78,"fill":217,"stroke":207,"style":787},"344","192","stroke-width:2.5",[65,789,793],{"x":790,"y":791,"style":792,"fill":207,"textAnchor":71},"196","68","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","allLayers = True",[65,795,103],{"x":796,"y":205,"style":797,"fill":102},"60","font-size:10.5px;font-family:sans-serif",[65,799,803],{"x":800,"y":205,"style":801,"fill":802},"330","text-anchor:end;font-size:10.5px;font-family:sans-serif","#166534","✓ shown",[65,805,224],{"x":796,"y":806,"style":797,"fill":102},"134",[65,808,803],{"x":800,"y":806,"style":801,"fill":802},[65,810,811],{"x":796,"y":178,"style":797,"fill":102},"parcels",[65,813,815],{"x":800,"y":178,"style":801,"fill":814},"#b91c1c","shown, fails",[65,817,819],{"x":796,"y":818,"style":797,"fill":102},"194","roads_osm",[65,821,815],{"x":800,"y":818,"style":801,"fill":814},[58,823],{"x":824,"y":90,"width":785,"height":786,"rx":78,"fill":825,"stroke":826,"style":787},"392","#e8efe6","#15803d",[65,828,830],{"x":829,"y":791,"style":792,"fill":802,"textAnchor":71},"564","allLayers = False + per layer",[65,832,103],{"x":833,"y":205,"style":797,"fill":102},"428",[65,835,803],{"x":836,"y":205,"style":801,"fill":802},"698",[65,838,224],{"x":833,"y":806,"style":797,"fill":102},[65,840,803],{"x":836,"y":806,"style":801,"fill":802},[65,842,811],{"x":833,"y":178,"style":797,"fill":102},[65,844,845],{"x":836,"y":178,"style":801,"fill":80},"not offered",[65,847,819],{"x":833,"y":818,"style":797,"fill":102},[65,849,845],{"x":836,"y":818,"style":801,"fill":80},[275,851,853],{"className":277,"code":852,"language":279,"meta":280,"style":280},"from qgis.core import QgsProject\n\nclass AssetToolsPlugin(AssetToolsPlugin):\n    def initGui(self):\n        self.export = QAction(\"Export to asset system\", self.iface.mainWindow())\n        self.export.triggered.connect(self.export_current_layer)\n        self.iface.addCustomActionForLayerType(self.export, self.MENU, Qgis.LayerType.Vector, False)\n\n        project = QgsProject.instance()\n        for layer in project.mapLayers().values():\n            self._maybe_enable(layer)\n        project.layerWasAdded.connect(self._maybe_enable)\n\n    def _maybe_enable(self, layer):\n        if layer.type() == Qgis.LayerType.Vector and \"asset_id\" in layer.fields().names():\n            self.iface.addCustomActionForLayer(self.export, layer)\n\n    def unload(self):\n        QgsProject.instance().layerWasAdded.disconnect(self._maybe_enable)\n        self.iface.removeCustomActionForLayerType(self.export)\n",[36,854,855,866,870,885,893,913,924,949,953,963,974,981,991,995,1005,1030,1042,1046,1054,1063],{"__ignoreMap":280},[284,856,857,859,861,863],{"class":123,"line":286},[284,858,290],{"class":289},[284,860,321],{"class":293},[284,862,297],{"class":289},[284,864,865],{"class":293}," QgsProject\n",[284,867,868],{"class":123,"line":303},[284,869,333],{"emptyLinePlaceholder":332},[284,871,872,874,876,879,882],{"class":123,"line":316},[284,873,344],{"class":289},[284,875,348],{"class":347},[284,877,878],{"class":293},"(",[284,880,881],{"class":347},"AssetToolsPlugin",[284,883,884],{"class":293},"):\n",[284,886,887,889,891],{"class":123,"line":329},[284,888,376],{"class":289},[284,890,423],{"class":347},[284,892,426],{"class":293},[284,894,895,897,900,902,904,907,909,911],{"class":123,"line":336},[284,896,388],{"class":357},[284,898,899],{"class":293},".export ",[284,901,394],{"class":289},[284,903,485],{"class":293},[284,905,906],{"class":364},"\"Export to asset system\"",[284,908,452],{"class":293},[284,910,455],{"class":357},[284,912,458],{"class":293},[284,914,915,917,920,922],{"class":123,"line":341},[284,916,388],{"class":357},[284,918,919],{"class":293},".export.triggered.connect(",[284,921,455],{"class":357},[284,923,469],{"class":293},[284,925,926,928,931,933,936,938,940,942,944,947],{"class":123,"line":354},[284,927,388],{"class":357},[284,929,930],{"class":293},".iface.addCustomActionForLayerType(",[284,932,455],{"class":357},[284,934,935],{"class":293},".export, ",[284,937,455],{"class":357},[284,939,545],{"class":293},[284,941,548],{"class":357},[284,943,551],{"class":293},[284,945,946],{"class":357},"False",[284,948,557],{"class":293},[284,950,951],{"class":123,"line":368},[284,952,333],{"emptyLinePlaceholder":332},[284,954,955,958,960],{"class":123,"line":373},[284,956,957],{"class":293},"        project ",[284,959,394],{"class":289},[284,961,962],{"class":293}," QgsProject.instance()\n",[284,964,965,967,969,971],{"class":123,"line":385},[284,966,516],{"class":289},[284,968,603],{"class":293},[284,970,522],{"class":289},[284,972,973],{"class":293}," project.mapLayers().values():\n",[284,975,976,978],{"class":123,"line":400},[284,977,531],{"class":357},[284,979,980],{"class":293},"._maybe_enable(layer)\n",[284,982,983,986,988],{"class":123,"line":413},[284,984,985],{"class":293},"        project.layerWasAdded.connect(",[284,987,455],{"class":357},[284,989,990],{"class":293},"._maybe_enable)\n",[284,992,993],{"class":123,"line":418},[284,994,333],{"emptyLinePlaceholder":332},[284,996,997,999,1002],{"class":123,"line":429},[284,998,376],{"class":289},[284,1000,1001],{"class":347}," _maybe_enable",[284,1003,1004],{"class":293},"(self, layer):\n",[284,1006,1007,1009,1012,1015,1018,1021,1024,1027],{"class":123,"line":446},[284,1008,600],{"class":289},[284,1010,1011],{"class":293}," layer.type() ",[284,1013,1014],{"class":289},"==",[284,1016,1017],{"class":293}," Qgis.LayerType.Vector ",[284,1019,1020],{"class":289},"and",[284,1022,1023],{"class":364}," \"asset_id\"",[284,1025,1026],{"class":289}," in",[284,1028,1029],{"class":293}," layer.fields().names():\n",[284,1031,1032,1034,1037,1039],{"class":123,"line":461},[284,1033,531],{"class":357},[284,1035,1036],{"class":293},".iface.addCustomActionForLayer(",[284,1038,455],{"class":357},[284,1040,1041],{"class":293},".export, layer)\n",[284,1043,1044],{"class":123,"line":472},[284,1045,333],{"emptyLinePlaceholder":332},[284,1047,1048,1050,1052],{"class":123,"line":477},[284,1049,376],{"class":289},[284,1051,695],{"class":347},[284,1053,426],{"class":293},[284,1055,1056,1059,1061],{"class":123,"line":497},[284,1057,1058],{"class":293},"        QgsProject.instance().layerWasAdded.disconnect(",[284,1060,455],{"class":357},[284,1062,990],{"class":293},[284,1064,1065,1067,1070,1072],{"class":123,"line":508},[284,1066,388],{"class":357},[284,1068,1069],{"class":293},".iface.removeCustomActionForLayerType(",[284,1071,455],{"class":357},[284,1073,1074],{"class":293},".export)\n",[14,1076,1077,1079,1080,1082,1083,1086,1087,1089,1090,1094],{},[240,1078,732],{}," Registering with ",[36,1081,758],{}," tells QGIS the action exists but belongs to no layer yet. Checking existing layers at start-up and every new layer through ",[36,1084,1085],{},"layerWasAdded"," keeps the menu accurate as projects change. The test here is a field name; checking provider type, geometry type, a custom layer property or a naming convention works the same way. Disconnecting the signal in ",[36,1088,253],{}," avoids calls into a plugin that is no longer loaded — the signal pattern is covered in ",[30,1091,1093],{"href":1092},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fconnect-layer-signals-pyqgis\u002F","connecting to layer and project signals",". The subclass here only separates the example from the previous one; in a real plugin, this is the whole class.",[229,1096,1098],{"id":1097},"feature-actions-in-the-attribute-table-and-identify-results","Feature actions in the attribute table and identify results",[14,1100,1101,1102,1104],{},"For commands on features, register a ",[36,1103,38],{},". It appears in the attribute table's actions column and toolbar, in the identify results' feature menu, and in feature forms, and it receives the features directly.",[275,1106,1108],{"className":277,"code":1107,"language":279,"meta":280,"style":280},"from qgis.gui import QgsGui, QgsMapLayerAction\n\nclass WorkOrders:\n    def __init__(self, iface):\n        self.iface = iface\n        self.action = None\n\n    def initGui(self):\n        self.action = QgsMapLayerAction(\n            \"Create work order\", self.iface.mainWindow(),\n            Qgis.LayerType.Vector,\n            Qgis.MapLayerActionTarget.SingleFeature | Qgis.MapLayerActionTarget.MultipleFeatures,\n        )\n        self.action.triggeredForFeaturesV2.connect(self.create_work_order)\n        QgsGui.mapLayerActionRegistry().addMapLayerAction(self.action)\n\n    def create_work_order(self, layer, features, context):\n        ids = [f[\"asset_id\"] for f in features]\n        self.iface.messageBar().pushSuccess(\n            \"Work orders\", f\"Draft order created for {len(ids)} assets from {layer.name()}\")\n\n    def unload(self):\n        if self.action:\n            QgsGui.mapLayerActionRegistry().removeMapLayerAction(self.action)\n            self.action = None\n",[36,1109,1110,1122,1126,1135,1143,1153,1165,1169,1177,1188,1200,1205,1216,1221,1233,1243,1247,1257,1284,1291,1325,1329,1337,1346,1355],{"__ignoreMap":280},[284,1111,1112,1114,1117,1119],{"class":123,"line":286},[284,1113,290],{"class":289},[284,1115,1116],{"class":293}," qgis.gui ",[284,1118,297],{"class":289},[284,1120,1121],{"class":293}," QgsGui, QgsMapLayerAction\n",[284,1123,1124],{"class":123,"line":303},[284,1125,333],{"emptyLinePlaceholder":332},[284,1127,1128,1130,1133],{"class":123,"line":316},[284,1129,344],{"class":289},[284,1131,1132],{"class":347}," WorkOrders",[284,1134,351],{"class":293},[284,1136,1137,1139,1141],{"class":123,"line":329},[284,1138,376],{"class":289},[284,1140,379],{"class":357},[284,1142,382],{"class":293},[284,1144,1145,1147,1149,1151],{"class":123,"line":336},[284,1146,388],{"class":357},[284,1148,391],{"class":293},[284,1150,394],{"class":289},[284,1152,397],{"class":293},[284,1154,1155,1157,1160,1162],{"class":123,"line":341},[284,1156,388],{"class":357},[284,1158,1159],{"class":293},".action ",[284,1161,394],{"class":289},[284,1163,1164],{"class":357}," None\n",[284,1166,1167],{"class":123,"line":354},[284,1168,333],{"emptyLinePlaceholder":332},[284,1170,1171,1173,1175],{"class":123,"line":368},[284,1172,376],{"class":289},[284,1174,423],{"class":347},[284,1176,426],{"class":293},[284,1178,1179,1181,1183,1185],{"class":123,"line":373},[284,1180,388],{"class":357},[284,1182,1159],{"class":293},[284,1184,394],{"class":289},[284,1186,1187],{"class":293}," QgsMapLayerAction(\n",[284,1189,1190,1193,1195,1197],{"class":123,"line":385},[284,1191,1192],{"class":364},"            \"Create work order\"",[284,1194,452],{"class":293},[284,1196,455],{"class":357},[284,1198,1199],{"class":293},".iface.mainWindow(),\n",[284,1201,1202],{"class":123,"line":400},[284,1203,1204],{"class":293},"            Qgis.LayerType.Vector,\n",[284,1206,1207,1210,1213],{"class":123,"line":413},[284,1208,1209],{"class":293},"            Qgis.MapLayerActionTarget.SingleFeature ",[284,1211,1212],{"class":289},"|",[284,1214,1215],{"class":293}," Qgis.MapLayerActionTarget.MultipleFeatures,\n",[284,1217,1218],{"class":123,"line":418},[284,1219,1220],{"class":293},"        )\n",[284,1222,1223,1225,1228,1230],{"class":123,"line":429},[284,1224,388],{"class":357},[284,1226,1227],{"class":293},".action.triggeredForFeaturesV2.connect(",[284,1229,455],{"class":357},[284,1231,1232],{"class":293},".create_work_order)\n",[284,1234,1235,1238,1240],{"class":123,"line":446},[284,1236,1237],{"class":293},"        QgsGui.mapLayerActionRegistry().addMapLayerAction(",[284,1239,455],{"class":357},[284,1241,1242],{"class":293},".action)\n",[284,1244,1245],{"class":123,"line":461},[284,1246,333],{"emptyLinePlaceholder":332},[284,1248,1249,1251,1254],{"class":123,"line":472},[284,1250,376],{"class":289},[284,1252,1253],{"class":347}," create_work_order",[284,1255,1256],{"class":293},"(self, layer, features, context):\n",[284,1258,1259,1262,1264,1267,1270,1273,1276,1279,1281],{"class":123,"line":477},[284,1260,1261],{"class":293},"        ids ",[284,1263,394],{"class":289},[284,1265,1266],{"class":293}," [f[",[284,1268,1269],{"class":364},"\"asset_id\"",[284,1271,1272],{"class":293},"] ",[284,1274,1275],{"class":289},"for",[284,1277,1278],{"class":293}," f ",[284,1280,522],{"class":289},[284,1282,1283],{"class":293}," features]\n",[284,1285,1286,1288],{"class":123,"line":497},[284,1287,388],{"class":357},[284,1289,1290],{"class":293},".iface.messageBar().pushSuccess(\n",[284,1292,1293,1296,1298,1300,1303,1306,1309,1311,1314,1316,1318,1320,1323],{"class":123,"line":508},[284,1294,1295],{"class":364},"            \"Work orders\"",[284,1297,452],{"class":293},[284,1299,633],{"class":289},[284,1301,1302],{"class":364},"\"Draft order created for ",[284,1304,1305],{"class":357},"{len",[284,1307,1308],{"class":293},"(ids)",[284,1310,645],{"class":357},[284,1312,1313],{"class":364}," assets from ",[284,1315,639],{"class":357},[284,1317,642],{"class":293},[284,1319,645],{"class":357},[284,1321,1322],{"class":364},"\"",[284,1324,557],{"class":293},[284,1326,1327],{"class":123,"line":513},[284,1328,333],{"emptyLinePlaceholder":332},[284,1330,1331,1333,1335],{"class":123,"line":528},[284,1332,376],{"class":289},[284,1334,695],{"class":347},[284,1336,426],{"class":293},[284,1338,1339,1341,1343],{"class":123,"line":537},[284,1340,600],{"class":289},[284,1342,591],{"class":357},[284,1344,1345],{"class":293},".action:\n",[284,1347,1348,1351,1353],{"class":123,"line":560},[284,1349,1350],{"class":293},"            QgsGui.mapLayerActionRegistry().removeMapLayerAction(",[284,1352,455],{"class":357},[284,1354,1242],{"class":293},[284,1356,1357,1359,1361,1363],{"class":123,"line":568},[284,1358,531],{"class":357},[284,1360,1159],{"class":293},[284,1362,394],{"class":289},[284,1364,1164],{"class":357},[14,1366,1367,1369,1370,1373,1374,1378],{},[240,1368,732],{}," Targets declare what the action works on: a single feature, several selected features, or the layer. Declaring both single and multiple lets users run it on one row or a selection. The ",[36,1371,1372],{},"V2"," signals pass a context object alongside the layer and features, which carries the originating widget; the older signals without context still work but are deprecated. Unlike the layer-tree actions, these appear wherever QGIS offers feature actions, so there is one registration for several interfaces. For actions that belong to one layer's data rather than a plugin, the lighter alternative is ",[30,1375,1377],{"href":1376},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fcreate-layer-actions-pyqgis\u002F","creating layer actions",", which are stored with the layer.",[14,1380,1381],{},[43,1382,1385,1388,1391,1394,1397,1400,1404,1407,1411,1413,1416,1418,1421,1423,1428,1432,1437,1441,1445],{"viewBox":1383,"role":46,"ariaLabel":1384,"xmlns":48},"0 0 760 236","Layer tree indicators: a small icon beside a layer name set per layer tree node, with a tooltip and a clicked signal, added with addIndicator and removed with removeIndicator when the status clears or the plugin unloads",[50,1386,1387],{},"A status icon beside the layer name",[54,1389,1390],{},"The Layers panel shows three layers. water_mains carries an orange warning indicator with tooltip schema check failed: missing field dn. Clicking the indicator emits clicked, which the plugin uses to open a report. valves carries a green tick indicator. parcels has none. The plugin keeps a dictionary from layer id to indicator so it can remove or replace indicators when a check is re-run.",[58,1392],{"x":60,"y":60,"width":61,"height":1393,"fill":63},"236",[65,1395,1396],{"x":67,"y":781,"style":69,"fill":70,"textAnchor":71},"Indicators show state without opening anything",[58,1398],{"x":75,"y":90,"width":800,"height":1399,"rx":78,"fill":79,"stroke":80,"style":81},"172",[65,1401,1403],{"x":1402,"y":791,"style":86,"fill":70,"textAnchor":71},"189","Layers",[65,1405,103],{"x":1406,"y":205,"style":797,"fill":102},"52",[202,1408],{"cx":1409,"cy":1410,"r":206,"fill":207},"320","100",[65,1412,211],{"x":1409,"y":205,"style":210,"fill":79,"textAnchor":71},[65,1414,224],{"x":1406,"y":1415,"style":797,"fill":102},"144",[202,1417],{"cx":1409,"cy":109,"r":206,"fill":826},[65,1419,1420],{"x":1409,"y":1415,"style":210,"fill":79,"textAnchor":71},"✓",[65,1422,811],{"x":1406,"y":131,"style":797,"fill":102},[58,1424],{"x":1425,"y":90,"width":1426,"height":1399,"rx":78,"fill":1427,"stroke":133,"style":81},"378","358","#eef7f4",[65,1429,1431],{"x":1430,"y":791,"style":86,"fill":133,"textAnchor":71},"557","per indicator",[65,1433,1436],{"x":1434,"y":1410,"style":1435,"fill":102},"400","font-size:10px;font-family:monospace","setIcon · setToolTip",[65,1438,1440],{"x":1434,"y":1439,"style":1435,"fill":102},"126","clicked → open report",[65,1442,1444],{"x":1434,"y":1443,"style":1435,"fill":102},"152","view.addIndicator(node, ind)",[65,1446,1448],{"x":1434,"y":1447,"style":1435,"fill":102},"178","view.removeIndicator(node, ind)",[229,1450,1452],{"id":1451},"show-status-with-a-layer-tree-indicator","Show status with a layer tree indicator",[14,1454,1455],{},"Indicators are small clickable icons beside a layer's name — QGIS uses them for filters, embedded layers and invalid sources. A plugin can add its own to surface the result of a check without opening a dialog.",[275,1457,1459],{"className":277,"code":1458,"language":279,"meta":280,"style":280},"from qgis.gui import QgsLayerTreeViewIndicator\n\nclass SchemaIndicators:\n    def __init__(self, iface):\n        self.iface = iface\n        self.indicators = {}\n\n    def mark(self, layer, ok, message):\n        view = self.iface.layerTreeView()\n        node = QgsProject.instance().layerTreeRoot().findLayer(layer.id())\n        if node is None:\n            return\n        self.clear(layer)\n        indicator = QgsLayerTreeViewIndicator(view)\n        indicator.setIcon(QIcon(\":\u002Fimages\u002Fthemes\u002Fdefault\u002FmIconSuccess.svg\" if ok\n                                else \":\u002Fimages\u002Fthemes\u002Fdefault\u002FmIconWarning.svg\"))\n        indicator.setToolTip(message)\n        indicator.clicked.connect(lambda _index, lyr=layer: self.show_report(lyr))\n        view.addIndicator(node, indicator)\n        self.indicators[layer.id()] = (node, indicator)\n\n    def clear(self, layer):\n        entry = self.indicators.pop(layer.id(), None)\n        if entry:\n            node, indicator = entry\n            self.iface.layerTreeView().removeIndicator(node, indicator)\n\n    def show_report(self, layer):\n        entry = self.indicators.get(layer.id())\n        if entry:\n            _node, indicator = entry\n            self.iface.messageBar().pushInfo(f\"Schema check: {layer.name()}\", indicator.toolTip())\n\n    def unload(self):\n        for layer_id in list(self.indicators):\n            node, indicator = self.indicators.pop(layer_id)\n            self.iface.layerTreeView().removeIndicator(node, indicator)\n",[36,1460,1461,1472,1476,1485,1493,1503,1515,1519,1529,1541,1551,1564,1568,1575,1585,1599,1610,1615,1636,1641,1653,1657,1666,1683,1690,1700,1707,1711,1720,1731,1737,1746,1768,1772,1780,1799,1810],{"__ignoreMap":280},[284,1462,1463,1465,1467,1469],{"class":123,"line":286},[284,1464,290],{"class":289},[284,1466,1116],{"class":293},[284,1468,297],{"class":289},[284,1470,1471],{"class":293}," QgsLayerTreeViewIndicator\n",[284,1473,1474],{"class":123,"line":303},[284,1475,333],{"emptyLinePlaceholder":332},[284,1477,1478,1480,1483],{"class":123,"line":316},[284,1479,344],{"class":289},[284,1481,1482],{"class":347}," SchemaIndicators",[284,1484,351],{"class":293},[284,1486,1487,1489,1491],{"class":123,"line":329},[284,1488,376],{"class":289},[284,1490,379],{"class":357},[284,1492,382],{"class":293},[284,1494,1495,1497,1499,1501],{"class":123,"line":336},[284,1496,388],{"class":357},[284,1498,391],{"class":293},[284,1500,394],{"class":289},[284,1502,397],{"class":293},[284,1504,1505,1507,1510,1512],{"class":123,"line":341},[284,1506,388],{"class":357},[284,1508,1509],{"class":293},".indicators ",[284,1511,394],{"class":289},[284,1513,1514],{"class":293}," {}\n",[284,1516,1517],{"class":123,"line":354},[284,1518,333],{"emptyLinePlaceholder":332},[284,1520,1521,1523,1526],{"class":123,"line":368},[284,1522,376],{"class":289},[284,1524,1525],{"class":347}," mark",[284,1527,1528],{"class":293},"(self, layer, ok, message):\n",[284,1530,1531,1534,1536,1538],{"class":123,"line":373},[284,1532,1533],{"class":293},"        view ",[284,1535,394],{"class":289},[284,1537,591],{"class":357},[284,1539,1540],{"class":293},".iface.layerTreeView()\n",[284,1542,1543,1546,1548],{"class":123,"line":385},[284,1544,1545],{"class":293},"        node ",[284,1547,394],{"class":289},[284,1549,1550],{"class":293}," QgsProject.instance().layerTreeRoot().findLayer(layer.id())\n",[284,1552,1553,1555,1558,1560,1562],{"class":123,"line":400},[284,1554,600],{"class":289},[284,1556,1557],{"class":293}," node ",[284,1559,606],{"class":289},[284,1561,609],{"class":357},[284,1563,351],{"class":293},[284,1565,1566],{"class":123,"line":413},[284,1567,617],{"class":289},[284,1569,1570,1572],{"class":123,"line":418},[284,1571,388],{"class":357},[284,1573,1574],{"class":293},".clear(layer)\n",[284,1576,1577,1580,1582],{"class":123,"line":429},[284,1578,1579],{"class":293},"        indicator ",[284,1581,394],{"class":289},[284,1583,1584],{"class":293}," QgsLayerTreeViewIndicator(view)\n",[284,1586,1587,1590,1593,1596],{"class":123,"line":446},[284,1588,1589],{"class":293},"        indicator.setIcon(QIcon(",[284,1591,1592],{"class":364},"\":\u002Fimages\u002Fthemes\u002Fdefault\u002FmIconSuccess.svg\"",[284,1594,1595],{"class":289}," if",[284,1597,1598],{"class":293}," ok\n",[284,1600,1601,1604,1607],{"class":123,"line":461},[284,1602,1603],{"class":289},"                                else",[284,1605,1606],{"class":364}," \":\u002Fimages\u002Fthemes\u002Fdefault\u002FmIconWarning.svg\"",[284,1608,1609],{"class":293},"))\n",[284,1611,1612],{"class":123,"line":472},[284,1613,1614],{"class":293},"        indicator.setToolTip(message)\n",[284,1616,1617,1620,1623,1626,1628,1631,1633],{"class":123,"line":477},[284,1618,1619],{"class":293},"        indicator.clicked.connect(",[284,1621,1622],{"class":289},"lambda",[284,1624,1625],{"class":293}," _index, lyr",[284,1627,394],{"class":289},[284,1629,1630],{"class":293},"layer: ",[284,1632,455],{"class":357},[284,1634,1635],{"class":293},".show_report(lyr))\n",[284,1637,1638],{"class":123,"line":497},[284,1639,1640],{"class":293},"        view.addIndicator(node, indicator)\n",[284,1642,1643,1645,1648,1650],{"class":123,"line":508},[284,1644,388],{"class":357},[284,1646,1647],{"class":293},".indicators[layer.id()] ",[284,1649,394],{"class":289},[284,1651,1652],{"class":293}," (node, indicator)\n",[284,1654,1655],{"class":123,"line":513},[284,1656,333],{"emptyLinePlaceholder":332},[284,1658,1659,1661,1664],{"class":123,"line":528},[284,1660,376],{"class":289},[284,1662,1663],{"class":347}," clear",[284,1665,1004],{"class":293},[284,1667,1668,1671,1673,1675,1678,1681],{"class":123,"line":537},[284,1669,1670],{"class":293},"        entry ",[284,1672,394],{"class":289},[284,1674,591],{"class":357},[284,1676,1677],{"class":293},".indicators.pop(layer.id(), ",[284,1679,1680],{"class":357},"None",[284,1682,557],{"class":293},[284,1684,1685,1687],{"class":123,"line":560},[284,1686,600],{"class":289},[284,1688,1689],{"class":293}," entry:\n",[284,1691,1692,1695,1697],{"class":123,"line":568},[284,1693,1694],{"class":293},"            node, indicator ",[284,1696,394],{"class":289},[284,1698,1699],{"class":293}," entry\n",[284,1701,1702,1704],{"class":123,"line":573},[284,1703,531],{"class":357},[284,1705,1706],{"class":293},".iface.layerTreeView().removeIndicator(node, indicator)\n",[284,1708,1709],{"class":123,"line":583},[284,1710,333],{"emptyLinePlaceholder":332},[284,1712,1713,1715,1718],{"class":123,"line":597},[284,1714,376],{"class":289},[284,1716,1717],{"class":347}," show_report",[284,1719,1004],{"class":293},[284,1721,1722,1724,1726,1728],{"class":123,"line":614},[284,1723,1670],{"class":293},[284,1725,394],{"class":289},[284,1727,591],{"class":357},[284,1729,1730],{"class":293},".indicators.get(layer.id())\n",[284,1732,1733,1735],{"class":123,"line":620},[284,1734,600],{"class":289},[284,1736,1689],{"class":293},[284,1738,1739,1742,1744],{"class":123,"line":653},[284,1740,1741],{"class":293},"            _node, indicator ",[284,1743,394],{"class":289},[284,1745,1699],{"class":293},[284,1747,1748,1750,1752,1754,1757,1759,1761,1763,1765],{"class":123,"line":658},[284,1749,531],{"class":357},[284,1751,625],{"class":293},[284,1753,633],{"class":289},[284,1755,1756],{"class":364},"\"Schema check: ",[284,1758,639],{"class":357},[284,1760,642],{"class":293},[284,1762,645],{"class":357},[284,1764,1322],{"class":364},[284,1766,1767],{"class":293},", indicator.toolTip())\n",[284,1769,1770],{"class":123,"line":668},[284,1771,333],{"emptyLinePlaceholder":332},[284,1773,1774,1776,1778],{"class":123,"line":679},[284,1775,376],{"class":289},[284,1777,695],{"class":347},[284,1779,426],{"class":293},[284,1781,1782,1784,1787,1789,1792,1794,1796],{"class":123,"line":685},[284,1783,516],{"class":289},[284,1785,1786],{"class":293}," layer_id ",[284,1788,522],{"class":289},[284,1790,1791],{"class":357}," list",[284,1793,878],{"class":293},[284,1795,455],{"class":357},[284,1797,1798],{"class":293},".indicators):\n",[284,1800,1801,1803,1805,1807],{"class":123,"line":690},[284,1802,1694],{"class":293},[284,1804,394],{"class":289},[284,1806,591],{"class":357},[284,1808,1809],{"class":293},".indicators.pop(layer_id)\n",[284,1811,1812,1814],{"class":123,"line":700},[284,1813,531],{"class":357},[284,1815,1706],{"class":293},[14,1817,1818,1820,1821,1824,1825,1828],{},[240,1819,732],{}," Indicators attach to layer tree nodes, not layers, so ",[36,1822,1823],{},"findLayer"," looks up the node for a layer id. Keeping a dictionary of the indicators the plugin added makes it possible to replace one when a check is re-run and to remove all of them on unload — indicators are not removed automatically. QGIS's built-in theme icons are available through ",[36,1826,1827],{},":\u002Fimages\u002Fthemes\u002Fdefault\u002F…"," resource paths, which gives indicators a native look without shipping icons. The click handler binds the layer as a default argument so each indicator remembers its own layer.",[14,1830,1831],{},"Use indicators sparingly. The Layers panel is where users manage everything else in the project, and a plugin that decorates every layer with an icon quickly turns the panel into noise. The good cases are states that need attention and that the user would otherwise have to go looking for: a failed validation, a layer that is out of date relative to its source, a layer that is locked for editing by another user. Pair each indicator with a tooltip that says what is wrong in plain language and a click action that leads to the fix, and clear it as soon as the state resolves.",[229,1833,1835],{"id":1834},"qgis-version-compatibility","QGIS version compatibility",[14,1837,1838,452,1841,1843,1844,1847,1848,1851,1852,1855,1856,1859,1860,1863],{},[36,1839,1840],{},"addCustomActionForLayerType",[36,1842,762],{}," and layer tree indicators have been available since QGIS 3.0 and 3.2. ",[36,1845,1846],{},"Qgis.MapLayerActionTarget"," and the ",[36,1849,1850],{},"triggeredFor…V2"," signals with a context argument arrived in 3.30; earlier releases use ",[36,1853,1854],{},"QgsMapLayerAction.SingleFeature"," and the signals without context. ",[36,1857,1858],{},"Qgis.LayerType.Vector"," replaced ",[36,1861,1862],{},"QgsMapLayer.VectorLayer"," in 3.30. The QGIS 4 series accepts only the scoped enums and the V2 signals.",[229,1865,1867],{"id":1866},"troubleshooting","Troubleshooting",[234,1869,1870,1878,1891,1897,1903],{},[237,1871,1872,1875,1876,545],{},[240,1873,1874],{},"Entries appear twice after reloading the plugin."," They were not removed in ",[36,1877,253],{},[237,1879,1880,1883,1884,1887,1888,1890],{},[240,1881,1882],{},"The action appears but acts on the wrong layer."," The handler used ",[36,1885,1886],{},"activeLayer()"," after the selection changed; read ",[36,1889,740],{}," at trigger time.",[237,1892,1893,1896],{},[240,1894,1895],{},"A feature action is missing from the attribute table."," Its targets do not include the kind of use, or the layer type does not match.",[237,1898,1899,1902],{},[240,1900,1901],{},"Indicators linger after the plugin is disabled."," They were not tracked and removed.",[237,1904,1905,1908,1909,545],{},[240,1906,1907],{},"Per-layer actions vanish when a project is reopened."," Layers are new objects; re-run the check on ",[36,1910,1085],{},[229,1912,1914],{"id":1913},"conclusion","Conclusion",[14,1916,1917,1918,1920,1921,1923,1924,545],{},"Add layer commands to the Layers panel context menu with ",[36,1919,1840],{},", grouped in a submenu and restricted with per-layer registration where they only apply to some layers. Register feature commands once with ",[36,1922,38],{}," so they appear in the attribute table, identify results and forms. Use indicators for status, and remove every action and indicator in ",[36,1925,253],{},[229,1927,1929],{"id":1928},"frequently-asked-questions","Frequently Asked Questions",[14,1931,1932,1935],{},[240,1933,1934],{},"Can I add actions to group nodes in the Layers panel?","\nThe custom layer actions target layers. For groups, connect to the layer tree view's context menu mechanism or provide the command elsewhere.",[14,1937,1938,1941,1942,1944,1945,1948,1949,545],{},[240,1939,1940],{},"Can a context menu action show a submenu of choices?","\nYes. Give the ",[36,1943,272],{}," a ",[36,1946,1947],{},"QMenu"," with its own actions via ",[36,1950,1951],{},"setMenu",[14,1953,1954,1957,1958,1961],{},[240,1955,1956],{},"Do these actions work for raster layers?","\nYes — pass ",[36,1959,1960],{},"Qgis.LayerType.Raster"," as the layer type.",[14,1963,1964,1967],{},[240,1965,1966],{},"How do I know which features were selected when a MultipleFeatures action runs?","\nThey are passed to the signal; you do not need to read the selection yourself.",[229,1969,1971],{"id":1970},"related","Related",[234,1973,1974,1979,1985,1990,1996],{},[237,1975,1976,1978],{},[30,1977,33],{"href":32}," — the guide this recipe belongs to",[237,1980,1981],{},[30,1982,1984],{"href":1983},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-toolbar-button-to-qgis-plugin\u002F","Add a Toolbar Button to a QGIS Plugin",[237,1986,1987],{},[30,1988,1989],{"href":1376},"Create Layer Actions in PyQGIS",[237,1991,1992],{},[30,1993,1995],{"href":1994},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Fadd-locator-filter-to-qgis-plugin\u002F","Add a Locator Filter to a QGIS Plugin",[237,1997,1998],{},[30,1999,2000],{"href":1092},"Connect to Layer and Project Signals in PyQGIS",[2002,2003,2004],"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":280,"searchDepth":303,"depth":303,"links":2006},[2007,2008,2009,2010,2011,2012,2013,2014,2015,2016],{"id":231,"depth":303,"text":232},{"id":262,"depth":303,"text":263},{"id":751,"depth":303,"text":752},{"id":1097,"depth":303,"text":1098},{"id":1451,"depth":303,"text":1452},{"id":1834,"depth":303,"text":1835},{"id":1866,"depth":303,"text":1867},{"id":1913,"depth":303,"text":1914},{"id":1928,"depth":303,"text":1929},{"id":1970,"depth":303,"text":1971},"Put plugin commands where users look for them — in the Layers panel right-click menu with addCustomActionForLayerType, on feature menus in the attribute table and identify results with QgsMapLayerAction, and as clickable layer tree indicators — and remove them cleanly on unload.","md",{"slug":2020,"type":2021,"breadcrumb":2022,"datePublished":2023,"dateModified":2023},"add-layer-context-menu-action-pyqgis","article","Layer Context Menu Action","2026-09-17","\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis",{"title":5,"description":2017},"qgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis\u002Findex","AwNulLuU3j9iG2PPUSbEF4L-yw08ohvC5JBJv9cwYLc",1789632907461]