Commit 681bad31 authored by Harald Sitter's avatar Harald Sitter 🌈
Browse files

port samba module to qml

looks nicer!

this twiddles with the UI a lot, a change I didn't want to make when I
rewrote the modeling tech for 5.18 to unbreak the kcm.

on the backend side this mostly just changes the previous column system
to an actual role system for the model as that is nicer to use from qml.
also more useful values are now being modelled in addition to the raw
backend data. notably a working shareurl can be constructed so long as
we can resolve a FQDN from avahi.

on the GUI side the entire kcm is now written in qtquick and requires
kdeclarative 5.74 due to its use of AbstractKCM

the two views are still on the same page but use more appealing
delegates now instead of plain tables. furthermore the delegates have
been made interactable where useful (shares can be edited, fully
qualified urls clicked, mounts can be clicked to open dolphin)
parent f89a4fd5
......@@ -37,6 +37,7 @@ find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
Declarative
Package
Solid
Declarative
)
if(${Qt5Gui_OPENGL_IMPLEMENTATION} STREQUAL "GL")
......
# KI18N Translation Domain for this library
add_definitions(-DTRANSLATION_DOMAIN=\"kcmsamba\")
file(GLOB QML_SRCS package/contents/*.qml)
add_custom_target(QmlFiles ALL echo SOURCES ${QML_SRCS})
set(kcm_samba_PART_SRCS
smbmountmodel.cpp
ksambasharemodel.cpp
)
qt5_add_dbus_interface(kcm_samba_PART_SRCS org.freedesktop.Avahi.Server.xml org.freedesktop.Avahi.Server)
# Intermediate lib for use in testing.
add_library(kcm_samba_static STATIC ${kcm_samba_PART_SRCS})
target_link_libraries(kcm_samba_static
KF5::KIOCore
KF5::Solid
KF5::I18n
KF5::I18n
KF5::KCMUtils
KF5::KIOWidgets
)
add_library(kcm_samba MODULE main.cpp)
set(kcm_samba_SRCS main.cpp)
qt5_add_dbus_interface(kcm_samba_SRCS org.freedesktop.DBus.Properties.xml org.freedesktop.DBus.Properties)
add_library(kcm_samba MODULE ${kcm_samba_SRCS})
target_link_libraries(kcm_samba
KF5::QuickAddons
kcm_samba_static
)
install(TARGETS kcm_samba DESTINATION ${PLUGIN_INSTALL_DIR})
kcoreaddons_desktop_to_json(kcm_samba smbstatus.desktop)
install(TARGETS kcm_samba DESTINATION ${PLUGIN_INSTALL_DIR}/kcms)
install(FILES smbstatus.desktop DESTINATION ${SERVICES_INSTALL_DIR})
kpackage_install_package(package kcmsamba kcms) # NB: the target name follows the kaboutdata name which in turn follows the i18n domain name
add_subdirectory(autotests)
#! /usr/bin/env bash
$XGETTEXT *.cpp -o $podir/kcmsamba.pot
#!/usr/bin/env bash
$XGETTEXT `find . -name '*.cpp' -o -name '*.h' -o -name '*.qml' -o -name '*.js'` -o $podir/kcmsamba.pot
......@@ -5,15 +5,21 @@
#include "ksambasharemodel.h"
#include <QDBusPendingCallWatcher>
#include <QMetaEnum>
#include <QApplication> // for kpropertiesdialog parenting
#include <KSambaShare>
#include <KLocalizedString>
#include <KIOWidgets/KPropertiesDialog>
#include "org.freedesktop.Avahi.Server.h"
KSambaShareModel::KSambaShareModel(QObject *parent)
: QAbstractListModel(parent)
{
connect(KSambaShare::instance(), &KSambaShare::changed,
this, &KSambaShareModel::reloadData);
reloadData();
metaObject()->invokeMethod(this, &KSambaShareModel::reloadData);
}
KSambaShareModel::~KSambaShareModel() = default;
......@@ -24,57 +30,50 @@ int KSambaShareModel::rowCount(const QModelIndex &parent) const
return m_list.size();
}
int KSambaShareModel::columnCount(const QModelIndex &parent) const
QVariant KSambaShareModel::data(const QModelIndex &index, int intRole) const
{
Q_UNUSED(parent)
return static_cast<int>(ColumnRole::ColumnCount);
}
QVariant KSambaShareModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation != Qt::Horizontal || role != Qt::DisplayRole) {
return {}; // we only have column headers.
}
Q_ASSERT(section < static_cast<int>(ColumnRole::ColumnCount));
switch (static_cast<ColumnRole>(section)) {
case ColumnRole::Name:
return i18nc("@title:column samba share name", "Name");
case ColumnRole::Path:
return i18nc("@title:column samba share dir path", "Path");
case ColumnRole::Comment:
return i18nc("@title:column samba share text comment/description", "Comment");
case ColumnRole::ColumnCount:
break; // noop
if (!index.isValid()) {
return {};
}
return {};
}
Q_ASSERT(index.row() < m_list.length());
QVariant KSambaShareModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid()) {
static QMetaEnum roleEnum = QMetaEnum::fromType<Role>();
if (roleEnum.valueToKey(intRole) == nullptr) {
return {};
}
const auto role = static_cast<Role>(intRole);
Q_ASSERT(index.row() < m_list.length());
Q_ASSERT(index.column() < static_cast<int>(ColumnRole::ColumnCount));
if (role == Qt::DisplayRole) {
switch (static_cast<ColumnRole>(index.column())) {
case ColumnRole::Name:
return m_list.at(index.row()).name();
case ColumnRole::Path:
return m_list.at(index.row()).path();
case ColumnRole::Comment:
return m_list.at(index.row()).comment();
case ColumnRole::ColumnCount:
break; // noop
const KSambaShareData &share = m_list.at(index.row());
switch (role) {
case Role::Name:
return share.name();
case Role::Path:
return share.path();
case Role::Comment:
return share.comment();
case Role::ShareUrl: {
if (m_fqdn.isEmpty()) {
return {};
}
QUrl url;
url.setScheme(QStringLiteral("smb"));
url.setHost(m_fqdn);
url.setPath(QStringLiteral("/") + share.name());
return url;
}
}
return {};
}
Q_INVOKABLE void KSambaShareModel::showPropertiesDialog(int index)
{
auto dialog = new KPropertiesDialog(QUrl::fromUserInput(m_list.at(index).path()), QApplication::activeWindow());
dialog->setFileNameReadOnly(true);
dialog->show();
}
void KSambaShareModel::reloadData()
{
beginResetModel();
......@@ -84,9 +83,48 @@ void KSambaShareModel::reloadData()
m_list += samba->getSharesByPath(path);
}
endResetModel();
// Reload FQDN
m_fqdn.clear();
auto avahi = new OrgFreedesktopAvahiServerInterface(QStringLiteral("org.freedesktop.Avahi"),
QStringLiteral("/"),
QDBusConnection::systemBus(),
this);
auto watcher = new QDBusPendingCallWatcher(avahi->GetHostNameFqdn(), this);
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, avahi, watcher] {
watcher->deleteLater();
avahi->deleteLater();
QDBusPendingReply<QString> reply = *watcher;
if (reply.isError()) {
// When Avahi isn't available there's not really a good way to resolve the FQDN. The user could drive
// resolution through LLMNR or NetBios or some other ad-hoc system, neither provide us with an easy
// way to get their configured FQDN. We are therefor opting to not render URLs in that scenario since
// we can't get a name that will reliably work.
m_fqdn.clear();
return;
}
m_fqdn = reply.argumentAt(0).toString();
Q_EMIT dataChanged(createIndex(0, 0), createIndex(m_list.count(), 0), { static_cast<int>(Role::ShareUrl) });
});
}
bool KSambaShareModel::hasChildren(const QModelIndex &parent) const
{
return !parent.isValid() ? false : (rowCount(parent) > 0);
}
QHash<int, QByteArray> KSambaShareModel::roleNames() const
{
static QHash<int, QByteArray> roles;
if (!roles.isEmpty()) {
return roles;
}
const QMetaEnum roleEnum = QMetaEnum::fromType<Role>();
for (int i = 0; i < roleEnum.keyCount(); ++i) {
const int value = roleEnum.value(i);
Q_ASSERT(value != -1);
roles[static_cast<int>(value)] = QByteArray("ROLE_") + roleEnum.valueToKey(value);
}
return roles;
}
......@@ -3,41 +3,35 @@
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#ifndef KSAMBASHAREMODEL_H
#define KSAMBASHAREMODEL_H
#pragma once
#include <QAbstractListModel>
#include <KIOCore/KSambaShareData>
/**
* Model of ksamabasharedata. Implementing properties
* as columns rather than roles.
* Model of KSambaShareData.
*/
class KSambaShareModel : public QAbstractListModel
{
Q_OBJECT
public:
enum class ColumnRole {
Name,
Path,
Comment,
ColumnCount, // End marker
};
enum class Role { Name = Qt::UserRole + 1, Path, ShareUrl, Comment };
Q_ENUM(Role);
explicit KSambaShareModel(QObject *parent = nullptr);
~KSambaShareModel() override;
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool hasChildren(const QModelIndex &parent) const override;
int rowCount(const QModelIndex &parent) const final;
QVariant data(const QModelIndex &index, int intRole) const final;
bool hasChildren(const QModelIndex &parent) const final;
Q_INVOKABLE void showPropertiesDialog(int index);
public slots:
QHash<int, QByteArray> roleNames() const final;
public Q_SLOTS:
void reloadData();
private:
QList<KSambaShareData> m_list;
QString m_fqdn;
};
#endif // KSAMBASHAREMODEL_H
......@@ -3,39 +3,29 @@
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
#include <QTableView>
#include <QHeaderView>
#include <QVBoxLayout>
#include <KCModule>
#include <KPluginFactory>
#include <KLocalizedString>
#include <KAboutData>
#include <KTitleWidget>
#include <KQuickAddons/ConfigModule>
#include "ksambasharemodel.h"
#include "smbmountmodel.h"
class SambaContainer : public KCModule {
class SambaModule : public KQuickAddons::ConfigModule
{
Q_OBJECT
public:
SambaContainer(QWidget *parent = nullptr, const QVariantList &list = QVariantList());
~SambaContainer() override = default;
private:
QTableView *addTableView(const QString &localizedLabel, QAbstractListModel *model);
SambaModule(QObject *parent = nullptr, const QVariantList &list = QVariantList());
~SambaModule() override = default;
};
K_PLUGIN_FACTORY(SambaFactory,
registerPlugin<SambaContainer>();
)
SambaContainer::SambaContainer(QWidget *parent, const QVariantList &)
: KCModule(parent)
SambaModule::SambaModule(QObject *parent, const QVariantList &args)
: KQuickAddons::ConfigModule(parent, args)
{
KAboutData *about = new KAboutData(i18n("kcmsamba"),
i18n("System Information Control Module"),
QString(), QString(), KAboutLicense::GPL,
i18n("(c) 2002 KDE Information Control Module Samba Team"));
i18n("(c) 2002-2020 KDE Information Control Module Samba Team"));
about->addAuthor(i18n("Michael Glauche"), QString(), QStringLiteral("glauche@isa.rwth-aachen.de"));
about->addAuthor(i18n("Matthias Hoelzer"), QString(), QStringLiteral("hoelzer@kde.org"));
about->addAuthor(i18n("David Faure"), QString(), QStringLiteral("faure@kde.org"));
......@@ -45,54 +35,12 @@ SambaContainer::SambaContainer(QWidget *parent, const QVariantList &)
about->addAuthor(i18n("Harald Sitter"), QString(), QStringLiteral("sitter@kde.org"));
setAboutData(about);
QVBoxLayout *layout = new QVBoxLayout(this);
Q_ASSERT(this->layout());
setLayout(layout);
addTableView(i18nc("@title/group", "Exported Shares"), new KSambaShareModel(this));
auto importsView = addTableView(i18nc("@title/group", "Mounted Shares"), new SmbMountModel(this));
importsView->horizontalHeader()->setSectionResizeMode(static_cast<int>(SmbMountModel::ColumnRole::Accessible),
QHeaderView::ResizeToContents);
qmlRegisterType<SmbMountModel>("org.kde.kinfocenter.samba", 1, 0, "MountModel");
qmlRegisterType<KSambaShareModel>("org.kde.kinfocenter.samba", 1, 0, "ShareModel");
setButtons(Help);
}
QTableView *SambaContainer::addTableView(const QString &localizedLabel, QAbstractListModel *model)
{
auto title = new KTitleWidget(this);
title->setText(localizedLabel);
title->setLevel(2);
layout()->addWidget(title);
auto view = new QTableView(this);
layout()->addWidget(view);
view->setModel(model);
// Stretching is a bit awkward because it allows resizing below the sizeHint of
// the header, effectively cutting off the text. This is made worse by kcmshell
// which rather unfortunately stacks scrollviews so size hinting is lost along
// the way allowing the actual window to be (even by default) smaller than
// what our preferred hint is. To mitigate this problem we manually make
// the sizeHint's width the minimal size. This is kind of like QSizePolicy::Minimum.
// https://bugs.kde.org/show_bug.cgi?id=419786
int maxSectionRequirement = 0;
for (auto i = 0; i < view->model()->columnCount(); ++i) {
const int hint = view->horizontalHeader()->sectionSizeHint(i);
maxSectionRequirement = qMax<int>(maxSectionRequirement, hint);
}
view->horizontalHeader()->setMinimumSectionSize(maxSectionRequirement);
// Combined with the minimum section size this makes sure the default size will
// be minimal sufficient regardless of parent sizing policies and model content
// i.e. an empty view will still have fine spacing for header text.
view->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
view->horizontalHeader()->reset();
view->horizontalHeader()->setVisible(true);
view->verticalHeader()->setVisible(false);
return view;
}
K_PLUGIN_CLASS_WITH_JSON(SambaModule, "smbstatus.json")
#include "main.moc"
<?xml version="1.0" standalone='no'?>
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.Avahi.Server">
<method name="GetHostNameFqdn">
<arg name="name" type="s" direction="out"/>
</method>
</interface>
</node>
<?xml version="1.0"?>
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg type="s" name="interface_name" direction="in"/>
<arg type="s" name="property_name" direction="in"/>
<arg type="v" name="value" direction="out"/>
</method>
</interface>
</node>
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
import org.kde.kcm 1.4 as KCM
import QtQuick 2.14
import org.kde.kirigami 2.12 as Kirigami
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kinfocenter.samba 1.0 as Samba
// This is a slightly bespoke variant of a BasicListItem. It features equally sized labels for the share information.
Kirigami.AbstractListItem {
id: listItem
contentItem: RowLayout {
spacing: LayoutMirroring.enabled ? listItem.rightPadding : listItem.leftPadding
ColumnLayout {
spacing: 0
Layout.fillWidth: true
RowLayout {
QQC2.Label {
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignLeft
font: Kirigami.Theme.smallFont
text: i18nc("@label local file system path", 'Path:')
}
Kirigami.UrlButton {
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
font: Kirigami.Theme.smallFont
url: ROLE_Path
}
}
RowLayout {
QQC2.Label {
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignLeft
font: Kirigami.Theme.smallFont
text: i18nc("@label labels a samba url or path", 'Shared at:')
}
// either fully qualified url
Kirigami.UrlButton {
id: link
visible: ROLE_ShareUrl !== undefined
Layout.fillWidth: true
horizontalAlignment: Text.AlignLeft
font: Kirigami.Theme.smallFont
url: ROLE_ShareUrl
}
// ... or name when we couldn't resolve a fully qualified url
QQC2.Label {
visible: !link.visible
Layout.fillWidth: true
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignLeft
font: Kirigami.Theme.smallFont
text: "/" + ROLE_Name
}
}
}
QQC2.ToolButton {
action: Kirigami.Action {
iconName: "document-properties"
tooltip: xi18nc("@info:tooltip", "Open folder properties to change share settings")
displayHint: Kirigami.Action.DisplayHint.IconOnly
onTriggered: view.model.showPropertiesDialog(model.row)
}
QQC2.ToolTip {
text: parent.action.tooltip
}
}
}
}
// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
// SPDX-FileCopyrightText: 2020 Harald Sitter <sitter@kde.org>
import org.kde.kcm 1.4 as KCM
import QtQuick 2.14
import org.kde.kirigami 2.12 as Kirigami
import QtQuick.Controls 2.14 as QQC2
import QtQuick.Layouts 1.14
import org.kde.kinfocenter.samba 1.0 as Samba
KCM.AbstractKCM {
GridLayout {
anchors.fill: parent
columns: 2
Kirigami.Heading {
text: i18nc("@title heading above listview", "User-Created Shares")
level: 2
}
Kirigami.Heading {
text: i18nc("@title heading above listview", "Mounted Remote Shares")
level: 2
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Component.onCompleted: background.visible = true // crashes when initialized with this. god knows why
ListView {
id: view
keyNavigationEnabled: false
model: Samba.ShareModel{}
delegate: ShareListItem {
// The view isn't navigatable nor intearactable. Disable highlighting.
hoverEnabled: false
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
visible: parent.count == 0
icon.name: "network-server"
text: i18nc("@info place holder for empty listview", "There are no directories shared by users")
}
}
}
QQC2.ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
Kirigami.Theme.colorSet: Kirigami.Theme.View
Kirigami.Theme.inherit: false
Component.onCompleted: background.visible = true // crashes when initialized with this. god knows why
ListView {
currentIndex: -1
model: Samba.MountModel {}
delegate: Kirigami.BasicListItem {
label: ROLE_Path
subtitle: ROLE_Share
action: Kirigami.Action {
// TODO document-open-remote is actually pretty cool but lacks a visualization for not connected
// emblem icons are kind of a crutch
iconName: ROLE_Accessible ? "emblem-mounted" : "emblem-unmounted"
onTriggered: {
// Append a slash as openurlexternally fucks with perfectly valid urls and turns them into
// invalid ones (file:///srv => file://srv) that KIO then thinks is a windows UNC path
// (file://srv => smb://srv).
// By appending a slash we effectively trick Qt. Kinda meh.
Qt.openUrlExternally("file://" + ROLE_Path + "/")
}
}
}
Kirigami.PlaceholderMessage {
anchors.centerIn: parent
width: parent.width - (Kirigami.Units.largeSpacing * 4)
visible: parent.count == 0
icon.name: "folder-network"
text: i18nc("@info place holder for empty listview",
"There are no Samba shares mounted on this system")
}
}
}
}
}
[Desktop Entry]
Name=Samba Status
Type=Service
Icon=preferences-system-network-sharing
X-KDE-ServiceTypes=SettingsModule
X-KDE-PluginInfo-Author=Harald Sitter
X-KDE-PluginInfo-Email=sitter@kde.org
X-KDE-PluginInfo-Name=kcmsamba
X-KDE-PluginInfo-Version=1.0
X-KDE-PluginInfo-Website=https://www.kde.org
X-KDE-PluginInfo-License=GPL
X-Plasma-MainScript=main.qml
......@@ -5,8 +5,9 @@
#include "smbmountmodel.h"
#include <QDebug>
#include <QIcon>
#include <KLocalizedString>
#include <QMetaEnum>
#include <Solid/Device>
#include <Solid/DeviceInterface>
#include <Solid/DeviceNotifier>
......@@ -20,7 +21,7 @@ SmbMountModel::SmbMountModel(QObject *parent)
this, &SmbMountModel::addDevice);
connect(Solid::DeviceNotifier::instance(), &Solid::DeviceNotifier::deviceRemoved,
this, &SmbMountModel::removeDevice);