import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.modules.common
import qs.modules.common.utils
import qs.modules.common.widgets
import qs.modules.icons
import qs.services
Item {
id: root
property real scale: Config.data.network.scale
property real history: Config.data.network.graph.history
property var points: [] // Graph points: [{x: time, up: bytes/s, down: bytes/s}]
readonly property var unitHierarchy: FileUtils.getUnitHierarchy(Config.data.network.rates.baseUnit)
implicitWidth: (graph.visible ? graph.width : 0)
+ (iconLoader.visible ? iconLoader.width : 0)
+ (rates.visible ? rates.width : 0) + 4
implicitHeight: Config.data.bar.size - Config.data.bar.size * 0.2
Component.onCompleted: points.push({x: Date.now(), up: 0, down: 0});
Timer {
interval: Config.data.network.rateUpdateInterval
running: true
repeat: true
onTriggered: {
const x = Date.now(), up = Network.rateUp, down = Network.rateDown;
points.push({x, up, down});
const cutoff = x - history * 1000;
while (points.length && points[0].x < cutoff) points.shift();
graph.requestPaint();
}
}
Loader {
id: iconLoader
visible: Config.data.network.icon.enabled
width: sourceComponent.width
height: sourceComponent.height
anchors.verticalCenter: parent.verticalCenter
sourceComponent: Network.networkType === Types.Network.Wireless ?
wirelessIcon :
(Network.networkType === Types.Network.Wired ? wiredIcon : virtualIcon)
}
Component {
id: wiredIcon
NetworkWiredIcon {
color: Config.data.network.icon.color
scale: Config.data.network.icon.scale * root.height
anchors.verticalCenter: parent.verticalCenter
}
}
Component {
id: wirelessIcon
NetworkWirelessIcon {
color: Config.data.network.icon.color
scale: Config.data.network.icon.scale * root.height
anchors.verticalCenter: parent.verticalCenter
bars: {
const thresholds = [-60, -70, -80, -90];
let numBars = 3;
for (let i=0; i < thresholds.length; ++i) {
if (Network.signalStrength > thresholds[i]) break;
numBars--;
}
return Math.max(numBars, 0);
}
}
}
Component {
id: virtualIcon
NetworkVirtualIcon {
color: Config.data.network.icon.color
scale: Config.data.network.icon.scale * root.height
anchors.verticalCenter: parent.verticalCenter
}
}
Canvas {
id: graph
visible: Config.data.network.graph.enabled
anchors {
top: parent.top
bottom: parent.bottom
left: iconLoader.right
leftMargin: iconLoader.visible ? 2 : 0
}
implicitWidth: visible ? 100 * root.scale : 0
onPaint: {
const ctx = getContext("2d");
ctx.clearRect(0, 0, width, height);
if (points.length < 2) return;
const range = history * 1000, currentTime = Date.now(), minTime = currentTime - range;
const visiblePoints = points.filter(p => p.x >= minTime);
const margin = 2, marginHeight = height - 2 * margin;
const drawLine = function(prop, color) {
ctx.strokeStyle = color;
ctx.beginPath();
for (let i = 0; i < visiblePoints.length; ++i) {
const px = ((visiblePoints[i].x - minTime) / range) * width;
const py = margin + (1 - visiblePoints[i][prop] / maxRate) * marginHeight;
if (i === 0) ctx.moveTo(px, py);
ctx.lineTo(px, py);
}
ctx.stroke();
}
ctx.lineWidth = 2;
drawLine('up', Config.data.network.colors.tx);
drawLine('down', Config.data.network.colors.rx)
}
property real maxRate: 1 // Computed: max of all up/down points, updated on paint
onPainted: { maxRate = Math.max(...points.map(p => Math.max(p.up, p.down)), 1); }
}
// Return the rate value using the most appropriate unit within the given
// hierarchy, starting with the given base unit.
// This ensures that the rate component never displays more than 5 digits,
// so that it can have a predictable max width.
function getDisplayRate(rateBytes, fromUnit, baseUnit, hierarchy) {
const startIndex = Math.max(0, hierarchy.indexOf(baseUnit));
for (let i = startIndex; i < hierarchy.length; i++) {
const unit = hierarchy[i];
const rate = FileUtils.convertSizeUnit(rateBytes, fromUnit, unit);
// Use this unit if rate fits OR it's the last available unit
if (rate < 1000 || i === hierarchy.length - 1) {
return {unit, rate};
}
}
// Fallback to largest unit. Shouldn't happen...
const lastUnit = hierarchy[hierarchy.length - 1];
return {
unit: lastUnit,
rate: FileUtils.convertSizeUnit(rateBytes, fromUnit, lastUnit)
};
}
ColumnLayout {
id: rates
visible: Config.data.network.rates.enabled
spacing: -4
anchors {
left: graph.right
leftMargin: 4
verticalCenter: parent.verticalCenter
}
// Calculate fixed width based on maximum possible text
TextMetrics {
id: rateTextMetrics
// The "M" in "MiB", "MB", etc. is usually the widest font glyph.
text: "999.99 " + root.unitHierarchy[1] + "/s"
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size * 0.9
}
RowLayout {
spacing: 2
ArrowIcon {
id: txArrowIcon
scale: root.height / 2
angle: 0
color: Config.data.network.colors.tx
}
Text {
text: {
const display = getDisplayRate(Network.rateUp, 'B', Config.data.network.rates.baseUnit, root.unitHierarchy);
return `${display.rate.toFixed(2)} ${display.unit}/s`;
}
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size * 0.9
color: Config.data.network.colors.tx
Layout.preferredWidth: rateTextMetrics.width
horizontalAlignment: Text.AlignRight
}
}
RowLayout {
spacing: 2
ArrowIcon {
id: rxArrowIcon
scale: root.height / 2
angle: 180
color: Config.data.network.colors.rx
}
Text {
text: {
const display = getDisplayRate(Network.rateDown, 'B', Config.data.network.rates.baseUnit, root.unitHierarchy);
return `${display.rate.toFixed(2)} ${display.unit}/s`;
}
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size * 0.9
color: Config.data.network.colors.rx
Layout.preferredWidth: rateTextMetrics.width
horizontalAlignment: Text.AlignRight
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton | Qt.LeftButton
onClicked: (mouse) => (mouse.button === Qt.RightButton) && menu.popup()
}
AutoSizingMenu {
id: menu
popupType: Popup.Native
title: "Available Interfaces"
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside | Popup.CloseOnPressOutsideParent
AutoSizingMenu {
title: "Select interface"
// Group 1: Wireless
Repeater {
model: Network.interfaces.get(Types.Network.Wireless)
MenuItem {
contentItem: Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
Loader {
active: true
sourceComponent: wirelessIcon
onLoaded: {
item.color = ColorUtils.withLightness(palette.windowText, 0.4);
item.scale = 16;
item.bars = 3;
}
}
Text {
text: modelData
color: palette.windowText
font.pixelSize: Config.data.theme.font.size * 0.9
}
}
onTriggered: {
Network.setActiveInterface(modelData, Types.Network.Wireless);
points = [];
}
}
}
// Separator after wireless group (only if wireless AND wired OR
// virtual groups are not empty).
// A Repeater must be used for the separator to be placed dynamically.
Repeater {
model: Network.interfaces.get(Types.Network.Wireless)?.length > 0
&& (Network.interfaces.get(Types.Network.Wired)?.length > 0
|| Network.interfaces.get(Types.Network.Virtual)?.length > 0)
? [1] : []
delegate: MenuSeparator {}
}
// Group 2: Wired
Repeater {
model: Network.interfaces.get(Types.Network.Wired)
MenuItem {
contentItem: Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
Loader {
active: true
sourceComponent: wiredIcon
onLoaded: {
item.color = ColorUtils.withLightness(palette.windowText, 0.4);
item.scale = 16;
}
}
Text {
text: modelData
color: palette.windowText
font.pixelSize: Config.data.theme.font.size * 0.9
}
}
onTriggered: {
Network.setActiveInterface(modelData, Types.Network.Wired);
points = [];
}
}
}
// Separator after wired group (only if wired AND virtual groups are
// not empty).
// A Repeater must be used for the separator to be placed dynamically.
Repeater {
model: Network.interfaces.get(Types.Network.Wired)?.length > 0
&& Network.interfaces.get(Types.Network.Virtual)?.length > 0
? [1] : []
delegate: MenuSeparator {}
}
// Group 3: Virtual
Repeater {
model: Network.interfaces.get(Types.Network.Virtual)
MenuItem {
contentItem: Row {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
Loader {
active: true
sourceComponent: virtualIcon
onLoaded: {
item.color = ColorUtils.withLightness(palette.windowText, 0.4);
item.scale = 16;
}
}
Text {
text: modelData
color: palette.windowText
font.pixelSize: Config.data.theme.font.size * 0.9
}
}
onTriggered: {
Network.setActiveInterface(modelData, Types.Network.Virtual);
points = [];
}
}
}
}
}
HoverPopup {
anchors.centerIn: root
hoverTarget: root
anchorPosition: Types.stringToPosition(Config.data.bar.position)
contentComponent: Component {
ColumnLayout {
id: contentColumn
spacing: 2
Text {
Layout.alignment: Qt.AlignHCenter
text: `Interface: ${Network.activeInterface} (${Types.networkToString(Network.networkType)})`
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size
font.bold: true
color: Config.data.theme.colors.foreground
}
Text {
Layout.alignment: Qt.AlignHCenter
font.family: Config.data.theme.font.family
font.pixelSize: Config.data.theme.font.size
color: Config.data.theme.colors.foreground
textFormat: Text.RichText
text: {
let rows = [
{
label: "LAN IPs",
value: Network.lanIPs.length ? Network.lanIPs.join("<br>") : "N/A",
font: Config.data.theme.fontMono.family,
fontSize: Config.data.theme.fontMono.size,
},
{
label: "WAN IP",
value: Network.wanIP || "N/A",
font: Config.data.theme.fontMono.family,
fontSize: Config.data.theme.fontMono.size,
},
];
switch (Network.networkType) {
case Types.Network.Wireless:
rows = [
{
label: "SSID",
value: Network.ssid || "N/A",
font: Config.data.theme.font.family,
fontSize: Config.data.theme.font.size,
},
{
label: "Signal",
value: Network.signalStrength < 0 ? Network.signalStrength + " dBm" : "N/A",
font: Config.data.theme.font.family,
fontSize: Config.data.theme.font.size,
},
{
label: "Bitrate",
value: function() {
let out = [];
if (Network.bitrateRx > 0) {
let rx = getDisplayRate(Network.bitrateRx, 'Mb', 'Mb', FileUtils.unitsMetricBit);
out.push(`↓ ${rx.rate} ${rx.unit}/s`);
}
if (Network.bitrateTx > 0) {
let tx = getDisplayRate(Network.bitrateTx, 'Mb', 'Mb', FileUtils.unitsMetricBit);
out.push(`↑ ${tx.rate} ${tx.unit}/s`);
}
if (out.length) {
return out.join("<br>");
}
return "N/A";
}(),
font: Config.data.theme.font.family,
fontSize: Config.data.theme.font.size,
},
].concat(rows);
break;
case Types.Network.Wired:
rows = [
{
label: "Link speed",
value: function() {
if (Network.linkSpeed > 0) {
let dr = getDisplayRate(Network.linkSpeed, 'Mb', 'Mb', FileUtils.unitsMetricBit);
return `${dr.rate} ${dr.unit}/s`;
}
return "N/A";
}(),
font: Config.data.theme.font.family,
fontSize: Config.data.theme.font.size,
},
].concat(rows);
break;
}
let createRow = function(rowData) {
return `
<tr>
<td align="left" width="60">${rowData.label}:</td>
<td align="left" width="150"><span style="font-family: '${rowData.font}'; font-size: ${rowData.fontSize}px;">${rowData.value}</span></td>
</tr>`;
};
return `<table>${rows.map(r => createRow(r)).join("")}</table>`;
}
}
}
}
}
}