[{"data":1,"prerenderedAt":2222},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-custom-layer-properties-page-pyqgis":3},{"id":4,"title":5,"body":6,"description":2211,"extension":2212,"meta":2213,"navigation":234,"path":2218,"seo":2219,"stem":2220,"__hash__":2221},"docs\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-custom-layer-properties-page-pyqgis\u002Findex.md","Add a Custom Layer Properties Page in PyQGIS",{"type":7,"value":8,"toc":2197},"minimark",[9,13,17,31,155,160,175,179,188,696,711,715,718,802,990,1007,1011,1018,1185,1199,1203,1206,1378,1414,1418,1421,1668,1678,1682,1685,1784,2012,2029,2033,2051,2055,2092,2096,2111,2115,2124,2136,2145,2160,2164,2193],[10,11,5],"h1",{"id":12},"add-a-custom-layer-properties-page-in-pyqgis",[14,15,16],"p",{},"Plugins often need per-layer settings: which field holds the asset id, how often the data should be refreshed, which validation rules apply, whether the layer takes part in a sync. A separate plugin dialog for those settings is easy to build and easy for users to miss. The natural home is the Layer Properties dialog itself — next to Symbology, Labels and Fields — where users already look for everything about a layer. QGIS lets plugins add pages there, and optionally to the Layer Styling panel, through a factory class.",[14,18,19,20,25,26,30],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002F","Extending QGIS with Custom Classes",". It builds a properties page with a ",[27,28,29],"code",{},"QgsMapLayerConfigWidget",", a factory that decides where it appears and for which layers, stores settings as layer custom properties so they travel with the project, and registers the factory from a plugin.",[14,32,33],{},[34,35,40,44,48,55,64,74,78,86,90,94,98,102,111,116,122,126,133,139,143,147,151],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 290","img","The Layer Properties dialog with built-in pages and a plugin page, whose widget stores settings as layer custom properties","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"A plugin page among the built-in pages",[45,46,47],"desc",{},"The Layer Properties dialog lists pages on the left: Information, Source, Symbology, Labels, Fields and others. A plugin's factory adds a page, here Asset Sync, with its own icon. The page's widget edits settings stored as custom properties on the layer, and its apply method writes them when the user presses OK or Apply.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","290","#f6f3ea",[56,57,63],"text",{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"380","28","text-anchor:middle;font-size:14px;font-family:sans-serif;font-weight:bold","#17211d","middle","Your page next to Symbology and Labels",[49,65],{"x":66,"y":67,"width":68,"height":69,"rx":70,"fill":71,"stroke":72,"style":73},"40","48","680","220","6","#fffdf7","#59645f","stroke-width:1.5",[49,75],{"x":66,"y":67,"width":76,"height":69,"rx":70,"fill":77,"stroke":72,"style":73},"170","#f0ebdd",[56,79,85],{"x":80,"y":81,"style":82,"fill":83,"textAnchor":84},"60","82","text-anchor:start;font-size:10.5px;font-family:sans-serif","#2f3b35","start","Information",[56,87,89],{"x":80,"y":88,"style":82,"fill":83,"textAnchor":84},"110","Source",[56,91,93],{"x":80,"y":92,"style":82,"fill":83,"textAnchor":84},"138","Symbology",[56,95,97],{"x":80,"y":96,"style":82,"fill":83,"textAnchor":84},"166","Labels",[56,99,101],{"x":80,"y":100,"style":82,"fill":83,"textAnchor":84},"194","Fields",[49,103],{"x":104,"y":105,"width":106,"height":107,"rx":108,"fill":109,"stroke":110,"style":73},"50","208","150","26","4","#eff3ff","#2563eb",[56,112,115],{"x":80,"y":113,"style":114,"fill":110,"textAnchor":84},"226","text-anchor:start;font-size:10.5px;font-family:sans-serif;font-weight:bold","Asset Sync",[56,117,121],{"x":118,"y":119,"style":120,"fill":61,"textAnchor":84},"240","86","text-anchor:start;font-size:12.0px;font-family:sans-serif;font-weight:bold","Asset Sync settings",[56,123,125],{"x":118,"y":124,"style":82,"fill":83,"textAnchor":84},"124","id field:",[49,127],{"x":128,"y":88,"width":129,"height":130,"rx":131,"fill":132,"stroke":72,"style":73},"340","200","22","3","#fbf8f0",[56,134,138],{"x":135,"y":136,"style":137,"fill":83,"textAnchor":84},"350","125","text-anchor:start;font-size:10.0px;font-family:monospace","asset_id",[56,140,142],{"x":118,"y":141,"style":82,"fill":83,"textAnchor":84},"162","sync every:",[49,144],{"x":128,"y":145,"width":146,"height":130,"rx":131,"fill":132,"stroke":72,"style":73},"148","120",[56,148,150],{"x":135,"y":149,"style":137,"fill":83,"textAnchor":84},"163","24 h",[56,152,154],{"x":118,"y":69,"style":153,"fill":72,"textAnchor":84},"text-anchor:start;font-size:10.0px;font-family:sans-serif","stored as layer custom properties → saved in the project",[156,157,159],"h2",{"id":158},"prerequisites","Prerequisites",[161,162,163,167],"ul",{},[164,165,166],"li",{},"QGIS 3.34 LTR or newer, or the QGIS 4 series, with the code in a plugin (the page must be registered each session).",[164,168,169,170,174],{},"Basic Qt widget knowledge. Layouts can be built in code as below or in Qt Designer, as in ",[21,171,173],{"href":172},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fload-ui-file-at-runtime-pyqgis\u002F","loading a .ui file at runtime",".",[156,176,178],{"id":177},"build-the-page-widget","Build the page widget",[14,180,181,182,184,185,174],{},"The page is a ",[27,183,29],{},". It receives the layer when created, shows the current settings, and writes them back in ",[27,186,187],{},"apply()",[189,190,195],"pre",{"className":191,"code":192,"language":193,"meta":194,"style":194},"language-python shiki shiki-themes github-dark","from qgis.gui import QgsMapLayerConfigWidget, QgsFieldComboBox\nfrom qgis.PyQt.QtWidgets import QFormLayout, QSpinBox, QCheckBox\n\nPREFIX = \"assetsync\u002F\"\n\nclass AssetSyncPage(QgsMapLayerConfigWidget):\n    def __init__(self, layer, canvas, parent=None):\n        super().__init__(layer, canvas, parent)\n        self.layer = layer\n        self.enabled = QCheckBox(\"Include this layer in asset sync\")\n        self.id_field = QgsFieldComboBox()\n        self.id_field.setLayer(layer)\n        self.interval = QSpinBox()\n        self.interval.setRange(1, 720)\n        self.interval.setSuffix(\" h\")\n\n        form = QFormLayout(self)\n        form.addRow(self.enabled)\n        form.addRow(\"Asset id field\", self.id_field)\n        form.addRow(\"Sync every\", self.interval)\n\n        self.enabled.setChecked(layer.customProperty(PREFIX + \"enabled\", False) in (True, \"true\"))\n        self.id_field.setField(layer.customProperty(PREFIX + \"id_field\", \"\"))\n        self.interval.setValue(int(layer.customProperty(PREFIX + \"interval_h\", 24)))\n\n        for w in (self.enabled.toggled, self.id_field.fieldChanged, self.interval.valueChanged):\n            w.connect(lambda *_: self.widgetChanged.emit())\n\n    def apply(self):\n        self.layer.setCustomProperty(PREFIX + \"enabled\", self.enabled.isChecked())\n        self.layer.setCustomProperty(PREFIX + \"id_field\", self.id_field.currentField())\n        self.layer.setCustomProperty(PREFIX + \"interval_h\", self.interval.value())\n","python","",[27,196,197,216,229,236,250,255,273,293,308,322,341,354,362,375,394,407,412,428,439,454,469,474,515,537,566,571,599,619,624,635,656,676],{"__ignoreMap":194},[198,199,202,206,210,213],"span",{"class":200,"line":201},"line",1,[198,203,205],{"class":204},"snl16","from",[198,207,209],{"class":208},"s95oV"," qgis.gui ",[198,211,212],{"class":204},"import",[198,214,215],{"class":208}," QgsMapLayerConfigWidget, QgsFieldComboBox\n",[198,217,219,221,224,226],{"class":200,"line":218},2,[198,220,205],{"class":204},[198,222,223],{"class":208}," qgis.PyQt.QtWidgets ",[198,225,212],{"class":204},[198,227,228],{"class":208}," QFormLayout, QSpinBox, QCheckBox\n",[198,230,232],{"class":200,"line":231},3,[198,233,235],{"emptyLinePlaceholder":234},true,"\n",[198,237,239,243,246],{"class":200,"line":238},4,[198,240,242],{"class":241},"sDLfK","PREFIX",[198,244,245],{"class":204}," =",[198,247,249],{"class":248},"sU2Wk"," \"assetsync\u002F\"\n",[198,251,253],{"class":200,"line":252},5,[198,254,235],{"emptyLinePlaceholder":234},[198,256,258,261,265,268,270],{"class":200,"line":257},6,[198,259,260],{"class":204},"class",[198,262,264],{"class":263},"svObZ"," AssetSyncPage",[198,266,267],{"class":208},"(",[198,269,29],{"class":263},[198,271,272],{"class":208},"):\n",[198,274,276,279,282,285,288,291],{"class":200,"line":275},7,[198,277,278],{"class":204},"    def",[198,280,281],{"class":241}," __init__",[198,283,284],{"class":208},"(self, layer, canvas, parent",[198,286,287],{"class":204},"=",[198,289,290],{"class":241},"None",[198,292,272],{"class":208},[198,294,296,299,302,305],{"class":200,"line":295},8,[198,297,298],{"class":241},"        super",[198,300,301],{"class":208},"().",[198,303,304],{"class":241},"__init__",[198,306,307],{"class":208},"(layer, canvas, parent)\n",[198,309,311,314,317,319],{"class":200,"line":310},9,[198,312,313],{"class":241},"        self",[198,315,316],{"class":208},".layer ",[198,318,287],{"class":204},[198,320,321],{"class":208}," layer\n",[198,323,325,327,330,332,335,338],{"class":200,"line":324},10,[198,326,313],{"class":241},[198,328,329],{"class":208},".enabled ",[198,331,287],{"class":204},[198,333,334],{"class":208}," QCheckBox(",[198,336,337],{"class":248},"\"Include this layer in asset sync\"",[198,339,340],{"class":208},")\n",[198,342,344,346,349,351],{"class":200,"line":343},11,[198,345,313],{"class":241},[198,347,348],{"class":208},".id_field ",[198,350,287],{"class":204},[198,352,353],{"class":208}," QgsFieldComboBox()\n",[198,355,357,359],{"class":200,"line":356},12,[198,358,313],{"class":241},[198,360,361],{"class":208},".id_field.setLayer(layer)\n",[198,363,365,367,370,372],{"class":200,"line":364},13,[198,366,313],{"class":241},[198,368,369],{"class":208},".interval ",[198,371,287],{"class":204},[198,373,374],{"class":208}," QSpinBox()\n",[198,376,378,380,383,386,389,392],{"class":200,"line":377},14,[198,379,313],{"class":241},[198,381,382],{"class":208},".interval.setRange(",[198,384,385],{"class":241},"1",[198,387,388],{"class":208},", ",[198,390,391],{"class":241},"720",[198,393,340],{"class":208},[198,395,397,399,402,405],{"class":200,"line":396},15,[198,398,313],{"class":241},[198,400,401],{"class":208},".interval.setSuffix(",[198,403,404],{"class":248},"\" h\"",[198,406,340],{"class":208},[198,408,410],{"class":200,"line":409},16,[198,411,235],{"emptyLinePlaceholder":234},[198,413,415,418,420,423,426],{"class":200,"line":414},17,[198,416,417],{"class":208},"        form ",[198,419,287],{"class":204},[198,421,422],{"class":208}," QFormLayout(",[198,424,425],{"class":241},"self",[198,427,340],{"class":208},[198,429,431,434,436],{"class":200,"line":430},18,[198,432,433],{"class":208},"        form.addRow(",[198,435,425],{"class":241},[198,437,438],{"class":208},".enabled)\n",[198,440,442,444,447,449,451],{"class":200,"line":441},19,[198,443,433],{"class":208},[198,445,446],{"class":248},"\"Asset id field\"",[198,448,388],{"class":208},[198,450,425],{"class":241},[198,452,453],{"class":208},".id_field)\n",[198,455,457,459,462,464,466],{"class":200,"line":456},20,[198,458,433],{"class":208},[198,460,461],{"class":248},"\"Sync every\"",[198,463,388],{"class":208},[198,465,425],{"class":241},[198,467,468],{"class":208},".interval)\n",[198,470,472],{"class":200,"line":471},21,[198,473,235],{"emptyLinePlaceholder":234},[198,475,477,479,482,484,487,490,492,495,498,501,504,507,509,512],{"class":200,"line":476},22,[198,478,313],{"class":241},[198,480,481],{"class":208},".enabled.setChecked(layer.customProperty(",[198,483,242],{"class":241},[198,485,486],{"class":204}," +",[198,488,489],{"class":248}," \"enabled\"",[198,491,388],{"class":208},[198,493,494],{"class":241},"False",[198,496,497],{"class":208},") ",[198,499,500],{"class":204},"in",[198,502,503],{"class":208}," (",[198,505,506],{"class":241},"True",[198,508,388],{"class":208},[198,510,511],{"class":248},"\"true\"",[198,513,514],{"class":208},"))\n",[198,516,518,520,523,525,527,530,532,535],{"class":200,"line":517},23,[198,519,313],{"class":241},[198,521,522],{"class":208},".id_field.setField(layer.customProperty(",[198,524,242],{"class":241},[198,526,486],{"class":204},[198,528,529],{"class":248}," \"id_field\"",[198,531,388],{"class":208},[198,533,534],{"class":248},"\"\"",[198,536,514],{"class":208},[198,538,540,542,545,548,551,553,555,558,560,563],{"class":200,"line":539},24,[198,541,313],{"class":241},[198,543,544],{"class":208},".interval.setValue(",[198,546,547],{"class":241},"int",[198,549,550],{"class":208},"(layer.customProperty(",[198,552,242],{"class":241},[198,554,486],{"class":204},[198,556,557],{"class":248}," \"interval_h\"",[198,559,388],{"class":208},[198,561,562],{"class":241},"24",[198,564,565],{"class":208},")))\n",[198,567,569],{"class":200,"line":568},25,[198,570,235],{"emptyLinePlaceholder":234},[198,572,574,577,580,582,584,586,589,591,594,596],{"class":200,"line":573},26,[198,575,576],{"class":204},"        for",[198,578,579],{"class":208}," w ",[198,581,500],{"class":204},[198,583,503],{"class":208},[198,585,425],{"class":241},[198,587,588],{"class":208},".enabled.toggled, ",[198,590,425],{"class":241},[198,592,593],{"class":208},".id_field.fieldChanged, ",[198,595,425],{"class":241},[198,597,598],{"class":208},".interval.valueChanged):\n",[198,600,602,605,608,611,614,616],{"class":200,"line":601},27,[198,603,604],{"class":208},"            w.connect(",[198,606,607],{"class":204},"lambda",[198,609,610],{"class":204}," *",[198,612,613],{"class":208},"_: ",[198,615,425],{"class":241},[198,617,618],{"class":208},".widgetChanged.emit())\n",[198,620,622],{"class":200,"line":621},28,[198,623,235],{"emptyLinePlaceholder":234},[198,625,627,629,632],{"class":200,"line":626},29,[198,628,278],{"class":204},[198,630,631],{"class":263}," apply",[198,633,634],{"class":208},"(self):\n",[198,636,638,640,643,645,647,649,651,653],{"class":200,"line":637},30,[198,639,313],{"class":241},[198,641,642],{"class":208},".layer.setCustomProperty(",[198,644,242],{"class":241},[198,646,486],{"class":204},[198,648,489],{"class":248},[198,650,388],{"class":208},[198,652,425],{"class":241},[198,654,655],{"class":208},".enabled.isChecked())\n",[198,657,659,661,663,665,667,669,671,673],{"class":200,"line":658},31,[198,660,313],{"class":241},[198,662,642],{"class":208},[198,664,242],{"class":241},[198,666,486],{"class":204},[198,668,529],{"class":248},[198,670,388],{"class":208},[198,672,425],{"class":241},[198,674,675],{"class":208},".id_field.currentField())\n",[198,677,679,681,683,685,687,689,691,693],{"class":200,"line":678},32,[198,680,313],{"class":241},[198,682,642],{"class":208},[198,684,242],{"class":241},[198,686,486],{"class":204},[198,688,557],{"class":248},[198,690,388],{"class":208},[198,692,425],{"class":241},[198,694,695],{"class":208},".interval.value())\n",[14,697,698,702,703,706,707,710],{},[699,700,701],"strong",{},"Breakdown:"," Custom properties are key–value pairs stored on the layer and written into the project file, so settings travel with the project and need no separate storage. A plugin-specific prefix keeps keys from colliding with other plugins. ",[27,704,705],{},"QgsFieldComboBox"," lists the layer's fields and updates if they change. Emitting ",[27,708,709],{},"widgetChanged"," when any input changes lets the dialog enable its Apply button and, in the styling panel, apply live. Booleans may come back as strings after a project round trip, hence the tolerant check.",[156,712,714],{"id":713},"write-the-factory","Write the factory",[14,716,717],{},"The factory tells QGIS what the page is called, which icon to show, which layers it applies to, and where it may appear.",[14,719,720],{},[34,721,724,727,730,733,736,744,750,755,759,762,765,768,771,776,780,783,786,791,796,799],{"viewBox":722,"role":37,"ariaLabel":723,"xmlns":39},"0 0 760 222","A config widget factory deciding title and icon, which layers it supports, where it appears, and creating the page widget",[41,725,726],{},"What the factory decides",[45,728,729],{},"The factory supplies the page title and icon, answers supportsLayer for each layer so the page appears only where it makes sense, declares whether it appears in the Layer Properties dialog, the Layer Styling panel or both, and creates the page widget on demand with createWidget.",[49,731],{"x":51,"y":51,"width":52,"height":732,"fill":54},"222",[56,734,735],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"One factory, many pages",[49,737],{"x":562,"y":738,"width":739,"height":106,"rx":740,"fill":741,"stroke":742,"style":743},"56","168","8","#eef7f4","#0f766e","stroke-width:2",[56,745,749],{"x":746,"y":747,"style":748,"fill":742,"textAnchor":62},"108","108.78","text-anchor:middle;font-size:11.5px;font-family:sans-serif;font-weight:bold","title · icon",[56,751,754],{"x":746,"y":752,"style":753,"fill":72,"textAnchor":62},"134.78","text-anchor:middle;font-size:10.5px;font-family:sans-serif","shown in",[56,756,758],{"x":746,"y":757,"style":753,"fill":72,"textAnchor":62},"160.78","page list",[49,760],{"x":761,"y":738,"width":739,"height":106,"rx":740,"fill":109,"stroke":110,"style":743},"206",[56,763,764],{"x":53,"y":747,"style":748,"fill":110,"textAnchor":62},"supportsLayer()",[56,766,767],{"x":53,"y":752,"style":753,"fill":72,"textAnchor":62},"vector, with",[56,769,770],{"x":53,"y":757,"style":753,"fill":72,"textAnchor":62},"an id field",[49,772],{"x":773,"y":738,"width":739,"height":106,"rx":740,"fill":774,"stroke":775,"style":743},"388","#fdf2e2","#b45309",[56,777,779],{"x":778,"y":747,"style":748,"fill":775,"textAnchor":62},"472","where",[56,781,782],{"x":778,"y":752,"style":753,"fill":72,"textAnchor":62},"properties dialog",[56,784,785],{"x":778,"y":757,"style":753,"fill":72,"textAnchor":62},"styling panel",[49,787],{"x":788,"y":738,"width":96,"height":106,"rx":740,"fill":789,"stroke":790,"style":743},"570","#e8efe6","#15803d",[56,792,795],{"x":793,"y":747,"style":748,"fill":794,"textAnchor":62},"653","#166534","createWidget()",[56,797,798],{"x":793,"y":752,"style":753,"fill":72,"textAnchor":62},"new page",[56,800,801],{"x":793,"y":757,"style":753,"fill":72,"textAnchor":62},"per layer",[189,803,805],{"className":191,"code":804,"language":193,"meta":194,"style":194},"from qgis.gui import QgsMapLayerConfigWidgetFactory\nfrom qgis.core import QgsVectorLayer\nfrom qgis.PyQt.QtGui import QIcon\n\nclass AssetSyncFactory(QgsMapLayerConfigWidgetFactory):\n    def __init__(self, icon_path):\n        super().__init__(\"Asset Sync\", QIcon(icon_path))\n\n    def supportsLayer(self, layer):\n        return isinstance(layer, QgsVectorLayer) and layer.isSpatial()\n\n    def supportLayerPropertiesDialog(self):\n        return True\n\n    def supportsStyleDock(self):\n        return False\n\n    def createWidget(self, layer, canvas, dockWidget=True, parent=None):\n        return AssetSyncPage(layer, canvas, parent)\n",[27,806,807,818,830,842,846,860,869,885,889,899,916,920,929,936,940,949,956,960,983],{"__ignoreMap":194},[198,808,809,811,813,815],{"class":200,"line":201},[198,810,205],{"class":204},[198,812,209],{"class":208},[198,814,212],{"class":204},[198,816,817],{"class":208}," QgsMapLayerConfigWidgetFactory\n",[198,819,820,822,825,827],{"class":200,"line":218},[198,821,205],{"class":204},[198,823,824],{"class":208}," qgis.core ",[198,826,212],{"class":204},[198,828,829],{"class":208}," QgsVectorLayer\n",[198,831,832,834,837,839],{"class":200,"line":231},[198,833,205],{"class":204},[198,835,836],{"class":208}," qgis.PyQt.QtGui ",[198,838,212],{"class":204},[198,840,841],{"class":208}," QIcon\n",[198,843,844],{"class":200,"line":238},[198,845,235],{"emptyLinePlaceholder":234},[198,847,848,850,853,855,858],{"class":200,"line":252},[198,849,260],{"class":204},[198,851,852],{"class":263}," AssetSyncFactory",[198,854,267],{"class":208},[198,856,857],{"class":263},"QgsMapLayerConfigWidgetFactory",[198,859,272],{"class":208},[198,861,862,864,866],{"class":200,"line":257},[198,863,278],{"class":204},[198,865,281],{"class":241},[198,867,868],{"class":208},"(self, icon_path):\n",[198,870,871,873,875,877,879,882],{"class":200,"line":275},[198,872,298],{"class":241},[198,874,301],{"class":208},[198,876,304],{"class":241},[198,878,267],{"class":208},[198,880,881],{"class":248},"\"Asset Sync\"",[198,883,884],{"class":208},", QIcon(icon_path))\n",[198,886,887],{"class":200,"line":295},[198,888,235],{"emptyLinePlaceholder":234},[198,890,891,893,896],{"class":200,"line":310},[198,892,278],{"class":204},[198,894,895],{"class":263}," supportsLayer",[198,897,898],{"class":208},"(self, layer):\n",[198,900,901,904,907,910,913],{"class":200,"line":324},[198,902,903],{"class":204},"        return",[198,905,906],{"class":241}," isinstance",[198,908,909],{"class":208},"(layer, QgsVectorLayer) ",[198,911,912],{"class":204},"and",[198,914,915],{"class":208}," layer.isSpatial()\n",[198,917,918],{"class":200,"line":343},[198,919,235],{"emptyLinePlaceholder":234},[198,921,922,924,927],{"class":200,"line":356},[198,923,278],{"class":204},[198,925,926],{"class":263}," supportLayerPropertiesDialog",[198,928,634],{"class":208},[198,930,931,933],{"class":200,"line":364},[198,932,903],{"class":204},[198,934,935],{"class":241}," True\n",[198,937,938],{"class":200,"line":377},[198,939,235],{"emptyLinePlaceholder":234},[198,941,942,944,947],{"class":200,"line":396},[198,943,278],{"class":204},[198,945,946],{"class":263}," supportsStyleDock",[198,948,634],{"class":208},[198,950,951,953],{"class":200,"line":409},[198,952,903],{"class":204},[198,954,955],{"class":241}," False\n",[198,957,958],{"class":200,"line":414},[198,959,235],{"emptyLinePlaceholder":234},[198,961,962,964,967,970,972,974,977,979,981],{"class":200,"line":430},[198,963,278],{"class":204},[198,965,966],{"class":263}," createWidget",[198,968,969],{"class":208},"(self, layer, canvas, dockWidget",[198,971,287],{"class":204},[198,973,506],{"class":241},[198,975,976],{"class":208},", parent",[198,978,287],{"class":204},[198,980,290],{"class":241},[198,982,272],{"class":208},[198,984,985,987],{"class":200,"line":441},[198,986,903],{"class":204},[198,988,989],{"class":208}," AssetSyncPage(layer, canvas, parent)\n",[14,991,992,994,995,998,999,1002,1003,1006],{},[699,993,701],{}," The constructor sets the title and icon shown in the page list. ",[27,996,997],{},"supportsLayer"," keeps the page off raster layers and attribute-only tables, where asset sync makes no sense; the narrower the rule, the less clutter for users. Settings pages belong in the Layer Properties dialog, so ",[27,1000,1001],{},"supportsStyleDock"," returns False; pages that change appearance — a custom renderer's options, for instance — are better in the styling panel, where changes preview live. ",[27,1004,1005],{},"createWidget"," is called each time the dialog opens, so the page always shows the layer's current settings.",[156,1008,1010],{"id":1009},"register-the-factory-from-the-plugin","Register the factory from the plugin",[14,1012,1013,1014,1017],{},"Registration and unregistration go through ",[27,1015,1016],{},"iface",". Keep the factory on the plugin so it is not garbage-collected.",[189,1019,1021],{"className":191,"code":1020,"language":193,"meta":194,"style":194},"import os\n\nclass AssetSyncPlugin:\n    def __init__(self, iface):\n        self.iface = iface\n        self.factory = None\n\n    def initGui(self):\n        icon = os.path.join(os.path.dirname(__file__), \"icons\", \"sync.svg\")\n        self.factory = AssetSyncFactory(icon)\n        self.iface.registerMapLayerConfigWidgetFactory(self.factory)\n\n    def unload(self):\n        if self.factory:\n            self.iface.unregisterMapLayerConfigWidgetFactory(self.factory)\n            self.factory = None\n",[27,1022,1023,1030,1034,1044,1053,1065,1077,1081,1090,1116,1127,1139,1143,1152,1163,1175],{"__ignoreMap":194},[198,1024,1025,1027],{"class":200,"line":201},[198,1026,212],{"class":204},[198,1028,1029],{"class":208}," os\n",[198,1031,1032],{"class":200,"line":218},[198,1033,235],{"emptyLinePlaceholder":234},[198,1035,1036,1038,1041],{"class":200,"line":231},[198,1037,260],{"class":204},[198,1039,1040],{"class":263}," AssetSyncPlugin",[198,1042,1043],{"class":208},":\n",[198,1045,1046,1048,1050],{"class":200,"line":238},[198,1047,278],{"class":204},[198,1049,281],{"class":241},[198,1051,1052],{"class":208},"(self, iface):\n",[198,1054,1055,1057,1060,1062],{"class":200,"line":252},[198,1056,313],{"class":241},[198,1058,1059],{"class":208},".iface ",[198,1061,287],{"class":204},[198,1063,1064],{"class":208}," iface\n",[198,1066,1067,1069,1072,1074],{"class":200,"line":257},[198,1068,313],{"class":241},[198,1070,1071],{"class":208},".factory ",[198,1073,287],{"class":204},[198,1075,1076],{"class":241}," None\n",[198,1078,1079],{"class":200,"line":275},[198,1080,235],{"emptyLinePlaceholder":234},[198,1082,1083,1085,1088],{"class":200,"line":295},[198,1084,278],{"class":204},[198,1086,1087],{"class":263}," initGui",[198,1089,634],{"class":208},[198,1091,1092,1095,1097,1100,1103,1106,1109,1111,1114],{"class":200,"line":310},[198,1093,1094],{"class":208},"        icon ",[198,1096,287],{"class":204},[198,1098,1099],{"class":208}," os.path.join(os.path.dirname(",[198,1101,1102],{"class":241},"__file__",[198,1104,1105],{"class":208},"), ",[198,1107,1108],{"class":248},"\"icons\"",[198,1110,388],{"class":208},[198,1112,1113],{"class":248},"\"sync.svg\"",[198,1115,340],{"class":208},[198,1117,1118,1120,1122,1124],{"class":200,"line":324},[198,1119,313],{"class":241},[198,1121,1071],{"class":208},[198,1123,287],{"class":204},[198,1125,1126],{"class":208}," AssetSyncFactory(icon)\n",[198,1128,1129,1131,1134,1136],{"class":200,"line":343},[198,1130,313],{"class":241},[198,1132,1133],{"class":208},".iface.registerMapLayerConfigWidgetFactory(",[198,1135,425],{"class":241},[198,1137,1138],{"class":208},".factory)\n",[198,1140,1141],{"class":200,"line":356},[198,1142,235],{"emptyLinePlaceholder":234},[198,1144,1145,1147,1150],{"class":200,"line":364},[198,1146,278],{"class":204},[198,1148,1149],{"class":263}," unload",[198,1151,634],{"class":208},[198,1153,1154,1157,1160],{"class":200,"line":377},[198,1155,1156],{"class":204},"        if",[198,1158,1159],{"class":241}," self",[198,1161,1162],{"class":208},".factory:\n",[198,1164,1165,1168,1171,1173],{"class":200,"line":396},[198,1166,1167],{"class":241},"            self",[198,1169,1170],{"class":208},".iface.unregisterMapLayerConfigWidgetFactory(",[198,1172,425],{"class":241},[198,1174,1138],{"class":208},[198,1176,1177,1179,1181,1183],{"class":200,"line":409},[198,1178,1167],{"class":241},[198,1180,1071],{"class":208},[198,1182,287],{"class":204},[198,1184,1076],{"class":241},[14,1186,1187,1189,1190,1193,1194,1198],{},[699,1188,701],{}," After registration, opening Layer Properties on any supported layer shows the new page. Unregistering in ",[27,1191,1192],{},"unload"," removes it, so reloading the plugin during development does not leave duplicate pages — see ",[21,1195,1197],{"href":1196},"\u002Fqgis-plugin-development\u002Fplugin-boilerplate-structure\u002Freload-qgis-plugin-without-restart\u002F","reloading a plugin without restarting",". Dialogs already open keep their pages until closed.",[156,1200,1202],{"id":1201},"offer-a-live-page-in-the-styling-panel","Offer a live page in the styling panel",[14,1204,1205],{},"Settings that change how a layer looks — the parameters of a custom renderer, a highlight colour for synchronised features — are better edited in the Layer Styling panel, where users see the effect immediately. The same widget class can serve both places; the factory says so, and the page applies changes as they are made.",[189,1207,1209],{"className":191,"code":1208,"language":193,"meta":194,"style":194},"class SyncHighlightFactory(QgsMapLayerConfigWidgetFactory):\n    def __init__(self, icon_path):\n        super().__init__(\"Sync highlight\", QIcon(icon_path))\n\n    def supportsLayer(self, layer):\n        return isinstance(layer, QgsVectorLayer) and layer.customProperty(PREFIX + \"enabled\", False) in (True, \"true\")\n\n    def supportLayerPropertiesDialog(self):\n        return True\n\n    def supportsStyleDock(self):\n        return True\n\n    def createWidget(self, layer, canvas, dockWidget=True, parent=None):\n        page = HighlightPage(layer, canvas, parent)\n        page.setDockMode(dockWidget)\n        return page\n",[27,1210,1211,1224,1232,1247,1251,1259,1296,1300,1308,1314,1318,1326,1332,1336,1356,1366,1371],{"__ignoreMap":194},[198,1212,1213,1215,1218,1220,1222],{"class":200,"line":201},[198,1214,260],{"class":204},[198,1216,1217],{"class":263}," SyncHighlightFactory",[198,1219,267],{"class":208},[198,1221,857],{"class":263},[198,1223,272],{"class":208},[198,1225,1226,1228,1230],{"class":200,"line":218},[198,1227,278],{"class":204},[198,1229,281],{"class":241},[198,1231,868],{"class":208},[198,1233,1234,1236,1238,1240,1242,1245],{"class":200,"line":231},[198,1235,298],{"class":241},[198,1237,301],{"class":208},[198,1239,304],{"class":241},[198,1241,267],{"class":208},[198,1243,1244],{"class":248},"\"Sync highlight\"",[198,1246,884],{"class":208},[198,1248,1249],{"class":200,"line":238},[198,1250,235],{"emptyLinePlaceholder":234},[198,1252,1253,1255,1257],{"class":200,"line":252},[198,1254,278],{"class":204},[198,1256,895],{"class":263},[198,1258,898],{"class":208},[198,1260,1261,1263,1265,1267,1269,1272,1274,1276,1278,1280,1282,1284,1286,1288,1290,1292,1294],{"class":200,"line":257},[198,1262,903],{"class":204},[198,1264,906],{"class":241},[198,1266,909],{"class":208},[198,1268,912],{"class":204},[198,1270,1271],{"class":208}," layer.customProperty(",[198,1273,242],{"class":241},[198,1275,486],{"class":204},[198,1277,489],{"class":248},[198,1279,388],{"class":208},[198,1281,494],{"class":241},[198,1283,497],{"class":208},[198,1285,500],{"class":204},[198,1287,503],{"class":208},[198,1289,506],{"class":241},[198,1291,388],{"class":208},[198,1293,511],{"class":248},[198,1295,340],{"class":208},[198,1297,1298],{"class":200,"line":275},[198,1299,235],{"emptyLinePlaceholder":234},[198,1301,1302,1304,1306],{"class":200,"line":295},[198,1303,278],{"class":204},[198,1305,926],{"class":263},[198,1307,634],{"class":208},[198,1309,1310,1312],{"class":200,"line":310},[198,1311,903],{"class":204},[198,1313,935],{"class":241},[198,1315,1316],{"class":200,"line":324},[198,1317,235],{"emptyLinePlaceholder":234},[198,1319,1320,1322,1324],{"class":200,"line":343},[198,1321,278],{"class":204},[198,1323,946],{"class":263},[198,1325,634],{"class":208},[198,1327,1328,1330],{"class":200,"line":356},[198,1329,903],{"class":204},[198,1331,935],{"class":241},[198,1333,1334],{"class":200,"line":364},[198,1335,235],{"emptyLinePlaceholder":234},[198,1337,1338,1340,1342,1344,1346,1348,1350,1352,1354],{"class":200,"line":377},[198,1339,278],{"class":204},[198,1341,966],{"class":263},[198,1343,969],{"class":208},[198,1345,287],{"class":204},[198,1347,506],{"class":241},[198,1349,976],{"class":208},[198,1351,287],{"class":204},[198,1353,290],{"class":241},[198,1355,272],{"class":208},[198,1357,1358,1361,1363],{"class":200,"line":396},[198,1359,1360],{"class":208},"        page ",[198,1362,287],{"class":204},[198,1364,1365],{"class":208}," HighlightPage(layer, canvas, parent)\n",[198,1367,1368],{"class":200,"line":409},[198,1369,1370],{"class":208},"        page.setDockMode(dockWidget)\n",[198,1372,1373,1375],{"class":200,"line":414},[198,1374,903],{"class":204},[198,1376,1377],{"class":208}," page\n",[14,1379,1380,1382,1383,1385,1386,1388,1389,1392,1393,1396,1397,1399,1400,1402,1403,1406,1407,1410,1411,1413],{},[699,1381,701],{}," ",[27,1384,1001],{}," returning True adds the page to the styling panel's list for supported layers, and ",[27,1387,1005],{}," is told through ",[27,1390,1391],{},"dockWidget"," which host it is being created for. Calling ",[27,1394,1395],{},"setDockMode"," lets the page adapt — a compact layout in the narrow panel, a fuller one in the dialog. In the panel, QGIS calls ",[27,1398,187],{}," automatically after ",[27,1401,709],{}," when live update is on, so the page's ",[27,1404,1405],{},"apply"," should be cheap and should end with ",[27,1408,1409],{},"layer.triggerRepaint()"," for visual settings. Restricting ",[27,1412,997],{}," to layers that have sync enabled means the page appears only where it is relevant, which keeps the panel's page list short.",[156,1415,1417],{"id":1416},"read-the-settings-where-they-are-used","Read the settings where they are used",[14,1419,1420],{},"The page only stores settings; the rest of the plugin reads them when it acts. A small helper keeps key names and defaults in one place.",[189,1422,1424],{"className":191,"code":1423,"language":193,"meta":194,"style":194},"from qgis.core import QgsProject\n\ndef sync_settings(layer):\n    return {\n        \"enabled\": layer.customProperty(PREFIX + \"enabled\", False) in (True, \"true\"),\n        \"id_field\": layer.customProperty(PREFIX + \"id_field\", \"\") or None,\n        \"interval_h\": int(layer.customProperty(PREFIX + \"interval_h\", 24)),\n    }\n\nfor layer in QgsProject.instance().mapLayers().values():\n    if isinstance(layer, QgsVectorLayer):\n        cfg = sync_settings(layer)\n        if cfg[\"enabled\"] and cfg[\"id_field\"]:\n            print(f\"{layer.name()}: sync on {cfg['id_field']} every {cfg['interval_h']} h\")\n",[27,1425,1426,1437,1441,1452,1460,1493,1521,1546,1551,1555,1568,1578,1588,1611],{"__ignoreMap":194},[198,1427,1428,1430,1432,1434],{"class":200,"line":201},[198,1429,205],{"class":204},[198,1431,824],{"class":208},[198,1433,212],{"class":204},[198,1435,1436],{"class":208}," QgsProject\n",[198,1438,1439],{"class":200,"line":218},[198,1440,235],{"emptyLinePlaceholder":234},[198,1442,1443,1446,1449],{"class":200,"line":231},[198,1444,1445],{"class":204},"def",[198,1447,1448],{"class":263}," sync_settings",[198,1450,1451],{"class":208},"(layer):\n",[198,1453,1454,1457],{"class":200,"line":238},[198,1455,1456],{"class":204},"    return",[198,1458,1459],{"class":208}," {\n",[198,1461,1462,1465,1468,1470,1472,1474,1476,1478,1480,1482,1484,1486,1488,1490],{"class":200,"line":252},[198,1463,1464],{"class":248},"        \"enabled\"",[198,1466,1467],{"class":208},": layer.customProperty(",[198,1469,242],{"class":241},[198,1471,486],{"class":204},[198,1473,489],{"class":248},[198,1475,388],{"class":208},[198,1477,494],{"class":241},[198,1479,497],{"class":208},[198,1481,500],{"class":204},[198,1483,503],{"class":208},[198,1485,506],{"class":241},[198,1487,388],{"class":208},[198,1489,511],{"class":248},[198,1491,1492],{"class":208},"),\n",[198,1494,1495,1498,1500,1502,1504,1506,1508,1510,1512,1515,1518],{"class":200,"line":257},[198,1496,1497],{"class":248},"        \"id_field\"",[198,1499,1467],{"class":208},[198,1501,242],{"class":241},[198,1503,486],{"class":204},[198,1505,529],{"class":248},[198,1507,388],{"class":208},[198,1509,534],{"class":248},[198,1511,497],{"class":208},[198,1513,1514],{"class":204},"or",[198,1516,1517],{"class":241}," None",[198,1519,1520],{"class":208},",\n",[198,1522,1523,1526,1529,1531,1533,1535,1537,1539,1541,1543],{"class":200,"line":275},[198,1524,1525],{"class":248},"        \"interval_h\"",[198,1527,1528],{"class":208},": ",[198,1530,547],{"class":241},[198,1532,550],{"class":208},[198,1534,242],{"class":241},[198,1536,486],{"class":204},[198,1538,557],{"class":248},[198,1540,388],{"class":208},[198,1542,562],{"class":241},[198,1544,1545],{"class":208},")),\n",[198,1547,1548],{"class":200,"line":295},[198,1549,1550],{"class":208},"    }\n",[198,1552,1553],{"class":200,"line":310},[198,1554,235],{"emptyLinePlaceholder":234},[198,1556,1557,1560,1563,1565],{"class":200,"line":324},[198,1558,1559],{"class":204},"for",[198,1561,1562],{"class":208}," layer ",[198,1564,500],{"class":204},[198,1566,1567],{"class":208}," QgsProject.instance().mapLayers().values():\n",[198,1569,1570,1573,1575],{"class":200,"line":343},[198,1571,1572],{"class":204},"    if",[198,1574,906],{"class":241},[198,1576,1577],{"class":208},"(layer, QgsVectorLayer):\n",[198,1579,1580,1583,1585],{"class":200,"line":356},[198,1581,1582],{"class":208},"        cfg ",[198,1584,287],{"class":204},[198,1586,1587],{"class":208}," sync_settings(layer)\n",[198,1589,1590,1592,1595,1598,1601,1603,1605,1608],{"class":200,"line":364},[198,1591,1156],{"class":204},[198,1593,1594],{"class":208}," cfg[",[198,1596,1597],{"class":248},"\"enabled\"",[198,1599,1600],{"class":208},"] ",[198,1602,912],{"class":204},[198,1604,1594],{"class":208},[198,1606,1607],{"class":248},"\"id_field\"",[198,1609,1610],{"class":208},"]:\n",[198,1612,1613,1616,1618,1621,1624,1627,1630,1633,1636,1638,1641,1644,1647,1649,1652,1654,1656,1659,1661,1663,1666],{"class":200,"line":377},[198,1614,1615],{"class":241},"            print",[198,1617,267],{"class":208},[198,1619,1620],{"class":204},"f",[198,1622,1623],{"class":248},"\"",[198,1625,1626],{"class":241},"{",[198,1628,1629],{"class":208},"layer.name()",[198,1631,1632],{"class":241},"}",[198,1634,1635],{"class":248},": sync on ",[198,1637,1626],{"class":241},[198,1639,1640],{"class":208},"cfg[",[198,1642,1643],{"class":248},"'id_field'",[198,1645,1646],{"class":208},"]",[198,1648,1632],{"class":241},[198,1650,1651],{"class":248}," every ",[198,1653,1626],{"class":241},[198,1655,1640],{"class":208},[198,1657,1658],{"class":248},"'interval_h'",[198,1660,1646],{"class":208},[198,1662,1632],{"class":241},[198,1664,1665],{"class":248}," h\"",[198,1667,340],{"class":208},[14,1669,1670,1672,1673,1677],{},[699,1671,701],{}," Reading through one function means a renamed key or a changed default is a single edit, and every caller handles missing or stringly-typed values the same way. Layers without settings fall back to defaults — here \"not enabled\" — so the plugin never acts on a layer the user has not configured. The same settings could drive a ",[21,1674,1676],{"href":1675},"\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-layer-tree-indicator-pyqgis\u002F","layer tree indicator"," showing which layers are synchronised.",[156,1679,1681],{"id":1680},"validate-before-applying","Validate before applying",[14,1683,1684],{},"A properties page should not accept settings that will fail later: a sync enabled without an id field, an interval outside what the server allows. Validating in the widget and signalling the problem keeps bad settings out.",[14,1686,1687],{},[34,1688,1691,1694,1697,1699,1714,1717,1719,1724,1728,1732,1737,1740,1744,1747,1749,1755,1759,1763,1768,1772,1776,1780],{"viewBox":1689,"role":37,"ariaLabel":1690,"xmlns":39},"0 0 760 208","Page-level validation: enabled sync requires an id field, invalid combinations show a message and are not applied",[41,1692,1693],{},"Validate in the page",[45,1695,1696],{},"As the user edits, the page checks the combination of settings: sync enabled requires an id field. When invalid, it shows a short message in the page and refuses to write in apply, keeping the previous settings. When valid, apply writes all values at once.",[49,1698],{"x":51,"y":51,"width":52,"height":105,"fill":54},[1700,1701,1702],"defs",{},[1703,1704,1710],"marker",{"id":1705,"viewBox":1706,"refX":740,"refY":1707,"markerWidth":1708,"markerHeight":1708,"orient":1709},"lpValidateArrow","0 0 10 10","5","7","auto-start-reverse",[1711,1712],"path",{"d":1713,"fill":83},"M0 0 L10 5 L0 10 z",[56,1715,1716],{"x":58,"y":59,"style":60,"fill":61,"textAnchor":62},"Refuse settings that cannot work",[49,1718],{"x":562,"y":80,"width":732,"height":146,"rx":740,"fill":71,"stroke":72,"style":743},[56,1720,1723],{"x":1721,"y":1722,"style":748,"fill":61,"textAnchor":62},"135","99.78","user edits",[56,1725,1727],{"x":1721,"y":1726,"style":753,"fill":72,"textAnchor":62},"123.78","enabled, field,",[56,1729,1731],{"x":1721,"y":1730,"style":753,"fill":72,"textAnchor":62},"147.78","interval",[200,1733],{"x1":1734,"y1":146,"x2":1735,"y2":146,"stroke":83,"style":1736},"246","288","stroke-width:1.8;marker-end:url(#lpValidateArrow)",[49,1738],{"x":1739,"y":80,"width":129,"height":146,"rx":740,"fill":774,"stroke":775,"style":743},"292",[56,1741,1743],{"x":1742,"y":1722,"style":748,"fill":775,"textAnchor":62},"392","valid?",[56,1745,1746],{"x":1742,"y":1726,"style":753,"fill":72,"textAnchor":62},"enabled needs",[56,1748,770],{"x":1742,"y":1730,"style":753,"fill":72,"textAnchor":62},[200,1750],{"x1":1751,"y1":1752,"x2":1753,"y2":1754,"stroke":83,"style":1736},"492","100","534","80",[200,1756],{"x1":1751,"y1":1757,"x2":1753,"y2":1758,"stroke":83,"style":1736},"140","160",[49,1760],{"x":1761,"y":67,"width":1762,"height":80,"rx":740,"fill":789,"stroke":790,"style":743},"538","198",[56,1764,1767],{"x":1765,"y":1766,"style":748,"fill":794,"textAnchor":62},"637","71.78","apply all",[56,1769,1771],{"x":1765,"y":1770,"style":753,"fill":72,"textAnchor":62},"91.78","write properties",[49,1773],{"x":1761,"y":1774,"width":1762,"height":80,"rx":740,"fill":774,"stroke":1775,"style":743},"132","#b91c1c",[56,1777,1779],{"x":1765,"y":1778,"style":748,"fill":1775,"textAnchor":62},"155.78","show message",[56,1781,1783],{"x":1765,"y":1782,"style":753,"fill":72,"textAnchor":62},"175.78","keep old values",[189,1785,1787],{"className":191,"code":1786,"language":193,"meta":194,"style":194},"from qgis.PyQt.QtWidgets import QLabel\n\nclass ValidatedAssetSyncPage(AssetSyncPage):\n    def __init__(self, layer, canvas, parent=None):\n        super().__init__(layer, canvas, parent)\n        self.message = QLabel()\n        self.message.setStyleSheet(\"color: #b91c1c\")\n        self.layout().addRow(self.message)\n        self.enabled.toggled.connect(self._validate)\n        self.id_field.fieldChanged.connect(self._validate)\n        self._validate()\n\n    def _validate(self, *_):\n        problem = \"\"\n        if self.enabled.isChecked() and not self.id_field.currentField():\n            problem = \"Choose an asset id field to enable sync.\"\n        self.message.setText(problem)\n        return not problem\n\n    def apply(self):\n        if self._validate():\n            super().apply()\n",[27,1788,1789,1800,1804,1818,1832,1842,1854,1866,1878,1890,1901,1908,1912,1928,1938,1957,1967,1974,1983,1987,1995,2004],{"__ignoreMap":194},[198,1790,1791,1793,1795,1797],{"class":200,"line":201},[198,1792,205],{"class":204},[198,1794,223],{"class":208},[198,1796,212],{"class":204},[198,1798,1799],{"class":208}," QLabel\n",[198,1801,1802],{"class":200,"line":218},[198,1803,235],{"emptyLinePlaceholder":234},[198,1805,1806,1808,1811,1813,1816],{"class":200,"line":231},[198,1807,260],{"class":204},[198,1809,1810],{"class":263}," ValidatedAssetSyncPage",[198,1812,267],{"class":208},[198,1814,1815],{"class":263},"AssetSyncPage",[198,1817,272],{"class":208},[198,1819,1820,1822,1824,1826,1828,1830],{"class":200,"line":238},[198,1821,278],{"class":204},[198,1823,281],{"class":241},[198,1825,284],{"class":208},[198,1827,287],{"class":204},[198,1829,290],{"class":241},[198,1831,272],{"class":208},[198,1833,1834,1836,1838,1840],{"class":200,"line":252},[198,1835,298],{"class":241},[198,1837,301],{"class":208},[198,1839,304],{"class":241},[198,1841,307],{"class":208},[198,1843,1844,1846,1849,1851],{"class":200,"line":257},[198,1845,313],{"class":241},[198,1847,1848],{"class":208},".message ",[198,1850,287],{"class":204},[198,1852,1853],{"class":208}," QLabel()\n",[198,1855,1856,1858,1861,1864],{"class":200,"line":275},[198,1857,313],{"class":241},[198,1859,1860],{"class":208},".message.setStyleSheet(",[198,1862,1863],{"class":248},"\"color: #b91c1c\"",[198,1865,340],{"class":208},[198,1867,1868,1870,1873,1875],{"class":200,"line":295},[198,1869,313],{"class":241},[198,1871,1872],{"class":208},".layout().addRow(",[198,1874,425],{"class":241},[198,1876,1877],{"class":208},".message)\n",[198,1879,1880,1882,1885,1887],{"class":200,"line":310},[198,1881,313],{"class":241},[198,1883,1884],{"class":208},".enabled.toggled.connect(",[198,1886,425],{"class":241},[198,1888,1889],{"class":208},"._validate)\n",[198,1891,1892,1894,1897,1899],{"class":200,"line":324},[198,1893,313],{"class":241},[198,1895,1896],{"class":208},".id_field.fieldChanged.connect(",[198,1898,425],{"class":241},[198,1900,1889],{"class":208},[198,1902,1903,1905],{"class":200,"line":343},[198,1904,313],{"class":241},[198,1906,1907],{"class":208},"._validate()\n",[198,1909,1910],{"class":200,"line":356},[198,1911,235],{"emptyLinePlaceholder":234},[198,1913,1914,1916,1919,1922,1925],{"class":200,"line":364},[198,1915,278],{"class":204},[198,1917,1918],{"class":263}," _validate",[198,1920,1921],{"class":208},"(self, ",[198,1923,1924],{"class":204},"*",[198,1926,1927],{"class":208},"_):\n",[198,1929,1930,1933,1935],{"class":200,"line":377},[198,1931,1932],{"class":208},"        problem ",[198,1934,287],{"class":204},[198,1936,1937],{"class":248}," \"\"\n",[198,1939,1940,1942,1944,1947,1949,1952,1954],{"class":200,"line":396},[198,1941,1156],{"class":204},[198,1943,1159],{"class":241},[198,1945,1946],{"class":208},".enabled.isChecked() ",[198,1948,912],{"class":204},[198,1950,1951],{"class":204}," not",[198,1953,1159],{"class":241},[198,1955,1956],{"class":208},".id_field.currentField():\n",[198,1958,1959,1962,1964],{"class":200,"line":409},[198,1960,1961],{"class":208},"            problem ",[198,1963,287],{"class":204},[198,1965,1966],{"class":248}," \"Choose an asset id field to enable sync.\"\n",[198,1968,1969,1971],{"class":200,"line":414},[198,1970,313],{"class":241},[198,1972,1973],{"class":208},".message.setText(problem)\n",[198,1975,1976,1978,1980],{"class":200,"line":430},[198,1977,903],{"class":204},[198,1979,1951],{"class":204},[198,1981,1982],{"class":208}," problem\n",[198,1984,1985],{"class":200,"line":441},[198,1986,235],{"emptyLinePlaceholder":234},[198,1988,1989,1991,1993],{"class":200,"line":456},[198,1990,278],{"class":204},[198,1992,631],{"class":263},[198,1994,634],{"class":208},[198,1996,1997,1999,2001],{"class":200,"line":471},[198,1998,1156],{"class":204},[198,2000,1159],{"class":241},[198,2002,2003],{"class":208},"._validate():\n",[198,2005,2006,2009],{"class":200,"line":476},[198,2007,2008],{"class":241},"            super",[198,2010,2011],{"class":208},"().apply()\n",[14,2013,2014,2016,2017,2019,2020,2024,2025,2028],{},[699,2015,701],{}," Validating on every change gives immediate feedback in the page itself, which is less disruptive than a message box. ",[27,2018,1405],{}," checks again and writes nothing if the combination is invalid, so the layer keeps its last good settings. For broader validation helpers, see ",[21,2021,2023],{"href":2022},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fvalidate-plugin-dialog-input-pyqgis\u002F","validating plugin dialog input",". Return ",[27,2026,2027],{},"ValidatedAssetSyncPage"," from the factory to use it.",[156,2030,2032],{"id":2031},"qgis-version-compatibility","QGIS version compatibility",[14,2034,2035,388,2037,2039,2040,2043,2044,2047,2048,2050],{},[27,2036,857],{},[27,2038,29],{}," and ",[27,2041,2042],{},"iface.registerMapLayerConfigWidgetFactory"," exist throughout QGIS 3.x and in QGIS 4. ",[27,2045,2046],{},"supportLayerPropertiesDialog"," was added in 3.0 and ",[27,2049,1001],{}," has always been available. Custom property values written as Python booleans may read back as strings in older releases; the tolerant checks above handle both.",[156,2052,2054],{"id":2053},"troubleshooting","Troubleshooting",[161,2056,2057,2069,2078,2084],{},[164,2058,2059,2062,2063,2065,2066,2068],{},[699,2060,2061],{},"The page does not appear."," The factory was not registered before the dialog opened, ",[27,2064,997],{}," returned False, or ",[27,2067,2046],{}," is False.",[164,2070,2071,2074,2075,174],{},[699,2072,2073],{},"Duplicate pages after reloading."," The factory was not unregistered in ",[27,2076,2077],{},"unload()",[164,2079,2080,2083],{},[699,2081,2082],{},"Settings are lost when the project reopens."," They were stored on the plugin instead of as layer custom properties, or the project was not saved.",[164,2085,2086,1382,2089,2091],{},[699,2087,2088],{},"Apply does nothing.",[27,2090,709],{}," is never emitted, so the dialog does not know the page changed.",[156,2093,2095],{"id":2094},"conclusion","Conclusion",[14,2097,2098,2099,2101,2102,2104,2105,2107,2108,2110],{},"Build a ",[27,2100,29],{}," that reads and writes layer custom properties, emit ",[27,2103,709],{}," on edits, write a factory with a title, icon and a narrow ",[27,2106,997],{}," rule, register it through ",[27,2109,1016],{}," and unregister on unload, read settings through one helper, and validate before applying.",[156,2112,2114],{"id":2113},"frequently-asked-questions","Frequently Asked Questions",[14,2116,2117,2120,2121,2123],{},[699,2118,2119],{},"Can the page appear in the styling panel too?","\nYes — return True from ",[27,2122,1001],{},", and apply changes live when the panel is in live-update mode.",[14,2125,2126,2129,2130,2133,2134,174],{},[699,2127,2128],{},"Can I add a page for raster layers?","\nYes; accept ",[27,2131,2132],{},"QgsRasterLayer"," in ",[27,2135,997],{},[14,2137,2138,2141,2142,174],{},[699,2139,2140],{},"Where else can plugins store per-layer data?","\nCustom properties are the standard place. For project-wide settings, use project entries; for user preferences, ",[27,2143,2144],{},"QgsSettings",[14,2146,2147,2150,2151,2154,2155,2159],{},[699,2148,2149],{},"Is there a page for project properties too?","\nYes: ",[27,2152,2153],{},"QgsOptionsWidgetFactory"," adds pages to the Options dialog, as in ",[21,2156,2158],{"href":2157},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fadd-plugin-options-page-to-qgis-settings\u002F","adding a plugin options page",", and project property pages are available on recent releases.",[156,2161,2163],{"id":2162},"related","Related",[161,2165,2166,2171,2176,2181,2187],{},[164,2167,2168,2170],{},[21,2169,24],{"href":23}," — the guide this recipe belongs to",[164,2172,2173],{},[21,2174,2175],{"href":1675},"Add a Layer Tree Indicator in PyQGIS",[164,2177,2178],{},[21,2179,2180],{"href":2157},"Add a Plugin Options Page to QGIS Settings",[164,2182,2183],{},[21,2184,2186],{"href":2185},"\u002Fqgis-plugin-development\u002Fplugin-settings-and-localization\u002Fstore-per-project-plugin-settings-pyqgis\u002F","Store Per-Project Plugin Settings in PyQGIS",[164,2188,2189],{},[21,2190,2192],{"href":2191},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fuse-qgis-custom-widgets-in-qt-designer\u002F","Use QGIS Custom Widgets in Qt Designer",[2194,2195,2196],"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 .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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":194,"searchDepth":218,"depth":218,"links":2198},[2199,2200,2201,2202,2203,2204,2205,2206,2207,2208,2209,2210],{"id":158,"depth":218,"text":159},{"id":177,"depth":218,"text":178},{"id":713,"depth":218,"text":714},{"id":1009,"depth":218,"text":1010},{"id":1201,"depth":218,"text":1202},{"id":1416,"depth":218,"text":1417},{"id":1680,"depth":218,"text":1681},{"id":2031,"depth":218,"text":2032},{"id":2053,"depth":218,"text":2054},{"id":2094,"depth":218,"text":2095},{"id":2113,"depth":218,"text":2114},{"id":2162,"depth":218,"text":2163},"Add your own page to the QGIS Layer Properties dialog and Layer Styling panel with QgsMapLayerConfigWidgetFactory — choosing which layers it supports, storing settings as layer custom properties, applying changes, and registering and unregistering from a plugin.","md",{"slug":2214,"type":2215,"breadcrumb":2216,"datePublished":2217,"dateModified":2217},"add-custom-layer-properties-page-pyqgis","article","Add a Custom Layer Properties Page","2026-10-02","\u002Fqgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-custom-layer-properties-page-pyqgis",{"title":5,"description":2211},"qgis-plugin-development\u002Fextending-qgis-with-custom-classes\u002Fadd-custom-layer-properties-page-pyqgis\u002Findex","4erFtERRWod3g-ZoCdQ3xALQLFVs3fTO5fFbHrRxm90",1790966257256]