diff --git a/src/e_mod_popup.c b/src/e_mod_popup.c index 99f7aaf..c987cf2 100644 --- a/src/e_mod_popup.c +++ b/src/e_mod_popup.c @@ -5,6 +5,7 @@ #include "iwd/iwd_network.h" #include "iwd/iwd_agent.h" #include "ui/wifi_auth.h" +#include "ui/wifi_hidden.h" #include #include #include @@ -17,6 +18,9 @@ typedef struct _Popup Evas_Object *list; Evas_Object *btn_scan; Evas_Object *btn_toggle; + Evas_Object *btn_hidden; + Evas_Object *btn_disconnect; /* shown only when connected */ + Evas_Object *action_row; Eina_Bool listening; } Popup; @@ -24,6 +28,10 @@ static Popup *_popup = NULL; /* Pending passphrase request from the agent — only one at a time. */ 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 --------------------------------------------------------- */ @@ -61,11 +69,47 @@ _net_cmp(const void *a, const void *b) 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; + /* 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 (!nb->ssid) return -1; 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 -------------------------------------------------- */ static void @@ -76,6 +120,14 @@ _on_net_clicked(void *data, Evas_Object *obj EINA_UNUSED, void *ev EINA_UNUSED) 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 _rebuild_list(Popup *p) { @@ -99,19 +151,48 @@ _rebuild_list(Popup *p) Eina_List *l; 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]; - 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->ssid ? n->ssid : "(hidden)", + raw_ssid, _sec_label(n->security), n->connected ? " ✔" : ""); elm_object_text_set(btn, label); evas_object_size_hint_weight_set(btn, EVAS_HINT_EXPAND, 0); evas_object_size_hint_align_set(btn, EVAS_HINT_FILL, 0); 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); + + 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); } @@ -127,6 +208,14 @@ _refresh(Popup *p) elm_object_text_set(p->btn_toggle, s == IWD_STATE_OFF ? "Enable" : "Disable"); if (p->btn_scan) 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); } @@ -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); 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 -------------------------------------------- */ static void _on_auth_done(void *data EINA_UNUSED, const char *pass, Eina_Bool ok) { + _pending_dialog = NULL; if (!_pending_req) return; if (ok) iwd_agent_reply (_pending_req, pass ? pass : ""); else iwd_agent_cancel(_pending_req); _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); void e_iwd_popup_install_passphrase_handler(void) { 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 _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) { 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); } } - wifi_auth_prompt(_popup ? _popup->box : e_comp->elm, ssid, sec, - _on_auth_done, NULL); + _pending_dialog = wifi_auth_prompt(_popup ? _popup->box : e_comp->elm, + ssid, sec, _on_auth_done, NULL); } /* ----- popup lifecycle ------------------------------------------------- */ @@ -264,6 +404,7 @@ e_iwd_popup_toggle(E_Gadcon_Client *gcc) Evas_Object *row = elm_box_add(box); elm_box_horizontal_set(row, EINA_TRUE); elm_box_padding_set(row, 4, 0); + p->action_row = row; p->btn_scan = elm_button_add(row); 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); 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); evas_object_show(row); diff --git a/src/ui/wifi_auth.h b/src/ui/wifi_auth.h index 684cdf2..79c6ee6 100644 --- a/src/ui/wifi_auth.h +++ b/src/ui/wifi_auth.h @@ -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. * cb is called exactly once with ok=EINA_TRUE + passphrase, or * 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, const char *security, Wifi_Auth_Cb cb, void *data);