This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/134/head
in repository enlightenment.
View the commit online.
commit 6415f2f4d52d03196666146d861044ed2b52e7c1
Author: [email protected] <[email protected]>
AuthorDate: Thu May 21 19:20:19 2026 -0600
refactor(networkmanager): incremental popup updates (no full-clear flicker)
The popup's _enm_popup_update used to elm_genlist_clear() and rebuild every
row on each state change. Genlist is realisation-driven and that pattern
forces it to throw away its content cache and re-realise every visible
row, which on the user's side reads as a brief "blink" each time an AP
appeared, signal strength ticked, or a VPN went up/down.
Replace with a section-by-section diff:
* Pass 1 walks the existing genlist items and indexes them by identity:
ethernet rows by their NM_Device pointer, wifi rows by SSID stringshare,
VPN rows by NM_VPN_Connection.path. Group headers live on three new
fields in the popup struct (group_eth/wifi/vpn) so they survive across
updates.
* Passes 2-4 walk the desired layout in order (ethernet -> wifi -> VPN)
and for each entry either look it up by identity (refresh per-item
data + elm_genlist_item_update for content/text re-render) or
elm_genlist_item_insert_after the running 'prev' anchor so newly
appearing entries land at the correct position without disturbing
neighbours.
Wifi rows match on SSID rather than AP D-Bus path so that roaming
the "best AP" within the same SSID does not look like a remove+add;
id->ap and id->ap_path are refreshed in place on reuse so the icon
and security cb pick up the new backing AP.
* Pass 5 deletes any rows the desired set did not consume. itc->func.del
still owns the Enm_Item_Data free for AP/eth rows; VPN rows keep
func.del=NULL because the module owns NM_VPN_Connection.
Group headers are created lazily and torn down when their section
empties (no ethernet device, no wifi adapter). The wifi header is
elm_genlist_item_update'd on reuse so its wireless-enabled toggle
reflects manager state changes.
Net effect: AP signal updates, VPN activate/deactivate, and AP
appearance/disappearance now touch only the rows that actually
changed. No more full repaint of the popup on every notify.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
---
src/modules/networkmanager/e_mod_main.c | 333 +++++++++++++++++++++++++-------
src/modules/networkmanager/e_mod_main.h | 5 +
2 files changed, 270 insertions(+), 68 deletions(-)
diff --git a/src/modules/networkmanager/e_mod_main.c b/src/modules/networkmanager/e_mod_main.c
index b6c1737df..97517e2ae 100644
--- a/src/modules/networkmanager/e_mod_main.c
+++ b/src/modules/networkmanager/e_mod_main.c
@@ -126,6 +126,12 @@ enm_popup_del(E_NM_Instance *inst)
E_FREE_FUNC(inst->popup, e_object_del);
E_FREE_FUNC(inst->ctxt->popup_update_timer, ecore_timer_del);
inst->ui.popup.genlist = NULL;
+ /* Section-header items live inside the genlist that just died with
+ * the popup — clear the cached pointers so the next popup starts
+ * fresh in _enm_popup_update. */
+ inst->ui.popup.group_eth = NULL;
+ inst->ui.popup.group_wifi = NULL;
+ inst->ui.popup.group_vpn = NULL;
E_FREE_FUNC(inst->ui.popup.itc_group, elm_genlist_item_class_free);
E_FREE_FUNC(inst->ui.popup.itc_group_wifi, elm_genlist_item_class_free);
E_FREE_FUNC(inst->ui.popup.itc_group_vpn, elm_genlist_item_class_free);
@@ -1155,119 +1161,310 @@ _enm_has_wifi_device(struct NM_Manager *nm)
return EINA_FALSE;
}
+/* Drop a tracked group-header pointer if it matches. Used in the orphan
+ * cleanup pass so we don't leave stale Elm_Object_Item * pointers in
+ * inst->ui.popup.group_{eth,wifi,vpn}. */
+static void
+_enm_drop_group_if(E_NM_Instance *inst, Elm_Object_Item *it)
+{
+ if (inst->ui.popup.group_eth == it) inst->ui.popup.group_eth = NULL;
+ if (inst->ui.popup.group_wifi == it) inst->ui.popup.group_wifi = NULL;
+ if (inst->ui.popup.group_vpn == it) inst->ui.popup.group_vpn = NULL;
+}
+
+/* Diff-and-sync update of the popup genlist contents.
+ *
+ * The previous implementation called elm_genlist_clear() and re-appended
+ * every row on each call, which produced a visible blink whenever any
+ * NetworkManager state changed (new AP, signal strength tick, VPN
+ * activate/deactivate, ...). This walker keeps existing rows in place,
+ * re-runs their text_get/content_get only when needed via
+ * elm_genlist_item_update(), and only appends/removes items at the diff
+ * boundary.
+ *
+ * Identity per row:
+ * - ethernet: Enm_Item_Data.dev pointer (stable per device)
+ * - wifi: Enm_Item_Data.ssid stringshare (stable per SSID, while the
+ * concrete "best AP" struct backing it can change across
+ * scans — we refresh id->ap and id->ap_path on reuse)
+ * - VPN: NM_VPN_Connection.path stringshare
+ * - groups: the per-instance itc pointer (one of each per popup)
+ */
static void
_enm_popup_update(struct NM_Manager *nm, E_NM_Instance *inst)
{
Evas_Object *gl = inst->ui.popup.genlist;
struct _Popup_Entry desired[256];
- int want_n, i;
- Elm_Object_Item *wifi_group = NULL, *eth_group = NULL;
- int wifi_count = 0, eth_count = 0;
+ int want_n, i, eth_count = 0, wifi_count = 0;
+ Eina_Hash *eth_idx, *ap_idx, *vpn_idx;
+ Eina_List *orphans = NULL;
+ Elm_Object_Item *it, *prev;
+ Eina_Iterator *itr;
EINA_SAFETY_ON_NULL_RETURN(nm);
EINA_SAFETY_ON_NULL_RETURN(gl);
- /* elm_genlist_clear() below frees all items — drop any pending
- * deselect targeting an item about to be destroyed. */
- E_FREE_FUNC(inst->ui.popup.deselect_timer, ecore_timer_del);
- inst->ui.popup.deselect_item = NULL;
+ /* NOTE: we intentionally do NOT kill the deselect timer here. When the
+ * user taps a row we schedule a 0.5s deselect so the highlight feels
+ * deliberate. The VPN active-changed path runs synchronously via an
+ * Ecore_Job and would land in _enm_popup_update almost immediately —
+ * killing the timer here meant the tapped row stayed selected, and
+ * Elementary genlist doesn't refire "selected" on an already-selected
+ * item, so the user couldn't tap again to deactivate. Instead we
+ * cancel only when the target item is actually deleted (orphan pass
+ * below). */
want_n = _enm_popup_build_entries(nm, desired, 256);
-
- /* Count per-type so we know whether to insert group headers */
for (i = 0; i < want_n; i++)
{
- if (desired[i].ap)
- wifi_count++;
- else
- eth_count++;
+ if (desired[i].ap) wifi_count++;
+ else eth_count++;
}
- /* Clear and rebuild — simpler than incremental diff with grouped headers */
- elm_genlist_clear(gl);
+ /* ------------------------------------------------------------------
+ * Pass 1: index every existing row by its identity. Group headers
+ * already live on inst->ui.popup.group_*, so they're skipped here.
+ * ------------------------------------------------------------------ */
+ eth_idx = eina_hash_pointer_new(NULL); /* key: NM_Device * */
+ ap_idx = eina_hash_string_superfast_new(NULL); /* key: ssid stringshare */
+ vpn_idx = eina_hash_string_superfast_new(NULL); /* key: vc->path */
- /* Insert ethernet group + items */
+ it = elm_genlist_first_item_get(gl);
+ while (it)
+ {
+ const Elm_Genlist_Item_Class *icl = elm_genlist_item_item_class_get(it);
+
+ if (icl == inst->ui.popup.itc_eth)
+ {
+ Enm_Item_Data *id = elm_object_item_data_get(it);
+ if (id && id->dev) eina_hash_add(eth_idx, &id->dev, it);
+ }
+ else if (icl == inst->ui.popup.itc_ap)
+ {
+ Enm_Item_Data *id = elm_object_item_data_get(it);
+ if (id && id->ssid) eina_hash_add(ap_idx, id->ssid, it);
+ }
+ else if (icl == inst->ui.popup.itc_vpn)
+ {
+ struct NM_VPN_Connection *vc = elm_object_item_data_get(it);
+ if (vc && vc->path) eina_hash_add(vpn_idx, vc->path, it);
+ }
+ it = elm_genlist_item_next_get(it);
+ }
+
+ /* ------------------------------------------------------------------
+ * Pass 2: sync ethernet section.
+ * Keep prev pointing at the just-placed item so new rows insert
+ * after their predecessor and ordering is preserved.
+ * ------------------------------------------------------------------ */
if (eth_count > 0)
{
- eth_group = elm_genlist_item_append(gl, inst->ui.popup.itc_group,
- (void *)_("Wired"), NULL,
- ELM_GENLIST_ITEM_GROUP,
- NULL, NULL);
- elm_genlist_item_select_mode_set(eth_group,
- ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY);
+ if (!inst->ui.popup.group_eth)
+ {
+ inst->ui.popup.group_eth = elm_genlist_item_append(
+ gl, inst->ui.popup.itc_group, (void *)_("Wired"), NULL,
+ ELM_GENLIST_ITEM_GROUP, NULL, NULL);
+ elm_genlist_item_select_mode_set(inst->ui.popup.group_eth,
+ ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY);
+ }
+ /* prev tracks the last placed real row in this section. While it
+ * is NULL we have no real predecessor yet, so the first cache-miss
+ * insert must use elm_genlist_item_append(parent=group) — passing
+ * the group header itself as the "after" of insert_after misroutes
+ * the new item (it lands as a sibling at top level instead of as a
+ * child of the group), breaking section grouping and the pass-1
+ * index next refresh. */
+ prev = NULL;
for (i = 0; i < want_n; i++)
{
- Enm_Item_Data *id;
-
- if (desired[i].ap) continue; /* skip wifi entries here */
-
- id = calloc(1, sizeof(*id));
- if (!id) continue;
- id->nm = nm;
- id->ap = NULL;
- id->dev = desired[i].dev;
- id->ap_path = NULL;
- id->ssid = NULL;
-
- elm_genlist_item_append(gl, inst->ui.popup.itc_eth, id,
- eth_group, ELM_GENLIST_ITEM_NONE,
- NULL, NULL);
+ if (desired[i].ap) continue;
+ it = eina_hash_find(eth_idx, &desired[i].dev);
+ if (it)
+ {
+ /* Reuse: refresh nm pointer (paranoia) and re-run cbs. */
+ Enm_Item_Data *id = elm_object_item_data_get(it);
+ if (id) id->nm = nm;
+ elm_genlist_item_update(it);
+ eina_hash_del(eth_idx, &desired[i].dev, it);
+ }
+ else
+ {
+ Enm_Item_Data *id = calloc(1, sizeof(*id));
+ if (!id) continue;
+ id->nm = nm;
+ id->dev = desired[i].dev;
+ if (!prev)
+ it = elm_genlist_item_append(
+ gl, inst->ui.popup.itc_eth, id,
+ inst->ui.popup.group_eth,
+ ELM_GENLIST_ITEM_NONE, NULL, NULL);
+ else
+ it = elm_genlist_item_insert_after(
+ gl, inst->ui.popup.itc_eth, id,
+ inst->ui.popup.group_eth, prev,
+ ELM_GENLIST_ITEM_NONE, NULL, NULL);
+ }
+ prev = it;
}
}
+ else if (inst->ui.popup.group_eth)
+ {
+ /* No ethernet rows desired and no group needed. Any leftover
+ * eth rows are picked up by the orphan pass below. */
+ elm_object_item_del(inst->ui.popup.group_eth);
+ inst->ui.popup.group_eth = NULL;
+ }
- /* Insert wifi group header (always when adapter present) + AP items */
+ /* ------------------------------------------------------------------
+ * Pass 3: sync wifi section. Header is present whenever a wifi
+ * adapter exists, regardless of AP count.
+ * ------------------------------------------------------------------ */
if (_enm_has_wifi_device(nm))
{
- wifi_group = elm_genlist_item_append(gl, inst->ui.popup.itc_group_wifi,
- inst, NULL,
- ELM_GENLIST_ITEM_GROUP,
- NULL, NULL);
- elm_genlist_item_select_mode_set(wifi_group,
- ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY);
-
- if (wifi_count > 0)
+ if (!inst->ui.popup.group_wifi)
{
- for (i = 0; i < want_n; i++)
+ inst->ui.popup.group_wifi = elm_genlist_item_append(
+ gl, inst->ui.popup.itc_group_wifi, inst, NULL,
+ ELM_GENLIST_ITEM_GROUP, NULL, NULL);
+ elm_genlist_item_select_mode_set(inst->ui.popup.group_wifi,
+ ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY);
+ }
+ else
+ {
+ /* Wireless-enabled toggle state may have flipped — refresh. */
+ elm_genlist_item_update(inst->ui.popup.group_wifi);
+ }
+ prev = NULL;
+
+ for (i = 0; i < want_n; i++)
+ {
+ if (!desired[i].ap) continue;
+ it = eina_hash_find(ap_idx, desired[i].label);
+ if (it)
{
- Enm_Item_Data *id;
-
- if (!desired[i].ap) continue;
-
- id = calloc(1, sizeof(*id));
+ /* Reuse: the "best AP" pointer for this SSID may have
+ * rotated to a stronger AP, and signal strength/security
+ * almost certainly changed — refresh the Enm_Item_Data
+ * fields before triggering a content/text re-fetch. */
+ Enm_Item_Data *id = elm_object_item_data_get(it);
+ if (id)
+ {
+ id->nm = nm;
+ id->ap = desired[i].ap;
+ eina_stringshare_replace(&id->ap_path,
+ desired[i].ap_path);
+ }
+ elm_genlist_item_update(it);
+ eina_hash_del(ap_idx, desired[i].label, it);
+ }
+ else
+ {
+ Enm_Item_Data *id = calloc(1, sizeof(*id));
if (!id) continue;
id->nm = nm;
id->ap = desired[i].ap;
- id->dev = NULL;
id->ap_path = eina_stringshare_add(desired[i].ap_path);
id->ssid = eina_stringshare_add(desired[i].label);
-
- elm_genlist_item_append(gl, inst->ui.popup.itc_ap, id,
- wifi_group, ELM_GENLIST_ITEM_NONE,
- NULL, NULL);
+ if (!prev)
+ it = elm_genlist_item_append(
+ gl, inst->ui.popup.itc_ap, id,
+ inst->ui.popup.group_wifi,
+ ELM_GENLIST_ITEM_NONE, NULL, NULL);
+ else
+ it = elm_genlist_item_insert_after(
+ gl, inst->ui.popup.itc_ap, id,
+ inst->ui.popup.group_wifi, prev,
+ ELM_GENLIST_ITEM_NONE, NULL, NULL);
}
+ prev = it;
}
}
+ else if (inst->ui.popup.group_wifi)
+ {
+ elm_object_item_del(inst->ui.popup.group_wifi);
+ inst->ui.popup.group_wifi = NULL;
+ }
- /* VPN section — always visible even when no VPN connections exist. */
+ /* ------------------------------------------------------------------
+ * Pass 4: sync VPN section. Always present.
+ * ------------------------------------------------------------------ */
{
- Elm_Object_Item *vpn_group;
struct NM_VPN_Connection *vc;
- vpn_group = elm_genlist_item_append(gl,
- inst->ui.popup.itc_group_vpn, inst, NULL,
- ELM_GENLIST_ITEM_GROUP, NULL, NULL);
- elm_genlist_item_select_mode_set(vpn_group,
- ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY);
+ if (!inst->ui.popup.group_vpn)
+ {
+ inst->ui.popup.group_vpn = elm_genlist_item_append(
+ gl, inst->ui.popup.itc_group_vpn, inst, NULL,
+ ELM_GENLIST_ITEM_GROUP, NULL, NULL);
+ elm_genlist_item_select_mode_set(inst->ui.popup.group_vpn,
+ ELM_OBJECT_SELECT_MODE_DISPLAY_ONLY);
+ }
+ prev = NULL;
EINA_INLIST_FOREACH(nm->vpn_connections, vc)
{
- elm_genlist_item_append(gl,
- inst->ui.popup.itc_vpn, vc, vpn_group,
- ELM_GENLIST_ITEM_NONE,
- NULL, NULL);
+ if (!vc->path) continue;
+ it = eina_hash_find(vpn_idx, vc->path);
+ if (it)
+ {
+ elm_genlist_item_update(it);
+ eina_hash_del(vpn_idx, vc->path, it);
+ }
+ else
+ {
+ if (!prev)
+ it = elm_genlist_item_append(
+ gl, inst->ui.popup.itc_vpn, vc,
+ inst->ui.popup.group_vpn,
+ ELM_GENLIST_ITEM_NONE, NULL, NULL);
+ else
+ it = elm_genlist_item_insert_after(
+ gl, inst->ui.popup.itc_vpn, vc,
+ inst->ui.popup.group_vpn, prev,
+ ELM_GENLIST_ITEM_NONE, NULL, NULL);
+ /* New VPN row: the genlist's "realized" smart callback
+ * will fire when the row is rendered and emit
+ * "e,pill,show". No extra wiring needed here. */
+ }
+ prev = it;
}
}
+
+ /* ------------------------------------------------------------------
+ * Pass 5: any items left in the indexes are orphans — desired set
+ * no longer contains them, so delete. Their itc->func.del takes
+ * care of freeing Enm_Item_Data; VPN rows have del=NULL because the
+ * module owns NM_VPN_Connection lifetime.
+ * ------------------------------------------------------------------ */
+ itr = eina_hash_iterator_data_new(eth_idx);
+ EINA_ITERATOR_FOREACH(itr, it) orphans = eina_list_append(orphans, it);
+ eina_iterator_free(itr);
+
+ itr = eina_hash_iterator_data_new(ap_idx);
+ EINA_ITERATOR_FOREACH(itr, it) orphans = eina_list_append(orphans, it);
+ eina_iterator_free(itr);
+
+ itr = eina_hash_iterator_data_new(vpn_idx);
+ EINA_ITERATOR_FOREACH(itr, it) orphans = eina_list_append(orphans, it);
+ eina_iterator_free(itr);
+
+ EINA_LIST_FREE(orphans, it)
+ {
+ _enm_drop_group_if(inst, it); /* defensive — shouldn't be a group */
+ /* If the row being deleted is the pending deselect target,
+ * cancel the timer so it doesn't fire on a dangling pointer. */
+ if (inst->ui.popup.deselect_item == it)
+ {
+ E_FREE_FUNC(inst->ui.popup.deselect_timer, ecore_timer_del);
+ inst->ui.popup.deselect_item = NULL;
+ }
+ elm_object_item_del(it);
+ }
+
+ eina_hash_free(eth_idx);
+ eina_hash_free(ap_idx);
+ eina_hash_free(vpn_idx);
}
/* Wrap content in an elm_table with a transparent sizer rect so the whole
diff --git a/src/modules/networkmanager/e_mod_main.h b/src/modules/networkmanager/e_mod_main.h
index 8408f4c46..1a0ca106c 100644
--- a/src/modules/networkmanager/e_mod_main.h
+++ b/src/modules/networkmanager/e_mod_main.h
@@ -45,6 +45,11 @@ struct E_NM_Instance
Elm_Genlist_Item_Class *itc_vpn; /* VPN connection row */
Ecore_Timer *deselect_timer;
Elm_Object_Item *deselect_item;
+ /* Section-header items, tracked across incremental updates so we
+ * can lazily create/delete them without touching surrounding rows. */
+ Elm_Object_Item *group_eth;
+ Elm_Object_Item *group_wifi;
+ Elm_Object_Item *group_vpn;
} popup;
} ui;
};
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.