Redesigned interface list widget, it now embeds the interface settings.

Dieser Commit ist enthalten in:
Reinder Feenstra 2025-02-13 22:41:19 +01:00
Ursprung 2ceae8ec9d
Commit 8fd5ac3d1f
8 geänderte Dateien mit 567 neuen und 6 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="96"
height="96"
viewBox="0 0 25.399999 25.400001"
version="1.1"
id="svg8"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="add.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="14.375"
inkscape:cy="40.446429"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="true"
units="px"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:grid
type="xygrid"
id="grid3713"
spacingx="0.26458333"
empspacing="4" />
</sodipodi:namedview>
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-nc/4.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-nc/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:prohibits
rdf:resource="http://creativecommons.org/ns#CommercialUse" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-271.59998)">
<circle
style="fill:#ea80fc;fill-opacity:1;stroke:none;stroke-width:2.51975;stroke-linecap:round;stroke-linejoin:round"
id="path252"
cx="12.7"
cy="284.29996"
r="12.7" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 12.7,274.77498 c -1.172633,0 -2.116667,0.94403 -2.116667,2.11666 v 5.29167 H 5.2916667 c -1.1726347,0 -2.1166667,0.94404 -2.1166667,2.11667 0,1.17263 0.944032,2.11667 2.1166667,2.11667 h 5.2916663 v 5.29167 c 0,1.17263 0.944034,2.11666 2.116667,2.11666 1.172633,0 2.116666,-0.94403 2.116666,-2.11666 v -5.29167 h 5.291667 c 1.172635,0 2.116667,-0.94404 2.116667,-2.11667 0,-1.17263 -0.944032,-2.11667 -2.116667,-2.11667 h -5.291667 v -5.29167 c 0,-1.17263 -0.944033,-2.11666 -2.116666,-2.11666 z"
id="rect843"
sodipodi:nodetypes="sscssscssscssscss" />
</g>
</svg>

Nachher

Breite:  |  Höhe:  |  Größe: 3.5 KiB

Datei anzeigen

@ -94,5 +94,6 @@
<file>board_tile.misc.label.svg</file>
<file>clear_persistent_variables.svg</file>
<file>swap.svg</file>
<file>circle/add.svg</file>
</qresource>
</RCC>

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2020-2024 Reinder Feenstra
* Copyright (C) 2020-2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -23,6 +23,7 @@
#include "createwidget.hpp"
#include "list/marklincanlocomotivelistwidget.hpp"
#include "objectlist/boardlistwidget.hpp"
#include "objectlist/interfacelistwidget.hpp"
#include "objectlist/throttleobjectlistwidget.hpp"
#include "objectlist/trainlistwidget.hpp"
#include "object/luascripteditwidget.hpp"
@ -47,8 +48,10 @@ QWidget* createWidgetIfCustom(const ObjectPtr& object, QWidget* parent)
{
const QString& classId = object->classId();
if(classId == "command_station_list")
return new ObjectListWidget(object, parent); // todo remove
if(classId == "list.interface")
{
return new InterfaceListWidget(object, parent);
}
else if(classId == "decoder_list")
return new ThrottleObjectListWidget(object, parent); // todo remove
else if(classId == "controller_list")

Datei anzeigen

@ -0,0 +1,111 @@
/**
* client/src/widget/objectlist/interfacelistwidget.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "interfacelistwidget.hpp"
#include <QApplication>
#include <QItemDelegate>
#include <QListView>
#include <QMenu>
#include <QPainter>
#include <traintastic/enum/interfacestate.hpp>
#include <traintastic/locale/locale.hpp>
#include "../../theme/theme.hpp"
#include "../../mainwindow.hpp"
class InterfaceListItemDelegate : public QItemDelegate
{
public:
inline InterfaceListItemDelegate(QListView* parent)
: QItemDelegate(parent)
{
}
inline void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const final
{
static const QSize classIconSize{40, 40};
static const QSize stateIconSize{16, 16};
auto* model = qobject_cast<QListView*>(parent())->model();
const auto id = model->data(model->index(index.row(), 0)).toString().prepend('#');
const auto name = model->data(model->index(index.row(), 1)).toString();
const auto state = model->data(model->index(index.row(), 2)).toString();
const auto classId = model->data(model->index(index.row(), 3)).toString();
const auto r = option.rect.adjusted(5, 5, -5, -5);
const int iconOffset = (r.height() - classIconSize.height()) / 2;
const auto palette = QApplication::palette();
QTextOption textOption;
textOption.setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
const auto classIcon = Theme::getIconForClassId(classId);
if(!classIcon.isNull())
{
painter->drawPixmap(r.topLeft() + QPoint(iconOffset, iconOffset), classIcon.pixmap(classIconSize));
}
painter->setPen(palette.color(QPalette::Disabled, QPalette::Text));
painter->drawText(r.adjusted((classIcon.isNull() ? 0 : r.height() + 10), r.height() / 2, 0, 0), id, textOption);
painter->setPen(palette.color(QPalette::Active, QPalette::Text));
painter->drawText(r.adjusted((classIcon.isNull() ? 0 : r.height() + 10), 0, 0, -r.height() / 2), name, textOption);
textOption.setAlignment(Qt::AlignRight | Qt::AlignVCenter);
const auto stateText = Locale::tr(QString(EnumName<InterfaceState>::value).append(":").append(state));
const auto stateRect = r.adjusted(0, r.height() / 2, 0, 0);
const auto stateTextWidth = qCeil(painter->boundingRect(stateRect, stateText, textOption).width());
painter->drawText(stateRect, stateText, textOption);
auto stateIcon = QIcon(Theme::getIconFile(QString("interface_state.").append(state)));
painter->drawPixmap(
stateRect.topRight() - QPoint(stateTextWidth + stateIconSize.width() + 5, -(stateRect.height() - stateIconSize.height()) / 2),
stateIcon.pixmap(stateIconSize));
painter->setPen(QColor(0x80, 0x80, 0x80, 0x30));
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
}
inline QSize sizeHint(const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/) const final
{
return QSize(-1, 50);
}
};
InterfaceListWidget::InterfaceListWidget(const ObjectPtr& object, QWidget* parent)
: StackedObjectListWidget(object, parent)
{
m_list->setItemDelegate(new InterfaceListItemDelegate(m_list));
if(m_createMenu) /*[[likely]]*/
{
m_createMenu->addSeparator();
m_createMenu->addAction(Theme::getIcon("wizard"), Locale::tr("list:setup_using_wizard") + "...",
[]()
{
MainWindow::instance->showAddInterfaceWizard();
});
}
}

Datei anzeigen

@ -0,0 +1,34 @@
/**
* client/src/widget/objectlist/interfacelistwidget.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_INTERFACELISTWIDGET_HPP
#define TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_INTERFACELISTWIDGET_HPP
#include "stackedobjectlistwidget.hpp"
class InterfaceListWidget : public StackedObjectListWidget
{
public:
explicit InterfaceListWidget(const ObjectPtr& object, QWidget* parent = nullptr);
};
#endif

Datei anzeigen

@ -0,0 +1,246 @@
/**
* client/src/widget/objectlist/stackedobjectlistwidget.cpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include "stackedobjectlistwidget.hpp"
#include <QEvent>
#include <QLabel>
#include <QListView>
#include <QMenu>
#include <QMessageBox>
#include <QStackedWidget>
#include <QToolBar>
#include <QVBoxLayout>
#include <traintastic/locale/locale.hpp>
#include "../createwidget.hpp"
#include "../tablewidget.hpp"
#include "../methodicon.hpp"
#include "../../network/object.hpp"
#include "../../network/method.hpp"
#include "../../network/connection.hpp"
#include "../../network/error.hpp"
#include "../../network/callmethod.hpp"
#include "../../network/tablemodel.hpp"
#include "../../theme/theme.hpp"
#include "../../misc/methodaction.hpp"
StackedObjectListWidget::StackedObjectListWidget(const ObjectPtr& object, QWidget* parent)
: QWidget(parent)
, m_object{object}
, m_navBar{new QToolBar(this)}
, m_stack{new QStackedWidget(this)}
, m_list{new QListView(this)}
, m_requestId{Connection::invalidRequestId}
{
m_navBar->hide();
m_navBar->addAction(Theme::getIcon("previous_page"), Locale::tr("stacked_object_list:back"), this, &StackedObjectListWidget::back);
m_navLabel = new QLabel(this);
m_navLabel->setAlignment(Qt::AlignCenter);
m_navLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_navLabel->show();
m_navBar->addWidget(m_navLabel);
if(auto* method = object->getMethod("delete"))
{
m_actionRemove = new MethodAction(Theme::getIcon("delete"), *method,
[this]()
{
if(!m_listObjectId.isEmpty())
{
callMethod(m_actionRemove->method(), nullptr, m_listObjectId);
back();
}
});
m_navBar->addAction(m_actionRemove);
}
connect(m_stack, &QStackedWidget::currentChanged,
[this](int index)
{
m_navBar->setVisible(index > 0);
});
m_requestId = object->connection()->getTableModel(object,
[this](const TableModelPtr& tableModel, std::optional<const Error> error)
{
m_requestId = Connection::invalidRequestId;
if(tableModel)
{
m_tableModel = tableModel;
m_list->setModel(m_tableModel.get());
connect(m_tableModel.get(), &TableModel::modelReset,
[this]()
{
m_tableModel->setRegion(0, m_tableModel->columnCount(), 0, m_tableModel->rowCount());
});
m_tableModel->setRegion(0, m_tableModel->columnCount(), 0, m_tableModel->rowCount());
}
else if(error)
{
QMessageBox::critical(this, "Error", error->toString());
}
});
if(auto* create = object->getMethod("create"))
{
if(create->argumentTypes().size() == 0) // Create method witout argument
{
m_create = new MethodIcon(*create, Theme::getIcon("circle/add"), m_list);
}
else if(create->argumentTypes().size() == 1)
{
m_createMenu = new QMenu(this);
m_createMenu->installEventFilter(this);
QStringList classList = create->getAttribute(AttributeName::ClassList, QVariant()).toStringList();
for(const QString& classId : classList)
{
QAction* action = m_createMenu->addAction(Locale::tr("class_id:" + classId));
action->setData(classId);
connect(action, &QAction::triggered, this,
[this, create, action]()
{
cancelRequest();
m_requestId = create->call(action->data().toString(),
[this](const ObjectPtr& addedObject, std::optional<const Error> error)
{
m_requestId = Connection::invalidRequestId;
if(addedObject)
{
show(addedObject);
}
else if(error)
{
QMessageBox::critical(this, "Error", error->toString());
}
});
});
}
m_create = new MethodIcon(*create, Theme::getIcon("circle/add"),
[this]()
{
m_createMenu->popup(m_create->mapToGlobal(m_create->rect().topRight()));
}, m_list);
}
m_create->setEnabled(create->getAttributeBool(AttributeName::Enabled, true));
if(!create->getAttributeBool(AttributeName::Visible, true))
{
m_create->hide();
}
m_list->installEventFilter(this);
m_create->installEventFilter(this);
}
m_list->setSelectionMode(QListView::NoSelection);
connect(m_list, &QListView::clicked,
[this](const QModelIndex &index)
{
if(!m_tableModel) /*[[unlikely]]*/
{
return;
}
cancelRequest();
m_requestId = m_object->connection()->getObject(m_tableModel->getRowObjectId(index.row()),
[this](const ObjectPtr& selectedObject, std::optional<const Error> error)
{
m_requestId = Connection::invalidRequestId;
if(selectedObject)
{
show(selectedObject);
}
else if(error)
{
QMessageBox::critical(this, "Error", error->toString());
}
});
});
m_stack->addWidget(m_list);
auto* l = new QVBoxLayout();
l->setContentsMargins(0, 0, 0, 0);
l->addWidget(m_navBar);
l->addWidget(m_stack);
setLayout(l);
}
StackedObjectListWidget::~StackedObjectListWidget()
{
cancelRequest();
}
bool StackedObjectListWidget::eventFilter(QObject* object, QEvent* event)
{
if(m_create && ((object == m_list && event->type() == QEvent::Resize) || (object == m_create && event->type() == QEvent::Show)))
{
auto pnt = m_create->rect().bottomRight();
pnt = m_list->rect().bottomRight() - pnt - pnt / 3;
m_create->move(pnt.x(), pnt.y());
}
if(m_createMenu && object == m_createMenu && event->type() == QEvent::Show)
{
m_createMenu->move(m_createMenu->pos() - QPoint{m_createMenu->width(), m_createMenu->height()});
return true;
}
return QWidget::eventFilter(object, event);
}
void StackedObjectListWidget::cancelRequest()
{
if(m_requestId != Connection::invalidRequestId)
{
m_object->connection()->cancelRequest(m_requestId);
}
}
void StackedObjectListWidget::back()
{
if(m_stack->currentIndex() > 0)
{
m_listObjectId.clear();
delete m_stack->currentWidget();
}
}
void StackedObjectListWidget::show(const ObjectPtr& listObject)
{
if(auto* w = createWidget(listObject, this)) /*[[likely]]*/
{
m_listObjectId = listObject->getPropertyValueString("id");
connect(w, &QWidget::windowTitleChanged, m_navLabel, &QLabel::setText);
m_navLabel->setText(w->windowTitle());
m_stack->setCurrentIndex(m_stack->addWidget(w));
}
}

Datei anzeigen

@ -0,0 +1,65 @@
/**
* client/src/widget/objectlist/stackedobjectlistwidget.hpp
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_STACKEDOBJECTLISTWIDGET_HPP
#define TRAINTASTIC_CLIENT_WIDGET_OBJECTLIST_STACKEDOBJECTLISTWIDGET_HPP
#include <QWidget>
#include "../../network/objectptr.hpp"
#include "../../network/tablemodelptr.hpp"
class QToolBar;
class QStackedWidget;
class QListView;
class QLabel;
class MethodIcon;
class MethodAction;
class StackedObjectListWidget : public QWidget
{
protected:
ObjectPtr m_object;
TableModelPtr m_tableModel;
QToolBar* m_navBar;
QLabel* m_navLabel;
QStackedWidget* m_stack;
QListView* m_list;
MethodIcon* m_create = nullptr;
QMenu* m_createMenu = nullptr;
MethodAction* m_actionRemove = nullptr;
QString m_listObjectId;
int m_requestId;
void cancelRequest();
void back();
void show(const ObjectPtr& listObject);
bool eventFilter(QObject* object, QEvent* event) override;
public:
explicit StackedObjectListWidget(const ObjectPtr& object, QWidget* parent = nullptr);
~StackedObjectListWidget() override;
};
#endif

Datei anzeigen

@ -3,7 +3,7 @@
*
* This file is part of the traintastic source code.
*
* Copyright (C) 2021,2023 Reinder Feenstra
* Copyright (C) 2021,2023,2025 Reinder Feenstra
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -27,6 +27,7 @@
constexpr uint32_t columnId = 0;
constexpr uint32_t columnName = 1;
constexpr uint32_t columnClassId = 3;
bool InterfaceListTableModel::isListedProperty(std::string_view name)
{
@ -43,6 +44,7 @@ InterfaceListTableModel::InterfaceListTableModel(InterfaceList& list) :
DisplayName::Object::id,
DisplayName::Object::name,
DisplayName::Interface::status,
"class_id"
});
}
@ -61,10 +63,15 @@ std::string InterfaceListTableModel::getText(uint32_t column, uint32_t row) cons
return interface.name;
case columnStatus:
if(const auto* it = EnumValues<InterfaceState>::value.find(interface.status->state); it != EnumValues<InterfaceState>::value.end())
return std::string("$").append(EnumName<InterfaceState>::value).append(":").append(it->second).append("$");
if(const auto* it = EnumValues<InterfaceState>::value.find(interface.status->state); it != EnumValues<InterfaceState>::value.end()) [[likely]]
{
return it->second;
}
break;
case columnClassId:
return std::string{interface.getClassId()};
default:
assert(false);
break;