popup: disconnect, forget, hidden + signal bars + agent cancel hook
Adds the long-missing user-visible affordances:
- Disconnect button (visible while connected)
- Per-row Forget (✕) button on known networks
- Hidden... button + wifi_hidden_prompt → Station.ConnectHiddenNetwork,
with one-shot passphrase pre-arming so the agent answers iwd
automatically without re-prompting.
- Signal-tier bars in network rows; sort prefers stronger signals
within the same known/unknown class.
- iwd Agent.Cancel now tears down any open auth dialog (cancel
handler installed at module init via the new manager hook).
wifi_auth_prompt now returns the popup widget so the cancel path can
dismiss it externally.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7c2ea76c63
commit
1cd214a1f5
2 changed files with 162 additions and 7 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
#include "iwd/iwd_network.h"
|
#include "iwd/iwd_network.h"
|
||||||
#include "iwd/iwd_agent.h"
|
#include "iwd/iwd_agent.h"
|
||||||
#include "ui/wifi_auth.h"
|
#include "ui/wifi_auth.h"
|
||||||
|
#include "ui/wifi_hidden.h"
|
||||||
#include <e_gadcon_popup.h>
|
#include <e_gadcon_popup.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
@ -17,6 +18,9 @@ typedef struct _Popup
|
||||||
Evas_Object *list;
|
Evas_Object *list;
|
||||||
Evas_Object *btn_scan;
|
Evas_Object *btn_scan;
|
||||||
Evas_Object *btn_toggle;
|
Evas_Object *btn_toggle;
|
||||||
|
Evas_Object *btn_hidden;
|
||||||
|
Evas_Object *btn_disconnect; /* shown only when connected */
|
||||||
|
Evas_Object *action_row;
|
||||||
Eina_Bool listening;
|
Eina_Bool listening;
|
||||||
} Popup;
|
} Popup;
|
||||||
|
|
||||||
|
|
@ -24,6 +28,10 @@ static Popup *_popup = NULL;
|
||||||
|
|
||||||
/* Pending passphrase request from the agent — only one at a time. */
|
/* Pending passphrase request from the agent — only one at a time. */
|
||||||
static Iwd_Agent_Request *_pending_req = NULL;
|
static Iwd_Agent_Request *_pending_req = NULL;
|
||||||
|
/* Tracked so iwd's Cancel(reason) can tear down the dialog. */
|
||||||
|
static Evas_Object *_pending_dialog = NULL;
|
||||||
|
/* One-shot passphrase pre-armed by the hidden-network dialog. */
|
||||||
|
static char *_hidden_pending_pass = NULL;
|
||||||
|
|
||||||
/* ----- helpers --------------------------------------------------------- */
|
/* ----- helpers --------------------------------------------------------- */
|
||||||
|
|
||||||
|
|
@ -61,11 +69,47 @@ _net_cmp(const void *a, const void *b)
|
||||||
if (na->connected != nb->connected) return nb->connected - na->connected;
|
if (na->connected != nb->connected) return nb->connected - na->connected;
|
||||||
if (na->known_path && !nb->known_path) return -1;
|
if (na->known_path && !nb->known_path) return -1;
|
||||||
if (!na->known_path && nb->known_path) return 1;
|
if (!na->known_path && nb->known_path) return 1;
|
||||||
|
/* Higher signal first within same class. */
|
||||||
|
int ta = iwd_network_signal_tier(na);
|
||||||
|
int tb = iwd_network_signal_tier(nb);
|
||||||
|
if (ta != tb) return tb - ta;
|
||||||
if (!na->ssid) return 1;
|
if (!na->ssid) return 1;
|
||||||
if (!nb->ssid) return -1;
|
if (!nb->ssid) return -1;
|
||||||
return strcasecmp(na->ssid, nb->ssid);
|
return strcasecmp(na->ssid, nb->ssid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *
|
||||||
|
_signal_bars(int tier)
|
||||||
|
{
|
||||||
|
switch (tier)
|
||||||
|
{
|
||||||
|
case 4: return "▂▄▆█";
|
||||||
|
case 3: return "▂▄▆ ";
|
||||||
|
case 2: return "▂▄ ";
|
||||||
|
case 1: return "▂ ";
|
||||||
|
default: return " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First device that has a station; used for "Disconnect" and hidden connect. */
|
||||||
|
static Iwd_Device *
|
||||||
|
_active_device(void)
|
||||||
|
{
|
||||||
|
if (!e_iwd || !e_iwd->manager) return NULL;
|
||||||
|
const Eina_Hash *h = iwd_manager_devices(e_iwd->manager);
|
||||||
|
if (!h) return NULL;
|
||||||
|
Eina_Iterator *it = eina_hash_iterator_data_new((Eina_Hash *)h);
|
||||||
|
Iwd_Device *d, *best = NULL;
|
||||||
|
EINA_ITERATOR_FOREACH(it, d)
|
||||||
|
{
|
||||||
|
if (!d->has_station) continue;
|
||||||
|
if (!best) best = d;
|
||||||
|
if (d->connected_network) { best = d; break; }
|
||||||
|
}
|
||||||
|
eina_iterator_free(it);
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/* ----- list rendering -------------------------------------------------- */
|
/* ----- list rendering -------------------------------------------------- */
|
||||||
|
|
||||||
static void
|
static void
|
||||||
|
|
@ -76,6 +120,14 @@ _on_net_clicked(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED)
|
||||||
iwd_network_connect(n);
|
iwd_network_connect(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
_on_net_forget(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED)
|
||||||
|
{
|
||||||
|
Iwd_Network *n = data;
|
||||||
|
if (!n) return;
|
||||||
|
iwd_network_forget(n);
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
_rebuild_list(Popup *p)
|
_rebuild_list(Popup *p)
|
||||||
{
|
{
|
||||||
|
|
@ -99,19 +151,48 @@ _rebuild_list(Popup *p)
|
||||||
Eina_List *l;
|
Eina_List *l;
|
||||||
EINA_LIST_FOREACH(items, l, n)
|
EINA_LIST_FOREACH(items, l, n)
|
||||||
{
|
{
|
||||||
Evas_Object *btn = elm_button_add(p->list);
|
Evas_Object *row = elm_box_add(p->list);
|
||||||
|
elm_box_horizontal_set(row, EINA_TRUE);
|
||||||
|
elm_box_padding_set(row, 4, 0);
|
||||||
|
evas_object_size_hint_weight_set(row, EVAS_HINT_EXPAND, 0);
|
||||||
|
evas_object_size_hint_align_set(row, EVAS_HINT_FILL, 0);
|
||||||
|
|
||||||
|
Evas_Object *btn = elm_button_add(row);
|
||||||
|
/* Truncate long SSIDs so the row never forces horizontal scrolling. */
|
||||||
|
const char *raw_ssid = n->ssid ? n->ssid : "(hidden)";
|
||||||
|
char ssid_buf[32];
|
||||||
|
const int max_ssid = 22;
|
||||||
|
if ((int)strlen(raw_ssid) > max_ssid)
|
||||||
|
{
|
||||||
|
snprintf(ssid_buf, sizeof(ssid_buf), "%.*s…", max_ssid - 1, raw_ssid);
|
||||||
|
raw_ssid = ssid_buf;
|
||||||
|
}
|
||||||
char label[256];
|
char label[256];
|
||||||
snprintf(label, sizeof(label), "%s%s [%s]%s",
|
snprintf(label, sizeof(label), "%s %s%s [%s]%s",
|
||||||
|
_signal_bars(iwd_network_signal_tier(n)),
|
||||||
n->known_path ? "★ " : " ",
|
n->known_path ? "★ " : " ",
|
||||||
n->ssid ? n->ssid : "(hidden)",
|
raw_ssid,
|
||||||
_sec_label(n->security),
|
_sec_label(n->security),
|
||||||
n->connected ? " ✔" : "");
|
n->connected ? " ✔" : "");
|
||||||
elm_object_text_set(btn, label);
|
elm_object_text_set(btn, label);
|
||||||
evas_object_size_hint_weight_set(btn, EVAS_HINT_EXPAND, 0);
|
evas_object_size_hint_weight_set(btn, EVAS_HINT_EXPAND, 0);
|
||||||
evas_object_size_hint_align_set(btn, EVAS_HINT_FILL, 0);
|
evas_object_size_hint_align_set(btn, EVAS_HINT_FILL, 0);
|
||||||
evas_object_smart_callback_add(btn, "clicked", _on_net_clicked, n);
|
evas_object_smart_callback_add(btn, "clicked", _on_net_clicked, n);
|
||||||
elm_box_pack_end(p->list, btn);
|
elm_box_pack_end(row, btn);
|
||||||
evas_object_show(btn);
|
evas_object_show(btn);
|
||||||
|
|
||||||
|
if (n->known_path)
|
||||||
|
{
|
||||||
|
Evas_Object *fb = elm_button_add(row);
|
||||||
|
elm_object_text_set(fb, "✕");
|
||||||
|
elm_object_tooltip_text_set(fb, "Forget network");
|
||||||
|
evas_object_smart_callback_add(fb, "clicked", _on_net_forget, n);
|
||||||
|
elm_box_pack_end(row, fb);
|
||||||
|
evas_object_show(fb);
|
||||||
|
}
|
||||||
|
|
||||||
|
elm_box_pack_end(p->list, row);
|
||||||
|
evas_object_show(row);
|
||||||
}
|
}
|
||||||
eina_list_free(items);
|
eina_list_free(items);
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +208,14 @@ _refresh(Popup *p)
|
||||||
elm_object_text_set(p->btn_toggle, s == IWD_STATE_OFF ? "Enable" : "Disable");
|
elm_object_text_set(p->btn_toggle, s == IWD_STATE_OFF ? "Enable" : "Disable");
|
||||||
if (p->btn_scan)
|
if (p->btn_scan)
|
||||||
elm_object_disabled_set(p->btn_scan, s == IWD_STATE_OFF);
|
elm_object_disabled_set(p->btn_scan, s == IWD_STATE_OFF);
|
||||||
|
if (p->btn_hidden)
|
||||||
|
elm_object_disabled_set(p->btn_hidden, s == IWD_STATE_OFF);
|
||||||
|
if (p->btn_disconnect)
|
||||||
|
{
|
||||||
|
Eina_Bool show = (s == IWD_STATE_CONNECTED);
|
||||||
|
if (show) evas_object_show(p->btn_disconnect);
|
||||||
|
else evas_object_hide(p->btn_disconnect);
|
||||||
|
}
|
||||||
_rebuild_list(p);
|
_rebuild_list(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,30 +237,81 @@ static void _on_toggle(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e
|
||||||
Eina_Bool off = (iwd_manager_state(e_iwd->manager) == IWD_STATE_OFF);
|
Eina_Bool off = (iwd_manager_state(e_iwd->manager) == IWD_STATE_OFF);
|
||||||
iwd_manager_set_powered(e_iwd->manager, off);
|
iwd_manager_set_powered(e_iwd->manager, off);
|
||||||
}
|
}
|
||||||
|
static void _on_disconnect(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED)
|
||||||
|
{
|
||||||
|
Iwd_Device *dev = _active_device();
|
||||||
|
if (dev) iwd_device_disconnect(dev);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
_on_hidden_done(void *data EINA_UNUSED, const char *ssid, const char *pass, Eina_Bool ok)
|
||||||
|
{
|
||||||
|
if (!ok || !ssid || !*ssid) return;
|
||||||
|
Iwd_Device *dev = _active_device();
|
||||||
|
if (!dev) return;
|
||||||
|
/* Pre-arm the agent reply so the next RequestPassphrase from iwd is
|
||||||
|
* answered automatically. If the network turns out to be open, the
|
||||||
|
* stashed passphrase is simply never consumed. */
|
||||||
|
if (pass && *pass) _hidden_pending_pass = strdup(pass);
|
||||||
|
iwd_device_connect_hidden(dev, ssid);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _on_hidden(void *d EINA_UNUSED, Evas_Object *o EINA_UNUSED, void *e EINA_UNUSED)
|
||||||
|
{
|
||||||
|
wifi_hidden_prompt(_popup ? _popup->box : e_comp->elm, _on_hidden_done, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
/* ----- passphrase plumbing -------------------------------------------- */
|
/* ----- passphrase plumbing -------------------------------------------- */
|
||||||
|
|
||||||
static void
|
static void
|
||||||
_on_auth_done(void *data EINA_UNUSED, const char *pass, Eina_Bool ok)
|
_on_auth_done(void *data EINA_UNUSED, const char *pass, Eina_Bool ok)
|
||||||
{
|
{
|
||||||
|
_pending_dialog = NULL;
|
||||||
if (!_pending_req) return;
|
if (!_pending_req) return;
|
||||||
if (ok) iwd_agent_reply (_pending_req, pass ? pass : "");
|
if (ok) iwd_agent_reply (_pending_req, pass ? pass : "");
|
||||||
else iwd_agent_cancel(_pending_req);
|
else iwd_agent_cancel(_pending_req);
|
||||||
_pending_req = NULL;
|
_pending_req = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
_on_agent_cancel(void *data EINA_UNUSED, const char *reason EINA_UNUSED)
|
||||||
|
{
|
||||||
|
/* iwd dropped the auth attempt — close any open dialog. The dialog's
|
||||||
|
* DEL handler will fire _on_auth_done(ok=FALSE), but _pending_req has
|
||||||
|
* already been consumed by iwd, so clear it first to avoid double-cancel. */
|
||||||
|
_pending_req = NULL;
|
||||||
|
if (_pending_dialog)
|
||||||
|
{
|
||||||
|
Evas_Object *d = _pending_dialog;
|
||||||
|
_pending_dialog = NULL;
|
||||||
|
evas_object_del(d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void _on_passphrase_request(void *data, Iwd_Agent_Request *req, const char *netpath);
|
static void _on_passphrase_request(void *data, Iwd_Agent_Request *req, const char *netpath);
|
||||||
|
|
||||||
void
|
void
|
||||||
e_iwd_popup_install_passphrase_handler(void)
|
e_iwd_popup_install_passphrase_handler(void)
|
||||||
{
|
{
|
||||||
if (e_iwd && e_iwd->manager)
|
if (e_iwd && e_iwd->manager)
|
||||||
iwd_manager_set_passphrase_handler(e_iwd->manager, _on_passphrase_request, NULL);
|
{
|
||||||
|
iwd_manager_set_passphrase_handler(e_iwd->manager, _on_passphrase_request, NULL);
|
||||||
|
iwd_manager_set_cancel_handler (e_iwd->manager, _on_agent_cancel, NULL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
_on_passphrase_request(void *data EINA_UNUSED, Iwd_Agent_Request *req, const char *netpath)
|
_on_passphrase_request(void *data EINA_UNUSED, Iwd_Agent_Request *req, const char *netpath)
|
||||||
{
|
{
|
||||||
|
/* If the user just kicked off a hidden-network connect with a passphrase,
|
||||||
|
* answer this request automatically without prompting. */
|
||||||
|
if (_hidden_pending_pass)
|
||||||
|
{
|
||||||
|
iwd_agent_reply(req, _hidden_pending_pass);
|
||||||
|
free(_hidden_pending_pass);
|
||||||
|
_hidden_pending_pass = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (_pending_req)
|
if (_pending_req)
|
||||||
{
|
{
|
||||||
iwd_agent_cancel(req);
|
iwd_agent_cancel(req);
|
||||||
|
|
@ -200,8 +340,8 @@ _on_passphrase_request(void *data EINA_UNUSED, Iwd_Agent_Request *req, const cha
|
||||||
if (n) sec = _sec_label(n->security);
|
if (n) sec = _sec_label(n->security);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
wifi_auth_prompt(_popup ? _popup->box : e_comp->elm, ssid, sec,
|
_pending_dialog = wifi_auth_prompt(_popup ? _popup->box : e_comp->elm,
|
||||||
_on_auth_done, NULL);
|
ssid, sec, _on_auth_done, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ----- popup lifecycle ------------------------------------------------- */
|
/* ----- popup lifecycle ------------------------------------------------- */
|
||||||
|
|
@ -264,6 +404,7 @@ e_iwd_popup_toggle(E_Gadcon_Client *gcc)
|
||||||
Evas_Object *row = elm_box_add(box);
|
Evas_Object *row = elm_box_add(box);
|
||||||
elm_box_horizontal_set(row, EINA_TRUE);
|
elm_box_horizontal_set(row, EINA_TRUE);
|
||||||
elm_box_padding_set(row, 4, 0);
|
elm_box_padding_set(row, 4, 0);
|
||||||
|
p->action_row = row;
|
||||||
|
|
||||||
p->btn_scan = elm_button_add(row);
|
p->btn_scan = elm_button_add(row);
|
||||||
elm_object_text_set(p->btn_scan, "Rescan");
|
elm_object_text_set(p->btn_scan, "Rescan");
|
||||||
|
|
@ -275,6 +416,17 @@ e_iwd_popup_toggle(E_Gadcon_Client *gcc)
|
||||||
evas_object_smart_callback_add(p->btn_toggle, "clicked", _on_toggle, NULL);
|
evas_object_smart_callback_add(p->btn_toggle, "clicked", _on_toggle, NULL);
|
||||||
elm_box_pack_end(row, p->btn_toggle); evas_object_show(p->btn_toggle);
|
elm_box_pack_end(row, p->btn_toggle); evas_object_show(p->btn_toggle);
|
||||||
|
|
||||||
|
p->btn_hidden = elm_button_add(row);
|
||||||
|
elm_object_text_set(p->btn_hidden, "Hidden…");
|
||||||
|
evas_object_smart_callback_add(p->btn_hidden, "clicked", _on_hidden, NULL);
|
||||||
|
elm_box_pack_end(row, p->btn_hidden); evas_object_show(p->btn_hidden);
|
||||||
|
|
||||||
|
p->btn_disconnect = elm_button_add(row);
|
||||||
|
elm_object_text_set(p->btn_disconnect, "Disconnect");
|
||||||
|
evas_object_smart_callback_add(p->btn_disconnect, "clicked", _on_disconnect, NULL);
|
||||||
|
elm_box_pack_end(row, p->btn_disconnect);
|
||||||
|
/* Visibility is driven by _refresh() based on connection state. */
|
||||||
|
|
||||||
elm_box_pack_end(box, row);
|
elm_box_pack_end(box, row);
|
||||||
evas_object_show(row);
|
evas_object_show(row);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ typedef void (*Wifi_Auth_Cb)(void *data, const char *passphrase, Eina_Bool ok);
|
||||||
* (e.g. "WPA", "WEP") shown alongside the SSID; pass NULL to omit it.
|
* (e.g. "WPA", "WEP") shown alongside the SSID; pass NULL to omit it.
|
||||||
* cb is called exactly once with ok=EINA_TRUE + passphrase, or
|
* cb is called exactly once with ok=EINA_TRUE + passphrase, or
|
||||||
* ok=EINA_FALSE on cancel. The dialog destroys itself. */
|
* ok=EINA_FALSE on cancel. The dialog destroys itself. */
|
||||||
|
/* Returns the popup widget so the caller can dismiss it externally
|
||||||
|
* (e.g. on Agent.Cancel from iwd). The widget self-deletes on user
|
||||||
|
* action; treat the returned pointer as a weak reference. */
|
||||||
Evas_Object *wifi_auth_prompt(Evas_Object *parent, const char *ssid,
|
Evas_Object *wifi_auth_prompt(Evas_Object *parent, const char *ssid,
|
||||||
const char *security,
|
const char *security,
|
||||||
Wifi_Auth_Cb cb, void *data);
|
Wifi_Auth_Cb cb, void *data);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue