Skip to content

Commit ce55b9f

Browse files
committed
Render QGIS CheckBox-widget fields as real checkboxes in popups
Fields configured with a CheckBox edit widget in QGIS previously displayed their raw value ("Ja", "Nein", "true", "false", etc.) in the feature popup, instead of mirroring the editing form's native checkbox. This change covers two popup render paths: * Auto popup (popupDefaultContent, popup_all_features_table): new jTpl modifier popupcheckbox is used in place of featurepopup; receives a per-layer map of CheckBox fields resolved from the QGIS project XML by WMSRequest::getCheckBoxFieldsForLayer. * QGIS/form popup (popupSource=form|qgis): WMSRequest::applyCheckBoxesToFormPopup post-processes the form HTML returned by QGIS Server, replacing the text inside each <span id="dd_jforms_view_edition_FIELD" class="jforms-control-input"> with a disabled <input type="checkbox"> when the field is a CheckBox widget. Matching is lenient so the common real-world variants all render as checkboxes: the configured CheckedState/UncheckedState, common boolean representations (true/t/1/yes/on and false/f/0/no/off) for fields stored as boolean, and null-like values ('', NULL, QGIS's '()') which render as unchecked. Fixes #6748
1 parent a2e4287 commit ce55b9f

File tree

4 files changed

+228
-3
lines changed

4 files changed

+228
-3
lines changed

lizmap/modules/lizmap/lib/Request/WMSRequest.php

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use Lizmap\App\WktTools;
1717
use Lizmap\Project\Project;
18+
use Lizmap\Project\Qgis;
1819
use Lizmap\Project\UnknownLizmapProjectException;
1920

2021
/**
@@ -695,6 +696,10 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,
695696

696697
$remoteStorageProfile = RemoteStorageRequest::getProfile('webdav');
697698

699+
// Fields configured in QGIS with a CheckBox edit widget: rendered as
700+
// actual checkboxes in the auto popup, mirroring the editing form.
701+
$checkBoxFields = $this->getCheckBoxFieldsForLayer($layerId);
702+
698703
// Get the template for the popup content
699704
$templateConfigured = false;
700705
$popupTemplate = '';
@@ -754,6 +759,7 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,
754759
'featureId' => $id,
755760
'attributes' => $feature->Attribute,
756761
'remoteStorageProfile' => $remoteStorageProfile,
762+
'checkBoxFields' => $checkBoxFields,
757763
));
758764
$autoContent = $popupFeatureContent;
759765
// Get specific template for the layer has been configured
@@ -852,7 +858,7 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,
852858
$finalContent = $autoContent;
853859
if (property_exists($configLayer, 'popupSource')) {
854860
if (in_array($configLayer->popupSource, array('qgis', 'form')) && $maptipValue) {
855-
$finalContent = $maptipValue;
861+
$finalContent = $this->applyCheckBoxesToFormPopup($maptipValue, $checkBoxFields);
856862
}
857863
if ($configLayer->popupSource == 'lizmap' && $templateConfigured) {
858864
$finalContent = $lizmapContent;
@@ -875,6 +881,7 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,
875881
'allFeatureAttributes' => array_reverse($allFeatureAttributes),
876882
'remoteStorageProfile' => $remoteStorageProfile,
877883
'allFeatureToolbars' => array_reverse($allFeatureToolbars),
884+
'checkBoxFields' => $checkBoxFields,
878885
));
879886
}
880887

@@ -1369,4 +1376,136 @@ protected function getMapData($project, $params, $forced = false)
13691376

13701377
return new OGCResponse($code, $mime, $data, $cached);
13711378
}
1379+
1380+
/**
1381+
* Build a map of fields configured with a CheckBox edit widget for the
1382+
* given layer, using the QGIS project's typed XML info (cached per request
1383+
* by ProjectInfo::fromQgisPath).
1384+
*
1385+
* @param string $layerId
1386+
*
1387+
* @return array<string, array{CheckedState: string, UncheckedState: string}>
1388+
*/
1389+
private function getCheckBoxFieldsForLayer($layerId)
1390+
{
1391+
$checkBoxFields = array();
1392+
1393+
$qgisPath = $this->project->getQgisPath();
1394+
if (!$qgisPath || !file_exists($qgisPath)) {
1395+
return $checkBoxFields;
1396+
}
1397+
1398+
try {
1399+
$projectInfo = Qgis\ProjectInfo::fromQgisPath($qgisPath);
1400+
} catch (\Exception $e) {
1401+
return $checkBoxFields;
1402+
}
1403+
1404+
$xmlLayer = $projectInfo->getLayerById($layerId);
1405+
if (!$xmlLayer instanceof Qgis\Layer\VectorLayer) {
1406+
return $checkBoxFields;
1407+
}
1408+
1409+
foreach ($xmlLayer->fieldConfiguration as $field) {
1410+
$editWidget = $field->editWidget;
1411+
if (strtolower($editWidget->type) !== 'checkbox') {
1412+
continue;
1413+
}
1414+
$config = $editWidget->config;
1415+
if (!$config instanceof Qgis\BaseQgisObject) {
1416+
continue;
1417+
}
1418+
$data = $config->getData();
1419+
$checked = array_key_exists('CheckedState', $data) ? (string) $data['CheckedState'] : '';
1420+
$unchecked = array_key_exists('UncheckedState', $data) ? (string) $data['UncheckedState'] : '';
1421+
// Fall back to QGIS defaults (see QgisFormControl::fillCheckboxValues)
1422+
$checkBoxFields[(string) $field->name] = array(
1423+
'CheckedState' => $checked === '' ? 't' : $checked,
1424+
'UncheckedState' => $unchecked === '' ? 'f' : $unchecked,
1425+
);
1426+
}
1427+
1428+
return $checkBoxFields;
1429+
}
1430+
1431+
/**
1432+
* Replace raw CheckBox-widget values in the QGIS drag-and-drop form popup
1433+
* HTML with disabled <input type="checkbox"> elements, mirroring the
1434+
* editing form. Leaves non-matching values untouched.
1435+
*
1436+
* QGIS Server emits each field in the form popup as:
1437+
* <span id="dd_jforms_view_edition_FIELDNAME" class="jforms-control-input">VALUE</span>
1438+
*
1439+
* @param string $maptipHtml form popup HTML returned by QGIS Server
1440+
* @param array $checkBoxFields map fieldName => ['CheckedState' => string, 'UncheckedState' => string]
1441+
*
1442+
* @return string
1443+
*/
1444+
private function applyCheckBoxesToFormPopup($maptipHtml, $checkBoxFields)
1445+
{
1446+
if (!is_string($maptipHtml) || $maptipHtml === '' || !is_array($checkBoxFields) || count($checkBoxFields) === 0) {
1447+
return $maptipHtml;
1448+
}
1449+
1450+
return preg_replace_callback(
1451+
'/(<span\s+id="dd_jforms_view_edition_([^"]+)"\s+class="jforms-control-input"\s*>)(.*?)(<\/span>)/us',
1452+
function ($m) use ($checkBoxFields) {
1453+
$fieldName = html_entity_decode($m[2], ENT_QUOTES, 'UTF-8');
1454+
if (!isset($checkBoxFields[$fieldName])) {
1455+
return $m[0];
1456+
}
1457+
$value = trim(html_entity_decode($m[3], ENT_QUOTES, 'UTF-8'));
1458+
$cfg = $checkBoxFields[$fieldName];
1459+
$state = self::matchCheckBoxState(
1460+
$value,
1461+
isset($cfg['CheckedState']) ? (string) $cfg['CheckedState'] : '',
1462+
isset($cfg['UncheckedState']) ? (string) $cfg['UncheckedState'] : ''
1463+
);
1464+
if ($state === 'checked') {
1465+
return $m[1].'<input type="checkbox" disabled="disabled" checked="checked" class="lizmap-popup-checkbox-widget">'.$m[4];
1466+
}
1467+
if ($state === 'unchecked') {
1468+
return $m[1].'<input type="checkbox" disabled="disabled" class="lizmap-popup-checkbox-widget">'.$m[4];
1469+
}
1470+
1471+
return $m[0];
1472+
},
1473+
$maptipHtml
1474+
);
1475+
}
1476+
1477+
/**
1478+
* Match a raw attribute value against CheckBox widget states. Tries the
1479+
* configured CheckedState/UncheckedState first, then falls back to common
1480+
* boolean representations so that fields stored as boolean (which come
1481+
* through WMS/WFS as 'true'/'false') still render as checkboxes.
1482+
*
1483+
* @param string $value raw attribute value
1484+
* @param string $checkedExpected CheckedState configured in QGIS
1485+
* @param string $uncheckedExpected UncheckedState configured in QGIS
1486+
*
1487+
* @return null|string 'checked', 'unchecked', or null for no match
1488+
*/
1489+
private static function matchCheckBoxState($value, $checkedExpected, $uncheckedExpected)
1490+
{
1491+
if ($checkedExpected !== '' && $value === $checkedExpected) {
1492+
return 'checked';
1493+
}
1494+
if ($uncheckedExpected !== '' && $value === $uncheckedExpected) {
1495+
return 'unchecked';
1496+
}
1497+
$normalized = strtolower(trim($value));
1498+
if (in_array($normalized, array('true', 't', '1', 'yes', 'on'), true)) {
1499+
return 'checked';
1500+
}
1501+
if (in_array($normalized, array('false', 'f', '0', 'no', 'off'), true)) {
1502+
return 'unchecked';
1503+
}
1504+
// Treat null-like values (NULL, empty, QGIS's "()" for NULL boolean) as unchecked
1505+
if (in_array($normalized, array('', 'null', '()'), true)) {
1506+
return 'unchecked';
1507+
}
1508+
1509+
return null;
1510+
}
13721511
}

lizmap/modules/view/templates/popupDefaultContent.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
{if $attribute['name'] != 'geometry' && $attribute['name'] != 'maptip'}
1212
<tr data-field-name="{$attribute['name']}" {if $attribute['value']=='' || $attribute['value']=='NULL' } class="empty-data" {/if}>
1313
<th>{$attribute['name']}</th>
14-
<td>{$attribute['name']|featurepopup:$attribute['value'],$repository,$project,$remoteStorageProfile}</td>
14+
<td>{$attribute['name']|popupcheckbox:$attribute['value'],$repository,$project,$checkBoxFields,$remoteStorageProfile}</td>
1515
</tr>
1616
{/if}
1717
{/foreach}

lizmap/modules/view/templates/popup_all_features_table.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<td>{$allFeatureToolbars[$key]}</td>
2323
{foreach $featureAttributes as $attribute}
2424
{if $attribute['name'] != 'geometry' && $attribute['name'] != 'maptip' && $attribute['value'] != ''}
25-
<td>{$attribute['name']|featurepopup:$attribute['value'],$repository,$project,$remoteStorageProfile}</td>
25+
<td>{$attribute['name']|popupcheckbox:$attribute['value'],$repository,$project,$checkBoxFields,$remoteStorageProfile}</td>
2626
{/if}
2727
{/foreach}
2828
</tr>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
/**
4+
* Plugin modifier for the popup templates.
5+
*
6+
* Renders a field value as a disabled checkbox when the field is configured
7+
* with a QGIS CheckBox edit widget. Falls back to the standard featurepopup
8+
* modifier for anything that doesn't match a recognised checked/unchecked
9+
* state.
10+
*
11+
* @author 3liz
12+
* @copyright 2026 3liz
13+
*
14+
* @see http://3liz.com
15+
*
16+
* @license Mozilla Public License : http://www.mozilla.org/MPL/
17+
*
18+
* @param mixed $attributeName feature Attribute name
19+
* @param mixed $attributeValue feature Attribute value
20+
* @param string $repository lizmap Repository
21+
* @param string $project name of the project
22+
* @param array $checkBoxFields map fieldName => ['CheckedState' => string, 'UncheckedState' => string]
23+
* @param null|array $remoteStorageProfile webDav configuration
24+
*
25+
* @return string
26+
*/
27+
function jtpl_modifier_common_popupcheckbox($attributeName, $attributeValue, $repository, $project, $checkBoxFields, $remoteStorageProfile = null)
28+
{
29+
$name = (string) $attributeName;
30+
31+
if (is_array($checkBoxFields) && isset($checkBoxFields[$name])) {
32+
$cfg = $checkBoxFields[$name];
33+
$state = lizmap_popup_checkbox_match_state(
34+
(string) $attributeValue,
35+
isset($cfg['CheckedState']) ? (string) $cfg['CheckedState'] : '',
36+
isset($cfg['UncheckedState']) ? (string) $cfg['UncheckedState'] : ''
37+
);
38+
if ($state === 'checked') {
39+
return '<input type="checkbox" disabled="disabled" checked="checked" class="lizmap-popup-checkbox-widget">';
40+
}
41+
if ($state === 'unchecked') {
42+
return '<input type="checkbox" disabled="disabled" class="lizmap-popup-checkbox-widget">';
43+
}
44+
}
45+
46+
$popupClass = jClasses::getService('view~popup');
47+
48+
return $popupClass->getHtmlFeatureAttribute($attributeName, $attributeValue, $repository, $project, null, $remoteStorageProfile);
49+
}
50+
51+
/**
52+
* Decide whether the given raw value represents a checked, unchecked, or
53+
* unrecognised state for a QGIS CheckBox-widget field. Matches the
54+
* configured CheckedState/UncheckedState first, then falls back to common
55+
* boolean representations (so fields typed as boolean, which come through
56+
* WMS/WFS as 'true'/'false' regardless of the widget's labels, also render
57+
* as checkboxes). Null-like values render as unchecked.
58+
*
59+
* @param string $value raw attribute value
60+
* @param string $checkedExpected CheckedState configured in QGIS
61+
* @param string $uncheckedExpected UncheckedState configured in QGIS
62+
*
63+
* @return null|string 'checked', 'unchecked', or null for no match
64+
*/
65+
function lizmap_popup_checkbox_match_state($value, $checkedExpected, $uncheckedExpected)
66+
{
67+
if ($checkedExpected !== '' && $value === $checkedExpected) {
68+
return 'checked';
69+
}
70+
if ($uncheckedExpected !== '' && $value === $uncheckedExpected) {
71+
return 'unchecked';
72+
}
73+
$normalized = strtolower(trim($value));
74+
if (in_array($normalized, array('true', 't', '1', 'yes', 'on'), true)) {
75+
return 'checked';
76+
}
77+
if (in_array($normalized, array('false', 'f', '0', 'no', 'off'), true)) {
78+
return 'unchecked';
79+
}
80+
// QGIS's "()" represents a NULL boolean in some popup renderings
81+
if (in_array($normalized, array('', 'null', '()'), true)) {
82+
return 'unchecked';
83+
}
84+
85+
return null;
86+
}

0 commit comments

Comments
 (0)