Line |
Branch |
Exec |
Source |
1 |
|
|
/* |
2 |
|
|
* Copyright © 2018 Red Hat Inc. |
3 |
|
|
* |
4 |
|
|
* This program is free software; you can redistribute it and/or |
5 |
|
|
* modify it under the terms of the GNU General Public License as |
6 |
|
|
* published by the Free Software Foundation; either version 2 of the |
7 |
|
|
* License, or (at your option) any later version. |
8 |
|
|
* |
9 |
|
|
* This program is distributed in the hope that it will be useful, but |
10 |
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of |
11 |
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 |
|
|
* General Public License for more details. |
13 |
|
|
* |
14 |
|
|
* You should have received a copy of the GNU General Public License |
15 |
|
|
* along with this program; if not, see <http://www.gnu.org/licenses/>. |
16 |
|
|
*/ |
17 |
|
|
|
18 |
|
|
#include <glib/gi18n.h> |
19 |
|
|
|
20 |
|
|
#include "cc-wifi-connection-list.h" |
21 |
|
|
#include "cc-wifi-connection-row.h" |
22 |
|
|
|
23 |
|
|
struct _CcWifiConnectionList |
24 |
|
|
{ |
25 |
|
|
AdwBin parent_instance; |
26 |
|
|
|
27 |
|
|
GtkListBox *listbox; |
28 |
|
|
|
29 |
|
|
guint freeze_count; |
30 |
|
|
gboolean updating; |
31 |
|
|
|
32 |
|
|
gboolean checkable; |
33 |
|
|
gboolean forgettable; |
34 |
|
|
gboolean hide_unavailable; |
35 |
|
|
gboolean show_aps; |
36 |
|
|
|
37 |
|
|
NMClient *client; |
38 |
|
|
NMDeviceWifi *device; |
39 |
|
|
|
40 |
|
|
NMConnection *last_active; |
41 |
|
|
|
42 |
|
|
GPtrArray *connections; |
43 |
|
|
GPtrArray *connections_row; |
44 |
|
|
|
45 |
|
|
/* AP SSID cache stores the APs SSID used for assigning it to a row. |
46 |
|
|
* This is necessary to efficiently remove it when its SSID changes. |
47 |
|
|
* |
48 |
|
|
* Note that we only group APs that cannot be assigned to a connection |
49 |
|
|
* by the SSID. In principle this is wrong, because other attributes may |
50 |
|
|
* be different rendering them separate networks. |
51 |
|
|
* In practice this will almost never happen, and if it does, we just |
52 |
|
|
* show and select the strongest AP. |
53 |
|
|
*/ |
54 |
|
|
GHashTable *ap_ssid_cache; |
55 |
|
|
GHashTable *ssid_to_row; |
56 |
|
|
}; |
57 |
|
|
|
58 |
|
|
static void on_device_ap_added_cb (CcWifiConnectionList *self, |
59 |
|
|
NMAccessPoint *ap, |
60 |
|
|
NMDeviceWifi *device); |
61 |
|
|
static void on_device_ap_removed_cb (CcWifiConnectionList *self, |
62 |
|
|
NMAccessPoint *ap, |
63 |
|
|
NMDeviceWifi *device); |
64 |
|
|
static void on_row_configured_cb (CcWifiConnectionList *self, |
65 |
|
|
CcWifiConnectionRow *row); |
66 |
|
|
static void on_row_forget_cb (CcWifiConnectionList *self, |
67 |
|
|
CcWifiConnectionRow *row); |
68 |
|
|
static void on_row_show_qr_code_cb (CcWifiConnectionList *self, |
69 |
|
|
CcWifiConnectionRow *row); |
70 |
|
|
|
71 |
|
✗ |
G_DEFINE_TYPE (CcWifiConnectionList, cc_wifi_connection_list, ADW_TYPE_BIN) |
72 |
|
|
|
73 |
|
|
enum |
74 |
|
|
{ |
75 |
|
|
PROP_0, |
76 |
|
|
PROP_CHECKABLE, |
77 |
|
|
PROP_HIDE_UNAVAILABLE, |
78 |
|
|
PROP_SHOW_APS, |
79 |
|
|
PROP_CLIENT, |
80 |
|
|
PROP_DEVICE, |
81 |
|
|
PROP_FORGETTABLE, |
82 |
|
|
PROP_LAST |
83 |
|
|
}; |
84 |
|
|
|
85 |
|
|
static GParamSpec *props [PROP_LAST]; |
86 |
|
|
|
87 |
|
|
static GBytes* |
88 |
|
✗ |
new_hashable_ssid (GBytes *ssid) |
89 |
|
|
{ |
90 |
|
|
GBytes *res; |
91 |
|
|
const guint8 *data; |
92 |
|
|
gsize size; |
93 |
|
|
|
94 |
|
|
/* This is what nm_utils_same_ssid does, but returning it so that we can |
95 |
|
|
* use the result in other ways (i.e. hash table lookups). */ |
96 |
|
✗ |
data = g_bytes_get_data ((GBytes*) ssid, &size); |
97 |
|
✗ |
if (data[size-1] == '\0') |
98 |
|
✗ |
size -= 1; |
99 |
|
✗ |
res = g_bytes_new (data, size); |
100 |
|
|
|
101 |
|
✗ |
return res; |
102 |
|
|
} |
103 |
|
|
|
104 |
|
|
static gboolean |
105 |
|
✗ |
connection_ignored (NMConnection *connection) |
106 |
|
|
{ |
107 |
|
|
NMSettingWireless *sw; |
108 |
|
|
|
109 |
|
|
/* Ignore AP and adhoc modes (i.e. accept infrastructure or empty) */ |
110 |
|
✗ |
sw = nm_connection_get_setting_wireless (connection); |
111 |
|
✗ |
if (!sw) |
112 |
|
✗ |
return TRUE; |
113 |
|
✗ |
if (g_strcmp0 (nm_setting_wireless_get_mode (sw), "adhoc") == 0 || |
114 |
|
✗ |
g_strcmp0 (nm_setting_wireless_get_mode (sw), "ap") == 0) |
115 |
|
|
{ |
116 |
|
✗ |
return TRUE; |
117 |
|
|
} |
118 |
|
|
|
119 |
|
✗ |
return FALSE; |
120 |
|
|
} |
121 |
|
|
|
122 |
|
|
static CcWifiConnectionRow* |
123 |
|
✗ |
cc_wifi_connection_list_row_add (CcWifiConnectionList *self, |
124 |
|
|
NMConnection *connection, |
125 |
|
|
NMAccessPoint *ap, |
126 |
|
|
gboolean known_connection) |
127 |
|
|
{ |
128 |
|
|
CcWifiConnectionRow *res; |
129 |
|
✗ |
g_autoptr(GPtrArray) aps = NULL; |
130 |
|
|
|
131 |
|
✗ |
if (ap) |
132 |
|
|
{ |
133 |
|
✗ |
aps = g_ptr_array_new (); |
134 |
|
✗ |
g_ptr_array_add (aps, ap); |
135 |
|
|
} |
136 |
|
|
|
137 |
|
✗ |
res = cc_wifi_connection_row_new (self->device, |
138 |
|
|
connection, |
139 |
|
|
aps, |
140 |
|
|
self->checkable, |
141 |
|
|
known_connection, |
142 |
|
|
self->forgettable); |
143 |
|
✗ |
gtk_list_box_append (self->listbox, GTK_WIDGET (res)); |
144 |
|
|
|
145 |
|
✗ |
g_signal_connect_object (res, "configure", G_CALLBACK (on_row_configured_cb), self, G_CONNECT_SWAPPED); |
146 |
|
✗ |
g_signal_connect_object (res, "forget", G_CALLBACK (on_row_forget_cb), self, G_CONNECT_SWAPPED); |
147 |
|
✗ |
g_signal_connect_object (res, "show-qr-code", G_CALLBACK (on_row_show_qr_code_cb), self, G_CONNECT_SWAPPED); |
148 |
|
|
|
149 |
|
✗ |
g_signal_emit_by_name (self, "add-row", res); |
150 |
|
|
|
151 |
|
✗ |
return res; |
152 |
|
|
} |
153 |
|
|
|
154 |
|
|
static void |
155 |
|
✗ |
clear_widget (CcWifiConnectionList *self) |
156 |
|
|
{ |
157 |
|
|
const GPtrArray *aps; |
158 |
|
|
GHashTableIter iter; |
159 |
|
|
CcWifiConnectionRow *row; |
160 |
|
|
gint i; |
161 |
|
|
|
162 |
|
|
/* Clear everything; disconnect all AP signals first */ |
163 |
|
✗ |
aps = nm_device_wifi_get_access_points (self->device); |
164 |
|
✗ |
for (i = 0; i < aps->len; i++) |
165 |
|
✗ |
g_signal_handlers_disconnect_by_data (g_ptr_array_index (aps, i), self); |
166 |
|
|
|
167 |
|
|
/* Remove all AP only rows */ |
168 |
|
✗ |
g_hash_table_iter_init (&iter, self->ssid_to_row); |
169 |
|
✗ |
while (g_hash_table_iter_next (&iter, NULL, (gpointer*) &row)) |
170 |
|
|
{ |
171 |
|
✗ |
g_hash_table_iter_remove (&iter); |
172 |
|
✗ |
g_signal_emit_by_name (self, "remove-row", row); |
173 |
|
✗ |
gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); |
174 |
|
|
} |
175 |
|
|
|
176 |
|
|
/* Remove all connection rows */ |
177 |
|
✗ |
for (i = 0; i < self->connections_row->len; i++) |
178 |
|
|
{ |
179 |
|
✗ |
if (!g_ptr_array_index (self->connections_row, i)) |
180 |
|
✗ |
continue; |
181 |
|
|
|
182 |
|
✗ |
row = g_ptr_array_index (self->connections_row, i); |
183 |
|
✗ |
g_ptr_array_index (self->connections_row, i) = NULL; |
184 |
|
✗ |
g_signal_emit_by_name (self, "remove-row", row); |
185 |
|
✗ |
gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); |
186 |
|
|
} |
187 |
|
|
|
188 |
|
|
/* Reset the internal state */ |
189 |
|
✗ |
g_ptr_array_set_size (self->connections, 0); |
190 |
|
✗ |
g_ptr_array_set_size (self->connections_row, 0); |
191 |
|
✗ |
g_hash_table_remove_all (self->ssid_to_row); |
192 |
|
✗ |
g_hash_table_remove_all (self->ap_ssid_cache); |
193 |
|
✗ |
} |
194 |
|
|
|
195 |
|
|
static void |
196 |
|
✗ |
update_connections (CcWifiConnectionList *self) |
197 |
|
|
{ |
198 |
|
|
const GPtrArray *aps; |
199 |
|
|
const GPtrArray *acs_client; |
200 |
|
✗ |
g_autoptr(GPtrArray) acs = NULL; |
201 |
|
|
NMActiveConnection *ac; |
202 |
|
✗ |
NMConnection *ac_con = NULL; |
203 |
|
|
gint i; |
204 |
|
|
|
205 |
|
|
/* We don't want full UI rebuilds during some UI interactions, so allow freezing the list. */ |
206 |
|
✗ |
if (self->freeze_count > 0) |
207 |
|
✗ |
return; |
208 |
|
|
|
209 |
|
|
/* Prevent recursion (maybe move this into an idle handler instead?) */ |
210 |
|
✗ |
if (self->updating) |
211 |
|
✗ |
return; |
212 |
|
✗ |
self->updating = TRUE; |
213 |
|
|
|
214 |
|
✗ |
clear_widget (self); |
215 |
|
|
|
216 |
|
|
/* Copy the new connections; also create a row if we show unavailable |
217 |
|
|
* connections */ |
218 |
|
✗ |
acs_client = nm_client_get_connections (self->client); |
219 |
|
|
|
220 |
|
✗ |
acs = g_ptr_array_new_full (acs_client->len + 1, NULL); |
221 |
|
✗ |
for (i = 0; i < acs_client->len; i++) |
222 |
|
✗ |
g_ptr_array_add (acs, g_ptr_array_index (acs_client, i)); |
223 |
|
|
|
224 |
|
✗ |
ac = nm_device_get_active_connection (NM_DEVICE (self->device)); |
225 |
|
✗ |
if (ac) |
226 |
|
✗ |
ac_con = NM_CONNECTION (nm_active_connection_get_connection (ac)); |
227 |
|
|
|
228 |
|
✗ |
if (ac_con && !g_ptr_array_find (acs, ac_con, NULL)) |
229 |
|
|
{ |
230 |
|
✗ |
g_debug ("Adding remote connection for active connection"); |
231 |
|
✗ |
g_ptr_array_add (acs, g_object_ref (ac_con)); |
232 |
|
|
} |
233 |
|
|
|
234 |
|
✗ |
for (i = 0; i < acs->len; i++) |
235 |
|
|
{ |
236 |
|
|
NMConnection *con; |
237 |
|
|
|
238 |
|
✗ |
con = g_ptr_array_index (acs, i); |
239 |
|
✗ |
if (connection_ignored (con)) |
240 |
|
✗ |
continue; |
241 |
|
|
|
242 |
|
✗ |
g_ptr_array_add (self->connections, g_object_ref (con)); |
243 |
|
✗ |
if (self->hide_unavailable && con != ac_con) |
244 |
|
✗ |
g_ptr_array_add (self->connections_row, NULL); |
245 |
|
|
else |
246 |
|
✗ |
g_ptr_array_add (self->connections_row, |
247 |
|
✗ |
cc_wifi_connection_list_row_add (self, con, |
248 |
|
|
NULL, TRUE)); |
249 |
|
|
} |
250 |
|
|
|
251 |
|
|
/* Coldplug all known APs again */ |
252 |
|
✗ |
aps = nm_device_wifi_get_access_points (self->device); |
253 |
|
✗ |
for (i = 0; i < aps->len; i++) |
254 |
|
✗ |
on_device_ap_added_cb (self, g_ptr_array_index (aps, i), self->device); |
255 |
|
|
|
256 |
|
✗ |
self->updating = FALSE; |
257 |
|
|
} |
258 |
|
|
|
259 |
|
|
static void |
260 |
|
✗ |
on_row_configured_cb (CcWifiConnectionList *self, CcWifiConnectionRow *row) |
261 |
|
|
{ |
262 |
|
✗ |
g_signal_emit_by_name (self, "configure", row); |
263 |
|
✗ |
} |
264 |
|
|
|
265 |
|
|
static void |
266 |
|
✗ |
on_row_forget_cb (CcWifiConnectionList *self, CcWifiConnectionRow *row) |
267 |
|
|
{ |
268 |
|
✗ |
g_signal_emit_by_name (self, "forget", row); |
269 |
|
✗ |
} |
270 |
|
|
|
271 |
|
|
static void |
272 |
|
✗ |
on_row_show_qr_code_cb (CcWifiConnectionList *self, CcWifiConnectionRow *row) |
273 |
|
|
{ |
274 |
|
✗ |
g_signal_emit_by_name (self, "show_qr_code", row); |
275 |
|
✗ |
} |
276 |
|
|
|
277 |
|
|
static void |
278 |
|
✗ |
on_access_point_property_changed (CcWifiConnectionList *self, |
279 |
|
|
GParamSpec *pspec, |
280 |
|
|
NMAccessPoint *ap) |
281 |
|
|
{ |
282 |
|
|
CcWifiConnectionRow *row; |
283 |
|
|
GBytes *ssid; |
284 |
|
✗ |
gboolean has_connection = FALSE; |
285 |
|
|
gint i; |
286 |
|
|
|
287 |
|
|
/* If the SSID changed then the AP needs to be added/removed from rows. |
288 |
|
|
* Do this by simulating an AP addition/removal. */ |
289 |
|
✗ |
if (g_str_equal (pspec->name, NM_ACCESS_POINT_SSID)) |
290 |
|
|
{ |
291 |
|
✗ |
g_debug ("Simulating add/remove for SSID change"); |
292 |
|
✗ |
on_device_ap_removed_cb (self, ap, self->device); |
293 |
|
✗ |
on_device_ap_added_cb (self, ap, self->device); |
294 |
|
✗ |
return; |
295 |
|
|
} |
296 |
|
|
|
297 |
|
|
/* Otherwise, find all rows that contain the AP and update it. Do this by |
298 |
|
|
* first searching all rows with connections, and then looking it up in the |
299 |
|
|
* SSID rows if not found. */ |
300 |
|
✗ |
for (i = 0; i < self->connections_row->len; i++) |
301 |
|
|
{ |
302 |
|
✗ |
row = g_ptr_array_index (self->connections_row, i); |
303 |
|
✗ |
if (row && cc_wifi_connection_row_has_access_point (row, ap)) |
304 |
|
|
{ |
305 |
|
✗ |
cc_wifi_connection_row_update (row); |
306 |
|
✗ |
has_connection = TRUE; |
307 |
|
|
} |
308 |
|
|
} |
309 |
|
|
|
310 |
|
✗ |
if (!self->show_aps || has_connection) |
311 |
|
✗ |
return; |
312 |
|
|
|
313 |
|
✗ |
ssid = g_hash_table_lookup (self->ap_ssid_cache, ap); |
314 |
|
✗ |
if (!ssid) |
315 |
|
✗ |
return; |
316 |
|
|
|
317 |
|
✗ |
row = g_hash_table_lookup (self->ssid_to_row, ssid); |
318 |
|
✗ |
if (!row) |
319 |
|
✗ |
g_assert_not_reached (); |
320 |
|
|
else |
321 |
|
✗ |
cc_wifi_connection_row_update (row); |
322 |
|
|
} |
323 |
|
|
|
324 |
|
|
static void |
325 |
|
✗ |
on_device_ap_added_cb (CcWifiConnectionList *self, |
326 |
|
|
NMAccessPoint *ap, |
327 |
|
|
NMDeviceWifi *device) |
328 |
|
|
{ |
329 |
|
✗ |
g_autoptr(GPtrArray) connections = NULL; |
330 |
|
|
NM80211ApSecurityFlags rsn_flags; |
331 |
|
|
CcWifiConnectionRow *row; |
332 |
|
|
GBytes *ap_ssid; |
333 |
|
✗ |
g_autoptr(GBytes) ssid = NULL; |
334 |
|
|
guint i, j; |
335 |
|
|
|
336 |
|
✗ |
g_signal_connect_object (ap, "notify", |
337 |
|
|
G_CALLBACK (on_access_point_property_changed), |
338 |
|
|
self, G_CONNECT_SWAPPED); |
339 |
|
|
|
340 |
|
✗ |
connections = nm_access_point_filter_connections (ap, self->connections); |
341 |
|
|
|
342 |
|
|
/* If this is the active AP, then add the active connection to the list. This |
343 |
|
|
* is a workaround because nm_access_pointer_filter_connections() will not |
344 |
|
|
* include it otherwise. |
345 |
|
|
* So it seems like the dummy AP entry that NM creates internally is not actually |
346 |
|
|
* compatible with the connection that is being activated. |
347 |
|
|
*/ |
348 |
|
✗ |
if (ap == nm_device_wifi_get_active_access_point (device)) |
349 |
|
|
{ |
350 |
|
|
NMActiveConnection *ac; |
351 |
|
|
NMConnection *ac_con; |
352 |
|
|
|
353 |
|
✗ |
ac = nm_device_get_active_connection (NM_DEVICE (self->device)); |
354 |
|
|
|
355 |
|
✗ |
if (ac) |
356 |
|
|
{ |
357 |
|
|
guint idx; |
358 |
|
|
|
359 |
|
✗ |
ac_con = NM_CONNECTION (nm_active_connection_get_connection (ac)); |
360 |
|
|
|
361 |
|
✗ |
if (!g_ptr_array_find (connections, ac_con, NULL) && |
362 |
|
✗ |
g_ptr_array_find (self->connections, ac_con, &idx)) |
363 |
|
|
{ |
364 |
|
✗ |
g_debug ("Adding active connection to list of valid connections for AP"); |
365 |
|
✗ |
g_ptr_array_add (connections, g_object_ref (ac_con)); |
366 |
|
|
} |
367 |
|
|
} |
368 |
|
|
} |
369 |
|
|
|
370 |
|
|
/* Add the AP to all connection related rows, creating the row if neccessary. */ |
371 |
|
✗ |
for (i = 0; i < connections->len; i++) |
372 |
|
|
{ |
373 |
|
✗ |
gboolean found = g_ptr_array_find (self->connections, g_ptr_array_index (connections, i), &j); |
374 |
|
|
|
375 |
|
✗ |
g_assert (found); |
376 |
|
|
|
377 |
|
✗ |
row = g_ptr_array_index (self->connections_row, j); |
378 |
|
✗ |
if (!row) |
379 |
|
✗ |
row = cc_wifi_connection_list_row_add (self, g_ptr_array_index (connections, i), NULL, TRUE); |
380 |
|
✗ |
cc_wifi_connection_row_add_access_point (row, ap); |
381 |
|
✗ |
g_ptr_array_index (self->connections_row, j) = row; |
382 |
|
|
} |
383 |
|
|
|
384 |
|
✗ |
if (connections->len > 0) |
385 |
|
✗ |
return; |
386 |
|
|
|
387 |
|
✗ |
if (!self->show_aps) |
388 |
|
✗ |
return; |
389 |
|
|
|
390 |
|
|
/* The AP is not compatible to any known connection, generate an entry for the |
391 |
|
|
* SSID or add to existing one. However, not for hidden APs that don't have an SSID |
392 |
|
|
* or a hidden OWE transition network. |
393 |
|
|
*/ |
394 |
|
✗ |
ap_ssid = nm_access_point_get_ssid (ap); |
395 |
|
✗ |
if (ap_ssid == NULL) |
396 |
|
✗ |
return; |
397 |
|
|
|
398 |
|
|
/* Skip OWE-TM network with OWE RSN */ |
399 |
|
✗ |
rsn_flags = nm_access_point_get_rsn_flags (ap); |
400 |
|
✗ |
if (rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_OWE && rsn_flags & NM_802_11_AP_SEC_KEY_MGMT_OWE_TM) |
401 |
|
✗ |
return; |
402 |
|
|
|
403 |
|
✗ |
ssid = new_hashable_ssid (ap_ssid); |
404 |
|
|
|
405 |
|
✗ |
g_hash_table_insert (self->ap_ssid_cache, ap, g_bytes_ref (ssid)); |
406 |
|
|
|
407 |
|
✗ |
row = g_hash_table_lookup (self->ssid_to_row, ssid); |
408 |
|
✗ |
if (!row) |
409 |
|
|
{ |
410 |
|
✗ |
row = cc_wifi_connection_list_row_add (self, NULL, ap, FALSE); |
411 |
|
|
|
412 |
|
✗ |
g_hash_table_insert (self->ssid_to_row, g_bytes_ref (ssid), row); |
413 |
|
|
} |
414 |
|
|
else |
415 |
|
|
{ |
416 |
|
✗ |
cc_wifi_connection_row_add_access_point (row, ap); |
417 |
|
|
} |
418 |
|
|
} |
419 |
|
|
|
420 |
|
|
static void |
421 |
|
✗ |
on_device_ap_removed_cb (CcWifiConnectionList *self, |
422 |
|
|
NMAccessPoint *ap, |
423 |
|
|
NMDeviceWifi *device) |
424 |
|
|
{ |
425 |
|
|
CcWifiConnectionRow *row; |
426 |
|
✗ |
g_autoptr(GBytes) ssid = NULL; |
427 |
|
✗ |
gboolean found = FALSE; |
428 |
|
|
gint i; |
429 |
|
|
|
430 |
|
✗ |
g_signal_handlers_disconnect_by_data (ap, self); |
431 |
|
|
|
432 |
|
|
/* Find any connection related row with the AP and remove the AP from it. Remove the |
433 |
|
|
* row if it was the last AP and we are hiding unavailable connections. */ |
434 |
|
✗ |
for (i = 0; i < self->connections_row->len; i++) |
435 |
|
|
{ |
436 |
|
✗ |
row = g_ptr_array_index (self->connections_row, i); |
437 |
|
✗ |
if (row && cc_wifi_connection_row_remove_access_point (row, ap)) |
438 |
|
|
{ |
439 |
|
✗ |
found = TRUE; |
440 |
|
|
|
441 |
|
✗ |
if (self->hide_unavailable) |
442 |
|
|
{ |
443 |
|
✗ |
g_ptr_array_index (self->connections_row, i) = NULL; |
444 |
|
✗ |
g_signal_emit_by_name (self, "remove-row", row); |
445 |
|
✗ |
gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); |
446 |
|
|
} |
447 |
|
|
} |
448 |
|
|
} |
449 |
|
|
|
450 |
|
✗ |
if (found || !self->show_aps) |
451 |
|
✗ |
return; |
452 |
|
|
|
453 |
|
|
/* If the AP was inserted into a row without a connection, then we will get an |
454 |
|
|
* SSID for it here. */ |
455 |
|
✗ |
g_hash_table_steal_extended (self->ap_ssid_cache, ap, NULL, (gpointer*) &ssid); |
456 |
|
✗ |
if (!ssid) |
457 |
|
✗ |
return; |
458 |
|
|
|
459 |
|
|
/* And we can update the row (possibly removing it) */ |
460 |
|
✗ |
row = g_hash_table_lookup (self->ssid_to_row, ssid); |
461 |
|
✗ |
g_assert (row != NULL); |
462 |
|
|
|
463 |
|
✗ |
if (cc_wifi_connection_row_remove_access_point (row, ap)) |
464 |
|
|
{ |
465 |
|
✗ |
g_hash_table_remove (self->ssid_to_row, ssid); |
466 |
|
✗ |
g_signal_emit_by_name (self, "remove-row", row); |
467 |
|
✗ |
gtk_list_box_remove (self->listbox, GTK_WIDGET (row)); |
468 |
|
|
} |
469 |
|
|
} |
470 |
|
|
|
471 |
|
|
static void |
472 |
|
✗ |
on_client_connection_added_cb (CcWifiConnectionList *self, |
473 |
|
|
NMConnection *connection, |
474 |
|
|
NMClient *client) |
475 |
|
|
{ |
476 |
|
✗ |
if (!nm_device_connection_compatible (NM_DEVICE (self->device), connection, NULL)) |
477 |
|
✗ |
return; |
478 |
|
|
|
479 |
|
✗ |
if (connection_ignored (connection)) |
480 |
|
✗ |
return; |
481 |
|
|
|
482 |
|
|
/* The approach we take to handle connection changes is to do a full rebuild. |
483 |
|
|
* It happens seldom enough to make this feasible. |
484 |
|
|
*/ |
485 |
|
✗ |
update_connections (self); |
486 |
|
|
} |
487 |
|
|
|
488 |
|
|
static void |
489 |
|
✗ |
on_client_connection_removed_cb (CcWifiConnectionList *self, |
490 |
|
|
NMConnection *connection, |
491 |
|
|
NMClient *client) |
492 |
|
|
{ |
493 |
|
✗ |
if (!g_ptr_array_find (self->connections, connection, NULL)) |
494 |
|
✗ |
return; |
495 |
|
|
|
496 |
|
|
/* The approach we take to handle connection changes is to do a full rebuild. |
497 |
|
|
* It happens seldom enough to make this feasible. |
498 |
|
|
*/ |
499 |
|
✗ |
update_connections (self); |
500 |
|
|
} |
501 |
|
|
|
502 |
|
|
static void |
503 |
|
✗ |
on_device_state_changed_cb (CcWifiConnectionList *self, |
504 |
|
|
GParamSpec *pspec, |
505 |
|
|
NMDeviceWifi *device) |
506 |
|
|
{ |
507 |
|
|
NMActiveConnection *ac; |
508 |
|
✗ |
NMConnection *connection = NULL; |
509 |
|
|
guint idx; |
510 |
|
|
|
511 |
|
✗ |
ac = nm_device_get_active_connection (NM_DEVICE (self->device)); |
512 |
|
✗ |
if (ac) |
513 |
|
✗ |
connection = NM_CONNECTION (nm_active_connection_get_connection (ac)); |
514 |
|
|
|
515 |
|
|
/* Just update the corresponding row if the AC is still the same. */ |
516 |
|
✗ |
if (self->last_active == connection && |
517 |
|
✗ |
g_ptr_array_find (self->connections, connection, &idx) && |
518 |
|
✗ |
g_ptr_array_index (self->connections_row, idx)) |
519 |
|
|
{ |
520 |
|
✗ |
cc_wifi_connection_row_update (g_ptr_array_index (self->connections_row, idx)); |
521 |
|
✗ |
return; |
522 |
|
|
} |
523 |
|
|
|
524 |
|
|
/* Give up and do a full update. */ |
525 |
|
✗ |
update_connections (self); |
526 |
|
✗ |
self->last_active = connection; |
527 |
|
|
} |
528 |
|
|
|
529 |
|
|
static void |
530 |
|
✗ |
on_device_active_ap_changed_cb (CcWifiConnectionList *self, |
531 |
|
|
GParamSpec *pspec, |
532 |
|
|
NMDeviceWifi *device) |
533 |
|
|
{ |
534 |
|
|
NMAccessPoint *ap; |
535 |
|
|
/* We need to make sure the active AP is grouped with the active connection. |
536 |
|
|
* Do so by simply removing and adding it. |
537 |
|
|
* |
538 |
|
|
* This is necessary because the AP is added before this property |
539 |
|
|
* is updated. */ |
540 |
|
✗ |
ap = nm_device_wifi_get_active_access_point (self->device); |
541 |
|
✗ |
if (ap) |
542 |
|
|
{ |
543 |
|
✗ |
g_debug ("Simulating add/remove for active AP change"); |
544 |
|
✗ |
on_device_ap_removed_cb (self, ap, self->device); |
545 |
|
✗ |
on_device_ap_added_cb (self, ap, self->device); |
546 |
|
|
} |
547 |
|
✗ |
} |
548 |
|
|
|
549 |
|
|
static void |
550 |
|
✗ |
cc_wifi_connection_list_dispose (GObject *object) |
551 |
|
|
{ |
552 |
|
✗ |
CcWifiConnectionList *self = (CcWifiConnectionList *)object; |
553 |
|
|
|
554 |
|
|
/* Prevent any further updates; clear_widget must not indirectly recurse |
555 |
|
|
* through updates_connections */ |
556 |
|
✗ |
self->updating = TRUE; |
557 |
|
|
|
558 |
|
|
/* Drop all external references */ |
559 |
|
✗ |
clear_widget (self); |
560 |
|
|
|
561 |
|
✗ |
G_OBJECT_CLASS (cc_wifi_connection_list_parent_class)->dispose (object); |
562 |
|
✗ |
} |
563 |
|
|
|
564 |
|
|
static void |
565 |
|
✗ |
cc_wifi_connection_list_finalize (GObject *object) |
566 |
|
|
{ |
567 |
|
✗ |
CcWifiConnectionList *self = (CcWifiConnectionList *)object; |
568 |
|
|
|
569 |
|
✗ |
g_clear_object (&self->client); |
570 |
|
✗ |
g_clear_object (&self->device); |
571 |
|
|
|
572 |
|
✗ |
g_clear_pointer (&self->connections, g_ptr_array_unref); |
573 |
|
✗ |
g_clear_pointer (&self->connections_row, g_ptr_array_unref); |
574 |
|
✗ |
g_clear_pointer (&self->ssid_to_row, g_hash_table_unref); |
575 |
|
✗ |
g_clear_pointer (&self->ap_ssid_cache, g_hash_table_unref); |
576 |
|
|
|
577 |
|
✗ |
G_OBJECT_CLASS (cc_wifi_connection_list_parent_class)->finalize (object); |
578 |
|
✗ |
} |
579 |
|
|
|
580 |
|
|
static void |
581 |
|
✗ |
cc_wifi_connection_list_constructed (GObject *object) |
582 |
|
|
{ |
583 |
|
✗ |
CcWifiConnectionList *self = (CcWifiConnectionList *)object; |
584 |
|
|
|
585 |
|
✗ |
G_OBJECT_CLASS (cc_wifi_connection_list_parent_class)->constructed (object); |
586 |
|
|
|
587 |
|
✗ |
g_assert (self->client); |
588 |
|
✗ |
g_assert (self->device); |
589 |
|
|
|
590 |
|
✗ |
g_signal_connect_object (self->client, "connection-added", |
591 |
|
|
G_CALLBACK (on_client_connection_added_cb), |
592 |
|
|
self, G_CONNECT_SWAPPED); |
593 |
|
✗ |
g_signal_connect_object (self->client, "connection-removed", |
594 |
|
|
G_CALLBACK (on_client_connection_removed_cb), |
595 |
|
|
self, G_CONNECT_SWAPPED); |
596 |
|
|
|
597 |
|
✗ |
g_signal_connect_object (self->device, "access-point-added", |
598 |
|
|
G_CALLBACK (on_device_ap_added_cb), |
599 |
|
|
self, G_CONNECT_SWAPPED); |
600 |
|
✗ |
g_signal_connect_object (self->device, "access-point-removed", |
601 |
|
|
G_CALLBACK (on_device_ap_removed_cb), |
602 |
|
|
self, G_CONNECT_SWAPPED); |
603 |
|
|
|
604 |
|
✗ |
g_signal_connect_object (self->device, "notify::state", |
605 |
|
|
G_CALLBACK (on_device_state_changed_cb), |
606 |
|
|
self, G_CONNECT_SWAPPED); |
607 |
|
✗ |
g_signal_connect_object (self->device, "notify::active-connection", |
608 |
|
|
G_CALLBACK (on_device_state_changed_cb), |
609 |
|
|
self, G_CONNECT_SWAPPED); |
610 |
|
✗ |
g_signal_connect_object (self->device, "notify::active-access-point", |
611 |
|
|
G_CALLBACK (on_device_active_ap_changed_cb), |
612 |
|
|
self, G_CONNECT_SWAPPED); |
613 |
|
✗ |
on_device_state_changed_cb (self, NULL, self->device); |
614 |
|
|
|
615 |
|
|
/* Simulate a change notification on the available connections. |
616 |
|
|
* This uses the implementation detail that the list is rebuild |
617 |
|
|
* completely in this case. */ |
618 |
|
✗ |
update_connections (self); |
619 |
|
✗ |
} |
620 |
|
|
|
621 |
|
|
static void |
622 |
|
✗ |
cc_wifi_connection_list_get_property (GObject *object, |
623 |
|
|
guint prop_id, |
624 |
|
|
GValue *value, |
625 |
|
|
GParamSpec *pspec) |
626 |
|
|
{ |
627 |
|
✗ |
CcWifiConnectionList *self = CC_WIFI_CONNECTION_LIST (object); |
628 |
|
|
|
629 |
|
✗ |
switch (prop_id) |
630 |
|
|
{ |
631 |
|
✗ |
case PROP_CHECKABLE: |
632 |
|
✗ |
g_value_set_boolean (value, self->checkable); |
633 |
|
✗ |
break; |
634 |
|
|
|
635 |
|
✗ |
case PROP_HIDE_UNAVAILABLE: |
636 |
|
✗ |
g_value_set_boolean (value, self->hide_unavailable); |
637 |
|
✗ |
break; |
638 |
|
|
|
639 |
|
✗ |
case PROP_SHOW_APS: |
640 |
|
✗ |
g_value_set_boolean (value, self->show_aps); |
641 |
|
✗ |
break; |
642 |
|
|
|
643 |
|
✗ |
case PROP_CLIENT: |
644 |
|
✗ |
g_value_set_object (value, self->client); |
645 |
|
✗ |
break; |
646 |
|
|
|
647 |
|
✗ |
case PROP_DEVICE: |
648 |
|
✗ |
g_value_set_object (value, self->device); |
649 |
|
✗ |
break; |
650 |
|
|
|
651 |
|
✗ |
case PROP_FORGETTABLE: |
652 |
|
✗ |
g_value_set_boolean (value, self->forgettable); |
653 |
|
✗ |
break; |
654 |
|
|
|
655 |
|
✗ |
default: |
656 |
|
✗ |
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
657 |
|
|
} |
658 |
|
✗ |
} |
659 |
|
|
|
660 |
|
|
static void |
661 |
|
✗ |
cc_wifi_connection_list_set_property (GObject *object, |
662 |
|
|
guint prop_id, |
663 |
|
|
const GValue *value, |
664 |
|
|
GParamSpec *pspec) |
665 |
|
|
{ |
666 |
|
✗ |
CcWifiConnectionList *self = CC_WIFI_CONNECTION_LIST (object); |
667 |
|
|
|
668 |
|
✗ |
switch (prop_id) |
669 |
|
|
{ |
670 |
|
✗ |
case PROP_CHECKABLE: |
671 |
|
✗ |
self->checkable = g_value_get_boolean (value); |
672 |
|
✗ |
break; |
673 |
|
|
|
674 |
|
✗ |
case PROP_HIDE_UNAVAILABLE: |
675 |
|
✗ |
self->hide_unavailable = g_value_get_boolean (value); |
676 |
|
✗ |
break; |
677 |
|
|
|
678 |
|
✗ |
case PROP_SHOW_APS: |
679 |
|
✗ |
self->show_aps = g_value_get_boolean (value); |
680 |
|
✗ |
break; |
681 |
|
|
|
682 |
|
✗ |
case PROP_CLIENT: |
683 |
|
✗ |
self->client = g_value_dup_object (value); |
684 |
|
✗ |
break; |
685 |
|
|
|
686 |
|
✗ |
case PROP_DEVICE: |
687 |
|
✗ |
self->device = g_value_dup_object (value); |
688 |
|
✗ |
break; |
689 |
|
|
|
690 |
|
✗ |
case PROP_FORGETTABLE: |
691 |
|
✗ |
self->forgettable = g_value_get_boolean (value); |
692 |
|
✗ |
break; |
693 |
|
|
|
694 |
|
✗ |
default: |
695 |
|
✗ |
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
696 |
|
|
} |
697 |
|
✗ |
} |
698 |
|
|
|
699 |
|
|
static void |
700 |
|
✗ |
cc_wifi_connection_list_class_init (CcWifiConnectionListClass *klass) |
701 |
|
|
{ |
702 |
|
✗ |
GObjectClass *object_class = G_OBJECT_CLASS (klass); |
703 |
|
|
|
704 |
|
✗ |
object_class->constructed = cc_wifi_connection_list_constructed; |
705 |
|
✗ |
object_class->dispose = cc_wifi_connection_list_dispose; |
706 |
|
✗ |
object_class->finalize = cc_wifi_connection_list_finalize; |
707 |
|
✗ |
object_class->get_property = cc_wifi_connection_list_get_property; |
708 |
|
✗ |
object_class->set_property = cc_wifi_connection_list_set_property; |
709 |
|
|
|
710 |
|
✗ |
props[PROP_CHECKABLE] = |
711 |
|
✗ |
g_param_spec_boolean ("checkable", "checkable", |
712 |
|
|
"Passed to the created rows to show/hide the checkbox for deletion", |
713 |
|
|
FALSE, |
714 |
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); |
715 |
|
|
|
716 |
|
✗ |
props[PROP_HIDE_UNAVAILABLE] = |
717 |
|
✗ |
g_param_spec_boolean ("hide-unavailable", "HideUnavailable", |
718 |
|
|
"Whether to show or hide unavailable connections", |
719 |
|
|
TRUE, |
720 |
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); |
721 |
|
|
|
722 |
|
✗ |
props[PROP_SHOW_APS] = |
723 |
|
✗ |
g_param_spec_boolean ("show-aps", "ShowAPs", |
724 |
|
|
"Whether to show available SSIDs/APs without a connection", |
725 |
|
|
TRUE, |
726 |
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); |
727 |
|
|
|
728 |
|
✗ |
props[PROP_CLIENT] = |
729 |
|
✗ |
g_param_spec_object ("client", "NMClient", |
730 |
|
|
"The NM Client", |
731 |
|
|
NM_TYPE_CLIENT, |
732 |
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); |
733 |
|
|
|
734 |
|
✗ |
props[PROP_DEVICE] = |
735 |
|
✗ |
g_param_spec_object ("device", "WiFi Device", |
736 |
|
|
"The WiFi Device for this connection list", |
737 |
|
|
NM_TYPE_DEVICE_WIFI, |
738 |
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); |
739 |
|
✗ |
props[PROP_FORGETTABLE] = |
740 |
|
✗ |
g_param_spec_boolean ("forgettable", "forgettable", |
741 |
|
|
"Passed to the created rows to show/hide the checkbox for deletion", |
742 |
|
|
FALSE, |
743 |
|
|
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); |
744 |
|
|
|
745 |
|
✗ |
g_object_class_install_properties (object_class, |
746 |
|
|
PROP_LAST, |
747 |
|
|
props); |
748 |
|
|
|
749 |
|
✗ |
g_signal_new ("configure", |
750 |
|
|
CC_TYPE_WIFI_CONNECTION_LIST, |
751 |
|
|
G_SIGNAL_RUN_LAST, |
752 |
|
|
0, NULL, NULL, NULL, |
753 |
|
|
G_TYPE_NONE, 1, CC_TYPE_WIFI_CONNECTION_ROW); |
754 |
|
✗ |
g_signal_new ("show_qr_code", |
755 |
|
|
CC_TYPE_WIFI_CONNECTION_LIST, |
756 |
|
|
G_SIGNAL_RUN_LAST, |
757 |
|
|
0, NULL, NULL, NULL, |
758 |
|
|
G_TYPE_NONE, 1, CC_TYPE_WIFI_CONNECTION_ROW); |
759 |
|
✗ |
g_signal_new ("forget", |
760 |
|
|
CC_TYPE_WIFI_CONNECTION_LIST, |
761 |
|
|
G_SIGNAL_RUN_LAST, |
762 |
|
|
0, NULL, NULL, NULL, |
763 |
|
|
G_TYPE_NONE, 1, CC_TYPE_WIFI_CONNECTION_ROW); |
764 |
|
✗ |
g_signal_new ("add-row", |
765 |
|
|
CC_TYPE_WIFI_CONNECTION_LIST, |
766 |
|
|
G_SIGNAL_RUN_LAST, |
767 |
|
|
0, NULL, NULL, NULL, |
768 |
|
|
G_TYPE_NONE, 1, CC_TYPE_WIFI_CONNECTION_ROW); |
769 |
|
✗ |
g_signal_new ("remove-row", |
770 |
|
|
CC_TYPE_WIFI_CONNECTION_LIST, |
771 |
|
|
G_SIGNAL_RUN_LAST, |
772 |
|
|
0, NULL, NULL, NULL, |
773 |
|
|
G_TYPE_NONE, 1, CC_TYPE_WIFI_CONNECTION_ROW); |
774 |
|
✗ |
} |
775 |
|
|
|
776 |
|
|
static void |
777 |
|
✗ |
cc_wifi_connection_list_init (CcWifiConnectionList *self) |
778 |
|
|
{ |
779 |
|
✗ |
self->listbox = GTK_LIST_BOX (gtk_list_box_new ()); |
780 |
|
✗ |
gtk_list_box_set_selection_mode (GTK_LIST_BOX (self->listbox), GTK_SELECTION_NONE); |
781 |
|
✗ |
gtk_widget_set_valign (GTK_WIDGET (self->listbox), GTK_ALIGN_START); |
782 |
|
✗ |
gtk_widget_add_css_class (GTK_WIDGET (self->listbox), "boxed-list"); |
783 |
|
✗ |
adw_bin_set_child (ADW_BIN (self), GTK_WIDGET (self->listbox)); |
784 |
|
|
|
785 |
|
✗ |
self->hide_unavailable = TRUE; |
786 |
|
✗ |
self->show_aps = TRUE; |
787 |
|
|
|
788 |
|
✗ |
self->connections = g_ptr_array_new_with_free_func (g_object_unref); |
789 |
|
✗ |
self->connections_row = g_ptr_array_new (); |
790 |
|
✗ |
self->ssid_to_row = g_hash_table_new_full (g_bytes_hash, g_bytes_equal, |
791 |
|
|
(GDestroyNotify) g_bytes_unref, NULL); |
792 |
|
✗ |
self->ap_ssid_cache = g_hash_table_new_full (g_direct_hash, g_direct_equal, |
793 |
|
|
NULL, (GDestroyNotify) g_bytes_unref); |
794 |
|
✗ |
} |
795 |
|
|
|
796 |
|
|
CcWifiConnectionList * |
797 |
|
✗ |
cc_wifi_connection_list_new (NMClient *client, |
798 |
|
|
NMDeviceWifi *device, |
799 |
|
|
gboolean hide_unavailable, |
800 |
|
|
gboolean show_aps, |
801 |
|
|
gboolean checkable, |
802 |
|
|
gboolean forgettable) |
803 |
|
|
{ |
804 |
|
✗ |
return g_object_new (CC_TYPE_WIFI_CONNECTION_LIST, |
805 |
|
|
"client", client, |
806 |
|
|
"device", device, |
807 |
|
|
"hide-unavailable", hide_unavailable, |
808 |
|
|
"show-aps", show_aps, |
809 |
|
|
"checkable", checkable, |
810 |
|
|
"forgettable", forgettable, |
811 |
|
|
NULL); |
812 |
|
|
} |
813 |
|
|
|
814 |
|
|
void |
815 |
|
✗ |
cc_wifi_connection_list_freeze (CcWifiConnectionList *self) |
816 |
|
|
{ |
817 |
|
✗ |
g_return_if_fail (CC_WIFI_CONNECTION_LIST (self)); |
818 |
|
|
|
819 |
|
✗ |
if (self->freeze_count == 0) |
820 |
|
✗ |
g_debug ("wifi connection list has been frozen"); |
821 |
|
|
|
822 |
|
✗ |
self->freeze_count += 1; |
823 |
|
|
} |
824 |
|
|
|
825 |
|
|
void |
826 |
|
✗ |
cc_wifi_connection_list_thaw (CcWifiConnectionList *self) |
827 |
|
|
{ |
828 |
|
✗ |
g_return_if_fail (CC_WIFI_CONNECTION_LIST (self)); |
829 |
|
|
|
830 |
|
✗ |
g_return_if_fail (self->freeze_count > 0); |
831 |
|
|
|
832 |
|
✗ |
self->freeze_count -= 1; |
833 |
|
|
|
834 |
|
✗ |
if (self->freeze_count == 0) |
835 |
|
|
{ |
836 |
|
✗ |
g_debug ("wifi connection list has been thawed"); |
837 |
|
✗ |
update_connections (self); |
838 |
|
|
} |
839 |
|
|
} |
840 |
|
|
|
841 |
|
|
GtkListBox * |
842 |
|
✗ |
cc_wifi_connection_list_get_list_box (CcWifiConnectionList *self) |
843 |
|
|
{ |
844 |
|
✗ |
g_return_val_if_fail (CC_IS_WIFI_CONNECTION_LIST (self), NULL); |
845 |
|
|
|
846 |
|
✗ |
return self->listbox; |
847 |
|
|
} |
848 |
|
|
|
849 |
|
|
gboolean |
850 |
|
✗ |
cc_wifi_connection_list_is_empty (CcWifiConnectionList *self) |
851 |
|
|
{ |
852 |
|
✗ |
g_return_val_if_fail (CC_IS_WIFI_CONNECTION_LIST (self), TRUE); |
853 |
|
|
|
854 |
|
✗ |
return self->connections->len == 0; |
855 |
|
|
} |
856 |
|
|
|
857 |
|
|
void |
858 |
|
✗ |
cc_wifi_connection_list_set_placeholder_text (CcWifiConnectionList *self, |
859 |
|
|
const gchar *placeholder_text) |
860 |
|
|
{ |
861 |
|
|
GtkWidget *listbox_placeholder; |
862 |
|
|
|
863 |
|
✗ |
g_return_if_fail (CC_IS_WIFI_CONNECTION_LIST (self)); |
864 |
|
|
|
865 |
|
✗ |
listbox_placeholder = gtk_label_new (placeholder_text); |
866 |
|
|
|
867 |
|
✗ |
gtk_label_set_wrap (GTK_LABEL (listbox_placeholder), TRUE); |
868 |
|
✗ |
gtk_label_set_max_width_chars (GTK_LABEL (listbox_placeholder), 50); |
869 |
|
✗ |
gtk_widget_add_css_class (listbox_placeholder, "dim-label"); |
870 |
|
✗ |
gtk_widget_add_css_class (listbox_placeholder, "cc-placeholder-row"); |
871 |
|
|
|
872 |
|
✗ |
gtk_list_box_set_placeholder (self->listbox, listbox_placeholder); |
873 |
|
|
} |
874 |
|
|
|