mirror of https://github.com/cutefishos/settings
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
13 KiB
C++
339 lines
13 KiB
C++
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <optional>
|
|
|
|
#include "base/i18n.h"
|
|
#include "base/timer.h"
|
|
#include "cutefish/networkmanager/connections.h"
|
|
#include "cutefish/networkmanager/handler.h"
|
|
#include "cutefish/networkmanager/hotspot.h"
|
|
#include "pages/network/common/detailsdialog.h"
|
|
#include "pages/network/common/networklist.h"
|
|
#include "pages/network/common/rows.h"
|
|
#include "pages/network/wlan/newnetworkdialog.h"
|
|
#include "ui/icon.h"
|
|
#include "ui/label.h"
|
|
#include "ui/layout.h"
|
|
#include "ui/cell_layout.h"
|
|
#include "ui/theme.h"
|
|
#include "widgets/card.h"
|
|
#include "widgets/listrow.h"
|
|
#include "widgets/metrics.h"
|
|
#include "widgets/sections.h"
|
|
#include "window/page.h"
|
|
#include "window/options.h"
|
|
#include "window/pagelist.h"
|
|
#include "window/settingswindow.h"
|
|
|
|
using namespace cutefish::ui;
|
|
using namespace std::chrono_literals;
|
|
using cutefish::i18n::tr;
|
|
namespace nm = cutefish::networkmanager;
|
|
|
|
namespace settings::network {
|
|
|
|
namespace {
|
|
|
|
// A titled card of the networks a section takes; hidden while it has none.
|
|
struct WifiSection
|
|
{
|
|
Section* section = nullptr;
|
|
Card* card = nullptr;
|
|
std::unique_ptr<KeyedRows<WifiRow>> rows;
|
|
// A last row that is always there, for joining a hidden network.
|
|
ListRow* footer = nullptr;
|
|
};
|
|
|
|
class WlanPage : public Page
|
|
{
|
|
public:
|
|
explicit WlanPage(SettingsWindow& window)
|
|
: Page(window, tr("Wi-Fi"))
|
|
, m_newNetwork(window)
|
|
{
|
|
content()->setSpacing(metrics::sectionSpacing);
|
|
m_newNetwork.onConnect = [this](const std::string& ssid, const std::string& userName,
|
|
const std::string& password, const std::string& type) {
|
|
if (!m_list.isMock())
|
|
m_handler.addOtherConnection(ssid, userName, password, type);
|
|
};
|
|
m_newNetwork.onJoinNetwork = [this](const std::string& device, const std::string& accessPoint,
|
|
const std::string& userName, const std::string& password) {
|
|
if (!m_list.isMock())
|
|
m_handler.addAndActivateEnterprise(device, accessPoint, userName, password);
|
|
};
|
|
|
|
// The switch and the network in use share the first card; the rest
|
|
// is split into the networks with a saved connection and the others.
|
|
m_view = content()->addChild<ColumnLayout>();
|
|
m_view->setSpacing(metrics::sectionSpacing);
|
|
m_view->setLayoutHints({.fillWidth = true});
|
|
m_mainCard = m_view->addChild<Card>(Card::Style::List);
|
|
m_switch = m_mainCard->addChild<SwitchRow>(tr("Wi-Fi"));
|
|
m_switch->setName("wlan:switch");
|
|
m_switch->onToggled = [this](bool checked) { wirelessToggled(checked); };
|
|
m_currentRows = std::make_unique<KeyedRows<WifiRow>>([this](View* parent) { return makeRow(parent); });
|
|
|
|
// The adapter serves the hotspot instead of joining networks, so there is nothing to list.
|
|
m_hotspotRow = m_mainCard->addChild<ListRow>();
|
|
m_hotspotRow->setName("wlan:hotspot");
|
|
m_hotspotRow->setLeading<Icon>("network-wireless-hotspot", 18);
|
|
m_hotspotLabel = m_hotspotRow->addContent<Label>(tr("Hotspot"));
|
|
m_hotspotChevron = m_hotspotRow->addContent<TrailingIcon>("chevron-right", 16);
|
|
m_hotspotRow->onClicked = [this] { settingsWindow().switchToPage("hotspot"); };
|
|
|
|
m_known = addSection(tr("Known networks"), false);
|
|
m_known.section->setName("wlan:known");
|
|
m_other = addSection(tr("Other networks"), true);
|
|
m_other.section->setName("wlan:other");
|
|
m_other.footer->setName("wlan:addOther");
|
|
|
|
m_subscriptions.push_back(m_list.changed.connect([this] { update(); }));
|
|
m_subscriptions.push_back(m_enabled.changed.connect([this] {
|
|
settleWireless();
|
|
update();
|
|
joinRequestedNetwork();
|
|
}));
|
|
m_subscriptions.push_back(m_handler.hotspotActiveChanged.connect([this] { update(); }));
|
|
m_subscriptions.push_back(
|
|
nm::HotspotConfiguration::instance()->changed.connect([this] { update(); }));
|
|
m_subscriptions.push_back(window.wifiJoinRequested.connect([this] { joinRequestedNetwork(); }));
|
|
m_subscriptions.push_back(window.revealedChanged.connect([this] { joinRequestedNetwork(); }));
|
|
|
|
if (!m_list.isMock()) {
|
|
m_handler.requestScan();
|
|
m_scanTimer.onTimeout = [this] { m_handler.requestScan(); };
|
|
m_scanTimer.start(10200ms);
|
|
}
|
|
update();
|
|
joinRequestedNetwork();
|
|
}
|
|
|
|
protected:
|
|
void layout() override
|
|
{
|
|
const Color disabled = Theme::current().disabledText;
|
|
m_hotspotLabel->setColor(disabled);
|
|
m_hotspotChevron->icon()->setColor(disabled);
|
|
Page::layout();
|
|
}
|
|
|
|
private:
|
|
WifiSection addSection(const std::string& title, bool footer)
|
|
{
|
|
WifiSection section;
|
|
section.section = m_view->addChild<Section>(title);
|
|
section.section->setLayoutHints({.fillWidth = true});
|
|
section.card = section.section->addChild<Card>(Card::Style::List);
|
|
section.rows = std::make_unique<KeyedRows<WifiRow>>([this](View* parent) { return makeRow(parent); });
|
|
if (footer) {
|
|
section.footer = section.card->addChild<ListRow>(tr("Add other…"));
|
|
// Lines the text up with the network names beside their icons.
|
|
View* spacer = section.footer->setLeading<View>();
|
|
spacer->setLayoutHints({.preferredWidth = 18, .preferredHeight = 18});
|
|
section.footer->onClicked = [this] { m_newNetwork.show(); };
|
|
}
|
|
return section;
|
|
}
|
|
|
|
WifiRow* makeRow(View* parent)
|
|
{
|
|
WifiRow* row = parent->addChild<WifiRow>();
|
|
row->onClicked = [this, row] {
|
|
if (row->displayedState() == nm::ConnectionStatus::Deactivated)
|
|
connectNetwork(row->item());
|
|
};
|
|
row->onInfo = [this](WifiRow* row) { showDetails(row->item()); };
|
|
return row;
|
|
}
|
|
|
|
void connectNetwork(const nm::NetworkItem& item)
|
|
{
|
|
if (!item.uuid.empty()) {
|
|
if (!m_list.isMock())
|
|
m_handler.activateConnection(item.connectionPath, item.devicePath, item.specificPath);
|
|
} else if (isEnterprise(item.securityType)) {
|
|
m_newNetwork.openNetwork(item.specificPath, m_list.isMock() ? std::nullopt
|
|
: m_handler.wirelessNetwork(item.specificPath));
|
|
} else if (!m_list.isMock()) {
|
|
m_handler.addAndActivateConnection(item.devicePath, item.specificPath);
|
|
}
|
|
}
|
|
|
|
void showDetails(const nm::NetworkItem& item)
|
|
{
|
|
m_details = std::make_unique<DetailsDialog>(settingsWindow(), m_list, DetailsDialog::Kind::Wireless, item);
|
|
m_details->show();
|
|
}
|
|
|
|
// The switch shows the requested state until NetworkManager reports it,
|
|
// so that a list refresh in between does not flip it back.
|
|
void wirelessToggled(bool checked)
|
|
{
|
|
if (m_list.isMock() || checked == wirelessEnabled())
|
|
return;
|
|
m_requestedWireless = checked;
|
|
++m_wirelessRequests;
|
|
m_handler.enableWireless(checked, [this](const cutefish::base::dbus::Reply& reply) {
|
|
--m_wirelessRequests;
|
|
if (reply.isError())
|
|
m_requestedWireless.reset();
|
|
settleWireless();
|
|
update();
|
|
});
|
|
if (checked)
|
|
m_handler.requestScan();
|
|
update();
|
|
}
|
|
|
|
bool wirelessEnabled() const { return m_requestedWireless.value_or(m_enabled.isWirelessEnabled()); }
|
|
|
|
void settleWireless()
|
|
{
|
|
if (m_wirelessRequests == 0 && m_requestedWireless == m_enabled.isWirelessEnabled())
|
|
m_requestedWireless.reset();
|
|
}
|
|
|
|
// The status bar sends enterprise networks here. The window and
|
|
// NetworkManager's objects may not be there yet when the request arrives.
|
|
void joinRequestedNetwork()
|
|
{
|
|
SettingsWindow& window = settingsWindow();
|
|
if (!window.isRevealed() || !m_enabled.isReady())
|
|
return;
|
|
const std::string accessPoint = window.takePendingWifiJoin();
|
|
if (accessPoint.empty())
|
|
return;
|
|
m_newNetwork.openNetwork(accessPoint, m_handler.wirelessNetwork(accessPoint));
|
|
}
|
|
|
|
void update()
|
|
{
|
|
const bool mock = m_list.isMock();
|
|
content()->setVisible(m_enabled.isWirelessHwEnabled() || mock);
|
|
m_switch->setChecked(mock || wirelessEnabled());
|
|
|
|
const std::vector<nm::NetworkItem> items = m_list.rows(nm::ConnectionType::Wireless);
|
|
// Switching networks leaves the old one Deactivating until the new one
|
|
// is up; show it as already gone so only the network being joined spins.
|
|
const bool activating = std::any_of(items.begin(), items.end(), [](const nm::NetworkItem& item) {
|
|
return item.connectionState == nm::ConnectionStatus::Activating;
|
|
});
|
|
const auto displayed = [activating](const nm::NetworkItem& item) {
|
|
if (item.connectionState == nm::ConnectionStatus::Deactivating && activating)
|
|
return nm::ConnectionStatus::Deactivated;
|
|
return item.connectionState;
|
|
};
|
|
|
|
std::vector<const nm::NetworkItem*> current;
|
|
std::vector<const nm::NetworkItem*> known;
|
|
std::vector<const nm::NetworkItem*> other;
|
|
for (const nm::NetworkItem& item : items) {
|
|
if (displayed(item) != nm::ConnectionStatus::Deactivated)
|
|
current.push_back(&item);
|
|
else if (!item.uuid.empty())
|
|
known.push_back(&item);
|
|
else
|
|
other.push_back(&item);
|
|
}
|
|
|
|
const auto fill = [&displayed](KeyedRows<WifiRow>& rows, Card* card,
|
|
const std::vector<const nm::NetworkItem*>& list) {
|
|
std::vector<std::string> keys;
|
|
for (const nm::NetworkItem* item : list)
|
|
keys.push_back(item->key);
|
|
const std::vector<WifiRow*> made = rows.update(card, keys);
|
|
for (size_t i = 0; i < made.size(); ++i)
|
|
made[i]->setItem(*list[i], displayed(*list[i]));
|
|
return std::vector<View*>(made.begin(), made.end());
|
|
};
|
|
|
|
std::vector<View*> mainOrder{m_switch};
|
|
for (View* row : fill(*m_currentRows, m_mainCard, current))
|
|
mainOrder.push_back(row);
|
|
mainOrder.push_back(m_hotspotRow);
|
|
arrange(m_mainCard, mainOrder);
|
|
|
|
const bool hotspot = m_handler.hotspotActive();
|
|
m_hotspotRow->setVisible(hotspot);
|
|
m_hotspotRow->setText(nm::HotspotConfiguration::instance()->name());
|
|
|
|
std::vector<View*> knownOrder = fill(*m_known.rows, m_known.card, known);
|
|
arrange(m_known.card, knownOrder);
|
|
m_known.section->setVisible(!hotspot && !known.empty());
|
|
|
|
std::vector<View*> otherOrder = fill(*m_other.rows, m_other.card, other);
|
|
otherOrder.push_back(m_other.footer);
|
|
arrange(m_other.card, otherOrder);
|
|
m_other.footer->setVisible(m_switch->isChecked());
|
|
m_other.section->setVisible(!hotspot && (!other.empty() || m_switch->isChecked()));
|
|
requestLayout();
|
|
}
|
|
|
|
NetworkList m_list;
|
|
nm::EnabledConnections m_enabled;
|
|
nm::Handler m_handler;
|
|
cutefish::base::Timer m_scanTimer;
|
|
std::vector<cutefish::base::Subscription> m_subscriptions;
|
|
NewNetworkDialog m_newNetwork;
|
|
std::unique_ptr<DetailsDialog> m_details;
|
|
std::optional<bool> m_requestedWireless;
|
|
int m_wirelessRequests = 0;
|
|
|
|
ColumnLayout* m_view;
|
|
Card* m_mainCard;
|
|
SwitchRow* m_switch;
|
|
std::unique_ptr<KeyedRows<WifiRow>> m_currentRows;
|
|
ListRow* m_hotspotRow;
|
|
Label* m_hotspotLabel;
|
|
TrailingIcon* m_hotspotChevron;
|
|
WifiSection m_known;
|
|
WifiSection m_other;
|
|
};
|
|
|
|
// Answered once, for the sample networks.
|
|
class FixedAvailability : public AvailabilityWatch
|
|
{
|
|
};
|
|
|
|
// Once NetworkManager has been read, Wi-Fi leaves the sidebar if no adapter is there.
|
|
class WirelessHardwareWatch : public AvailabilityWatch
|
|
{
|
|
public:
|
|
explicit WirelessHardwareWatch(AvailabilityCallback callback)
|
|
: m_callback(std::move(callback))
|
|
{
|
|
m_subscription = m_enabled.changed.connect([this] { report(); });
|
|
report();
|
|
}
|
|
|
|
private:
|
|
void report()
|
|
{
|
|
if (m_enabled.isReady())
|
|
m_callback(m_enabled.isWirelessHwEnabled());
|
|
}
|
|
|
|
nm::EnabledConnections m_enabled;
|
|
cutefish::base::Subscription m_subscription;
|
|
AvailabilityCallback m_callback;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
static const PageRegistration registration(
|
|
"wlan", [](SettingsWindow& window) -> std::unique_ptr<Page> { return std::make_unique<WlanPage>(window); },
|
|
[](SettingsWindow&, AvailabilityCallback callback) -> std::unique_ptr<AvailabilityWatch> {
|
|
// The sample networks show the page whatever the hardware.
|
|
if (options().mockNetwork) {
|
|
callback(true);
|
|
return std::make_unique<FixedAvailability>();
|
|
}
|
|
return std::make_unique<WirelessHardwareWatch>(std::move(callback));
|
|
});
|
|
|
|
} // namespace settings::network
|