mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-24 06:49:46 -05:00
Add 2FA suggestions report
Fixes #4096 Signed-off-by: Thomas Hobson <thomas@hexf.me>
This commit is contained in:
parent
aca197a96f
commit
e5dc3bd037
@ -170,6 +170,8 @@ set(keepassx_SOURCES
|
||||
gui/reports/ReportsPageHibp.cpp
|
||||
gui/reports/ReportsWidgetStatistics.cpp
|
||||
gui/reports/ReportsPageStatistics.cpp
|
||||
gui/reports/ReportsPage2FA.cpp
|
||||
gui/reports/ReportsWidget2FA.cpp
|
||||
gui/osutils/OSUtilsBase.cpp
|
||||
gui/osutils/ScreenLockListener.cpp
|
||||
gui/osutils/ScreenLockListenerPrivate.cpp
|
||||
|
@ -21,6 +21,8 @@
|
||||
#include "ReportsPageHealthcheck.h"
|
||||
#include "ReportsPageHibp.h"
|
||||
#include "ReportsPageStatistics.h"
|
||||
#include "ReportsPage2FA.h"
|
||||
|
||||
#ifdef WITH_XC_BROWSER
|
||||
#include "ReportsPageBrowserStatistics.h"
|
||||
#include "ReportsWidgetBrowserStatistics.h"
|
||||
@ -62,6 +64,7 @@ ReportsDialog::ReportsDialog(QWidget* parent)
|
||||
, m_healthPage(new ReportsPageHealthcheck())
|
||||
, m_hibpPage(new ReportsPageHibp())
|
||||
, m_statPage(new ReportsPageStatistics())
|
||||
, m_2faPage(new ReportsPage2FA())
|
||||
#ifdef WITH_XC_BROWSER
|
||||
, m_browserStatPage(new ReportsPageBrowserStatistics())
|
||||
#endif
|
||||
@ -76,6 +79,7 @@ ReportsDialog::ReportsDialog(QWidget* parent)
|
||||
#endif
|
||||
addPage(m_healthPage);
|
||||
addPage(m_hibpPage);
|
||||
addPage(m_2faPage);
|
||||
|
||||
m_ui->stackedWidget->setCurrentIndex(0);
|
||||
|
||||
@ -87,6 +91,8 @@ ReportsDialog::ReportsDialog(QWidget* parent)
|
||||
connect(m_ui->categoryList, SIGNAL(categoryChanged(int)), m_ui->stackedWidget, SLOT(setCurrentIndex(int)));
|
||||
connect(m_healthPage->m_healthWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
|
||||
connect(m_hibpPage->m_hibpWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
|
||||
connect(m_2faPage->m_2faWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
|
||||
|
||||
#ifdef WITH_XC_BROWSER
|
||||
connect(m_browserStatPage->m_browserWidget,
|
||||
SIGNAL(entryActivated(Entry*)),
|
||||
|
@ -29,6 +29,7 @@ class QTabWidget;
|
||||
class ReportsPageHealthcheck;
|
||||
class ReportsPageHibp;
|
||||
class ReportsPageStatistics;
|
||||
class ReportsPage2FA;
|
||||
#ifdef WITH_XC_BROWSER
|
||||
class ReportsPageBrowserStatistics;
|
||||
#endif
|
||||
@ -77,6 +78,7 @@ private:
|
||||
const QSharedPointer<ReportsPageHealthcheck> m_healthPage;
|
||||
const QSharedPointer<ReportsPageHibp> m_hibpPage;
|
||||
const QSharedPointer<ReportsPageStatistics> m_statPage;
|
||||
const QSharedPointer<ReportsPage2FA> m_2faPage;
|
||||
#ifdef WITH_XC_BROWSER
|
||||
const QSharedPointer<ReportsPageBrowserStatistics> m_browserStatPage;
|
||||
#endif
|
||||
|
42
src/gui/reports/ReportsPage2FA.cpp
Normal file
42
src/gui/reports/ReportsPage2FA.cpp
Normal file
@ -0,0 +1,42 @@
|
||||
//
|
||||
// Created by Thomas on 1/04/2022.
|
||||
//
|
||||
|
||||
#include "ReportsPage2FA.h"
|
||||
#include "ReportsWidget2FA.h"
|
||||
#include "gui/Icons.h"
|
||||
|
||||
|
||||
|
||||
QString ReportsPage2FA::name()
|
||||
{
|
||||
return QObject::tr("2FA");
|
||||
}
|
||||
|
||||
QIcon ReportsPage2FA::icon()
|
||||
{
|
||||
return icons()->icon("chronometer");
|
||||
}
|
||||
|
||||
QWidget* ReportsPage2FA::createWidget()
|
||||
{
|
||||
return m_2faWidget;
|
||||
}
|
||||
|
||||
void ReportsPage2FA::loadSettings(QWidget* widget, QSharedPointer<Database> db)
|
||||
{
|
||||
ReportsWidget2FA* settingsWidget = reinterpret_cast<ReportsWidget2FA*>(widget);
|
||||
settingsWidget->loadSettings(db);
|
||||
}
|
||||
|
||||
void ReportsPage2FA::saveSettings(QWidget* widget)
|
||||
{
|
||||
ReportsWidget2FA* settingsWidget = reinterpret_cast<ReportsWidget2FA*>(widget);
|
||||
settingsWidget->saveSettings();
|
||||
}
|
||||
|
||||
ReportsPage2FA::ReportsPage2FA()
|
||||
: m_2faWidget(new ReportsWidget2FA())
|
||||
{
|
||||
|
||||
}
|
25
src/gui/reports/ReportsPage2FA.h
Normal file
25
src/gui/reports/ReportsPage2FA.h
Normal file
@ -0,0 +1,25 @@
|
||||
//
|
||||
// Created by Thomas on 1/04/2022.
|
||||
//
|
||||
|
||||
#ifndef KEEPASSXC_REPORTSPAGE2FA_H
|
||||
#define KEEPASSXC_REPORTSPAGE2FA_H
|
||||
|
||||
#include "ReportsDialog.h"
|
||||
#include "ReportsWidget2FA.h"
|
||||
|
||||
class ReportsPage2FA : public IReportsPage
|
||||
{
|
||||
|
||||
QString name() override;
|
||||
QIcon icon() override;
|
||||
QWidget* createWidget() override;
|
||||
void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
|
||||
void saveSettings(QWidget* widget) override;
|
||||
|
||||
public:
|
||||
ReportsWidget2FA* m_2faWidget;
|
||||
ReportsPage2FA();
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSPAGE2FA_H
|
258
src/gui/reports/ReportsWidget2FA.cpp
Normal file
258
src/gui/reports/ReportsWidget2FA.cpp
Normal file
@ -0,0 +1,258 @@
|
||||
//
|
||||
// Created by Thomas on 1/04/2022.
|
||||
//
|
||||
|
||||
#include <QNetworkRequest>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <utility>
|
||||
#include <QDesktopServices>
|
||||
#include "ReportsWidget2FA.h"
|
||||
#include "ui_ReportsWidget2FA.h"
|
||||
#include "gui/Icons.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/NetworkManager.h"
|
||||
|
||||
ReportsWidget2FA::ReportsWidget2FA(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_ui(new Ui::ReportsWidget2FA)
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
m_ui->downloadingDirectory->hide();
|
||||
m_ui->downloadDirectoryError->hide();
|
||||
|
||||
#ifdef WITH_XC_NETWORKING
|
||||
m_ui->noNetwork->hide();
|
||||
#endif
|
||||
m_referencesModel.reset(new QStandardItemModel());
|
||||
m_ui->mfaTableView->setModel(m_referencesModel.data());
|
||||
m_ui->mfaTableView->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
m_ui->mfaTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
|
||||
connect(m_ui->enabledFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable()));
|
||||
connect(m_ui->disabledFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable()));
|
||||
connect(m_ui->notSupportedFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable()));
|
||||
connect(m_ui->supportedFilter, SIGNAL(clicked(bool)), this, SLOT(refresh2FATable()));
|
||||
|
||||
connect(m_ui->mfaTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(cellDoubleClick(QModelIndex)));
|
||||
}
|
||||
ReportsWidget2FA::~ReportsWidget2FA()
|
||||
{
|
||||
delete m_ui;
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::saveSettings()
|
||||
{
|
||||
// no settings to save
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::loadSettings(QSharedPointer<Database> db)
|
||||
{
|
||||
m_db = std::move(db);
|
||||
|
||||
m_referencesModel->clear();
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
row << new QStandardItem("Please wait while your database is analysed.");
|
||||
m_referencesModel->appendRow(row);
|
||||
|
||||
#ifdef WITH_XC_NETWORKING
|
||||
fetchDirectory();
|
||||
#else
|
||||
make2FATable(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::fetchDirectory()
|
||||
{
|
||||
m_ui->downloadingDirectory->show();
|
||||
m_ui->downloadDirectoryError->hide();
|
||||
|
||||
m_bytesReceived.clear();
|
||||
|
||||
QUrl directoryUrl = QUrl("https://2fa.directory/api/v1/data.json");
|
||||
|
||||
QNetworkRequest request(directoryUrl);
|
||||
|
||||
m_reply = getNetMgr()->get(request);
|
||||
|
||||
connect(m_reply, &QNetworkReply::finished, this, &ReportsWidget2FA::fetchDirectoryFinished);
|
||||
connect(m_reply, &QIODevice::readyRead, this, &ReportsWidget2FA::fetchDirectoryReadyRead);
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::fetchDirectoryReadyRead()
|
||||
{
|
||||
m_bytesReceived += m_reply->readAll();
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::fetchDirectoryFinished()
|
||||
{
|
||||
bool error = (m_reply->error() != QNetworkReply::NoError);
|
||||
|
||||
m_reply->deleteLater();
|
||||
m_reply = nullptr;
|
||||
|
||||
if(!error){
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(m_bytesReceived);
|
||||
QJsonObject jsonObject = jsonResponse.object();
|
||||
|
||||
// object is a dictionary of category->entryName->entry
|
||||
m_2faDirectoryEntries.clear();
|
||||
|
||||
for(auto category : jsonObject.keys()) {
|
||||
if(!jsonObject.value(category).isObject())
|
||||
goto error; // The schema appears to have changed
|
||||
|
||||
auto categoryEntries = jsonObject.value(category).toObject();
|
||||
|
||||
for(const auto& categoryEntriesName : categoryEntries.keys()){
|
||||
auto jsonEntry = categoryEntries.value(categoryEntriesName);
|
||||
if(!jsonEntry.isObject())
|
||||
goto error;
|
||||
|
||||
auto entry = jsonEntry.toObject();
|
||||
|
||||
auto entryInstance = new MFADirectoryEntry(
|
||||
entry.contains("tfa") && entry.value("tfa").toBool(),
|
||||
entry.value("name").toString(),
|
||||
entry.value("url").toString(),
|
||||
category,
|
||||
entry.value("doc").toString()
|
||||
);
|
||||
|
||||
m_2faDirectoryEntries.append(*entryInstance);
|
||||
}
|
||||
}
|
||||
|
||||
make2FATable(true);
|
||||
}else{
|
||||
goto error;
|
||||
}
|
||||
|
||||
m_ui->downloadingDirectory->hide();
|
||||
return;
|
||||
|
||||
error:
|
||||
m_ui->downloadDirectoryError->show();
|
||||
make2FATable(false);
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::make2FATable(bool useDirectory)
|
||||
{
|
||||
m_directoryUsed = useDirectory;
|
||||
m_referencesModel->clear();
|
||||
|
||||
if(useDirectory) {
|
||||
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("2FA Enabled")
|
||||
<< tr("2FA Supported") << tr("Matched Site")
|
||||
<< tr("Documentation"));
|
||||
|
||||
m_ui->supportedFilter->show();
|
||||
m_ui->notSupportedFilter->show();
|
||||
}else{
|
||||
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("2FA Enabled"));
|
||||
|
||||
m_ui->supportedFilter->hide();
|
||||
m_ui->notSupportedFilter->hide();
|
||||
}
|
||||
|
||||
|
||||
for(auto entry : m_db->rootGroup()->entriesRecursive()){
|
||||
if(!entry->isRecycled() && !entry->excludeFromReports()){
|
||||
auto group = entry->group();
|
||||
auto hasMFA = entry->hasTotp();
|
||||
MFADirectoryEntry* matchedDirectoryEntry = nullptr;
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
row << new QStandardItem(Icons::entryIconPixmap(entry), entry->title())
|
||||
<< new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/"))
|
||||
<< new QStandardItem(hasMFA ? tr("Enabled") : tr("Disabled"));
|
||||
|
||||
if(hasMFA && !m_ui->enabledFilter->isChecked()) continue;
|
||||
if(!hasMFA && !m_ui->disabledFilter->isChecked()) continue;
|
||||
|
||||
if(useDirectory){
|
||||
|
||||
for(auto directoryEntry : m_2faDirectoryEntries){
|
||||
if(QUrl(entry->url()).host() == QUrl(directoryEntry.url()).host()){ // highly likely this is a match
|
||||
matchedDirectoryEntry = &directoryEntry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(matchedDirectoryEntry == nullptr){
|
||||
row << new QStandardItem(tr("Unknown"))
|
||||
<< new QStandardItem(tr("No match"));
|
||||
|
||||
if(!m_ui->notSupportedFilter->isChecked()) continue;
|
||||
}else{
|
||||
row << new QStandardItem(matchedDirectoryEntry->supported() ? tr("Supported") : tr("Not Supported"))
|
||||
<< new QStandardItem(QString("%1 (%2)").arg(matchedDirectoryEntry->name(),
|
||||
matchedDirectoryEntry->url()))
|
||||
<< new QStandardItem(matchedDirectoryEntry->docs());
|
||||
|
||||
if(!m_ui->supportedFilter->isChecked()) continue;
|
||||
}
|
||||
}
|
||||
|
||||
m_referencesModel->appendRow(row);
|
||||
m_tableData.append(new QPair(entry, matchedDirectoryEntry->docs()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::refresh2FATable()
|
||||
{
|
||||
make2FATable(m_directoryUsed);
|
||||
}
|
||||
|
||||
void ReportsWidget2FA::cellDoubleClick(const QModelIndex& index)
|
||||
{
|
||||
if(!index.isValid()){
|
||||
return;
|
||||
}
|
||||
|
||||
auto clickedRow = m_tableData[index.row()];
|
||||
if(index.column() == 5) { // docs column
|
||||
auto directoryUrl = clickedRow->second;
|
||||
QDesktopServices::openUrl(directoryUrl);
|
||||
}else{
|
||||
emit entryActivated(clickedRow->first);
|
||||
}
|
||||
}
|
||||
|
||||
bool MFADirectoryEntry::supported()
|
||||
{
|
||||
return m_2faSupported;
|
||||
}
|
||||
|
||||
QString MFADirectoryEntry::url()
|
||||
{
|
||||
return m_url;
|
||||
}
|
||||
|
||||
QString MFADirectoryEntry::name()
|
||||
{
|
||||
return m_name;
|
||||
}
|
||||
|
||||
QString MFADirectoryEntry::category()
|
||||
{
|
||||
return m_category;
|
||||
}
|
||||
|
||||
QString MFADirectoryEntry::docs()
|
||||
{
|
||||
return m_docs;
|
||||
}
|
||||
MFADirectoryEntry::MFADirectoryEntry(bool supported, QString name, QString url, QString category, QString docs)
|
||||
{
|
||||
m_2faSupported = supported;
|
||||
m_name = std::move(name);
|
||||
m_url = std::move(url);
|
||||
m_category = std::move(category);
|
||||
m_docs = std::move(docs);
|
||||
}
|
||||
|
83
src/gui/reports/ReportsWidget2FA.h
Normal file
83
src/gui/reports/ReportsWidget2FA.h
Normal file
@ -0,0 +1,83 @@
|
||||
//
|
||||
// Created by Thomas on 1/04/2022.
|
||||
//
|
||||
|
||||
#ifndef KEEPASSXC_REPORTSWIDGET2FA_H
|
||||
#define KEEPASSXC_REPORTSWIDGET2FA_H
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QWidget>
|
||||
#include <QIcon>
|
||||
#include <QStandardItemModel>
|
||||
#include <QJsonDocument>
|
||||
|
||||
class Database;
|
||||
class Entry;
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui
|
||||
{
|
||||
class ReportsWidget2FA;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MFADirectoryEntry {
|
||||
public:
|
||||
MFADirectoryEntry(bool supported, QString name, QString url, QString category, QString docs);
|
||||
|
||||
bool supported();
|
||||
QString url();
|
||||
QString name();
|
||||
QString category();
|
||||
QString docs();
|
||||
|
||||
private:
|
||||
bool m_2faSupported;
|
||||
QString m_url;
|
||||
QString m_name;
|
||||
QString m_category;
|
||||
QString m_docs;
|
||||
|
||||
};
|
||||
|
||||
class ReportsWidget2FA : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ReportsWidget2FA(QWidget* parent = nullptr);
|
||||
~ReportsWidget2FA() override;
|
||||
|
||||
void loadSettings(QSharedPointer<Database> db);
|
||||
void saveSettings();
|
||||
|
||||
signals:
|
||||
void entryActivated(Entry*);
|
||||
|
||||
private slots:
|
||||
void fetchDirectoryFinished();
|
||||
void fetchDirectoryReadyRead();
|
||||
|
||||
void refresh2FATable();
|
||||
|
||||
void cellDoubleClick(const QModelIndex& index);
|
||||
|
||||
|
||||
private:
|
||||
void make2FATable(bool useDirectory);
|
||||
|
||||
void fetchDirectory();
|
||||
|
||||
Ui::ReportsWidget2FA* m_ui;
|
||||
|
||||
bool m_directoryUsed = false;
|
||||
|
||||
QByteArray m_bytesReceived;
|
||||
QList<QPair<Entry*, QString>*> m_tableData;
|
||||
QList<MFADirectoryEntry> m_2faDirectoryEntries;
|
||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||
QSharedPointer<Database> m_db;
|
||||
QNetworkReply* m_reply;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSWIDGET2FA_H
|
137
src/gui/reports/ReportsWidget2FA.ui
Normal file
137
src/gui/reports/ReportsWidget2FA.ui
Normal file
@ -0,0 +1,137 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ReportsWidget2FA</class>
|
||||
<widget class="QWidget" name="ReportsWidget2FA">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>634</width>
|
||||
<height>364</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>ReportsWidget2FA</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableView" name="mfaTableView">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="downloadDirectoryError">
|
||||
<property name="text">
|
||||
<string>Downloading the 2FA Directory Failed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="downloadingDirectory">
|
||||
<property name="text">
|
||||
<string>Downloading 2FA Directory (provided by 2fa.directory)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="noNetwork">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>This build of KeepassXC doesn't have networking functions. Networking is required to provide 2FA suggestions</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="hintLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<italic>true</italic>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Double-click entries to edit them</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Filter</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="supportedFilter">
|
||||
<property name="text">
|
||||
<string>Supported</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="notSupportedFilter">
|
||||
<property name="text">
|
||||
<string>Not Supported</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="enabledFilter">
|
||||
<property name="text">
|
||||
<string>Enabled</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="disabledFilter">
|
||||
<property name="text">
|
||||
<string>Disabled</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
Loading…
Reference in New Issue
Block a user