mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-22 01:11:42 +00:00
Add fluent builders for more flexibly adding data to Shuffleboard (#1022)
This commit is contained in:
committed by
Peter Johnson
parent
ac7dfa5042
commit
175c6c1f01
42
wpilibc/src/main/native/cpp/shuffleboard/ComplexWidget.cpp
Normal file
42
wpilibc/src/main/native/cpp/shuffleboard/ComplexWidget.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/ComplexWidget.h"
|
||||
|
||||
#include "frc/smartdashboard/Sendable.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
ComplexWidget::ComplexWidget(ShuffleboardContainer& parent,
|
||||
const wpi::Twine& title, Sendable& sendable)
|
||||
: ShuffleboardValue(title),
|
||||
ShuffleboardWidget(parent, title),
|
||||
m_sendable(sendable) {}
|
||||
|
||||
void ComplexWidget::EnableIfActuator() {
|
||||
if (m_builder.IsActuator()) {
|
||||
m_builder.StartLiveWindowMode();
|
||||
}
|
||||
}
|
||||
|
||||
void ComplexWidget::DisableIfActuator() {
|
||||
if (m_builder.IsActuator()) {
|
||||
m_builder.StopLiveWindowMode();
|
||||
}
|
||||
}
|
||||
|
||||
void ComplexWidget::BuildInto(std::shared_ptr<nt::NetworkTable> parentTable,
|
||||
std::shared_ptr<nt::NetworkTable> metaTable) {
|
||||
BuildMetadata(metaTable);
|
||||
if (!m_builderInit) {
|
||||
m_builder.SetTable(parentTable->GetSubTable(GetTitle()));
|
||||
m_sendable.InitSendable(m_builder);
|
||||
m_builder.StartListeners();
|
||||
m_builderInit = true;
|
||||
}
|
||||
m_builder.UpdateTable();
|
||||
}
|
||||
36
wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp
Normal file
36
wpilibc/src/main/native/cpp/shuffleboard/Shuffleboard.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/Shuffleboard.h"
|
||||
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
|
||||
#include "frc/shuffleboard/ShuffleboardTab.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
void Shuffleboard::Update() { GetInstance().Update(); }
|
||||
|
||||
ShuffleboardTab& Shuffleboard::GetTab(wpi::StringRef title) {
|
||||
return GetInstance().GetTab(title);
|
||||
}
|
||||
|
||||
void Shuffleboard::EnableActuatorWidgets() {
|
||||
GetInstance().EnableActuatorWidgets();
|
||||
}
|
||||
|
||||
void Shuffleboard::DisableActuatorWidgets() {
|
||||
// Need to update to make sure the sendable builders are initialized
|
||||
Update();
|
||||
GetInstance().DisableActuatorWidgets();
|
||||
}
|
||||
|
||||
detail::ShuffleboardInstance& Shuffleboard::GetInstance() {
|
||||
static detail::ShuffleboardInstance inst(
|
||||
nt::NetworkTableInstance::GetDefault());
|
||||
return inst;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/ShuffleboardComponentBase.h"
|
||||
|
||||
#include <wpi/SmallVector.h>
|
||||
|
||||
using namespace frc;
|
||||
|
||||
ShuffleboardComponentBase::ShuffleboardComponentBase(
|
||||
ShuffleboardContainer& parent, const wpi::Twine& title,
|
||||
const wpi::Twine& type)
|
||||
: ShuffleboardValue(title), m_parent(parent) {
|
||||
wpi::SmallVector<char, 16> storage;
|
||||
m_type = type.toStringRef(storage);
|
||||
}
|
||||
|
||||
void ShuffleboardComponentBase::SetType(const wpi::Twine& type) {
|
||||
wpi::SmallVector<char, 16> storage;
|
||||
m_type = type.toStringRef(storage);
|
||||
m_metadataDirty = true;
|
||||
}
|
||||
|
||||
void ShuffleboardComponentBase::BuildMetadata(
|
||||
std::shared_ptr<nt::NetworkTable> metaTable) {
|
||||
if (!m_metadataDirty) {
|
||||
return;
|
||||
}
|
||||
// Component type
|
||||
if (GetType() == "") {
|
||||
metaTable->GetEntry("PreferredComponent").Delete();
|
||||
} else {
|
||||
metaTable->GetEntry("PreferredComponent").ForceSetString(GetType());
|
||||
}
|
||||
|
||||
// Tile size
|
||||
if (m_width <= 0 || m_height <= 0) {
|
||||
metaTable->GetEntry("Size").Delete();
|
||||
} else {
|
||||
metaTable->GetEntry("Size").SetDoubleArray(
|
||||
{static_cast<double>(m_width), static_cast<double>(m_height)});
|
||||
}
|
||||
|
||||
// Tile position
|
||||
if (m_column < 0 || m_row < 0) {
|
||||
metaTable->GetEntry("Position").Delete();
|
||||
} else {
|
||||
metaTable->GetEntry("Position")
|
||||
.SetDoubleArray(
|
||||
{static_cast<double>(m_column), static_cast<double>(m_row)});
|
||||
}
|
||||
|
||||
// Custom properties
|
||||
if (GetProperties().size() > 0) {
|
||||
auto propTable = metaTable->GetSubTable("Properties");
|
||||
for (auto& entry : GetProperties()) {
|
||||
propTable->GetEntry(entry.first()).SetValue(entry.second);
|
||||
}
|
||||
}
|
||||
m_metadataDirty = false;
|
||||
}
|
||||
|
||||
ShuffleboardContainer& ShuffleboardComponentBase::GetParent() {
|
||||
return m_parent;
|
||||
}
|
||||
|
||||
const std::string& ShuffleboardComponentBase::GetType() const { return m_type; }
|
||||
|
||||
const wpi::StringMap<std::shared_ptr<nt::Value>>&
|
||||
ShuffleboardComponentBase::GetProperties() const {
|
||||
return m_properties;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/ShuffleboardContainer.h"
|
||||
|
||||
#include <wpi/SmallVector.h>
|
||||
#include <wpi/raw_ostream.h>
|
||||
|
||||
#include "frc/shuffleboard/ComplexWidget.h"
|
||||
#include "frc/shuffleboard/ShuffleboardComponent.h"
|
||||
#include "frc/shuffleboard/ShuffleboardLayout.h"
|
||||
#include "frc/shuffleboard/SimpleWidget.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
ShuffleboardContainer::ShuffleboardContainer(const wpi::Twine& title)
|
||||
: ShuffleboardValue(title) {}
|
||||
|
||||
const std::vector<std::unique_ptr<ShuffleboardComponentBase>>&
|
||||
ShuffleboardContainer::GetComponents() const {
|
||||
return m_components;
|
||||
}
|
||||
|
||||
ShuffleboardLayout& ShuffleboardContainer::GetLayout(const wpi::Twine& type,
|
||||
const wpi::Twine& title) {
|
||||
wpi::SmallVector<char, 16> storage;
|
||||
auto titleRef = title.toStringRef(storage);
|
||||
if (m_layouts.count(titleRef) == 0) {
|
||||
auto layout = std::make_unique<ShuffleboardLayout>(*this, type, titleRef);
|
||||
auto ptr = layout.get();
|
||||
m_components.emplace_back(std::move(layout));
|
||||
m_layouts.insert(std::make_pair(titleRef, ptr));
|
||||
}
|
||||
return *m_layouts[titleRef];
|
||||
}
|
||||
|
||||
ComplexWidget& ShuffleboardContainer::Add(const wpi::Twine& title,
|
||||
Sendable& sendable) {
|
||||
CheckTitle(title);
|
||||
auto widget = std::make_unique<ComplexWidget>(*this, title, sendable);
|
||||
auto ptr = widget.get();
|
||||
m_components.emplace_back(std::move(widget));
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
ComplexWidget& ShuffleboardContainer::Add(Sendable& sendable) {
|
||||
if (sendable.GetName().empty()) {
|
||||
wpi::outs() << "Sendable must have a name\n";
|
||||
}
|
||||
return Add(sendable.GetName(), sendable);
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(
|
||||
const wpi::Twine& title, std::shared_ptr<nt::Value> defaultValue) {
|
||||
CheckTitle(title);
|
||||
|
||||
auto widget = std::make_unique<SimpleWidget>(*this, title);
|
||||
auto ptr = widget.get();
|
||||
widget->GetEntry().SetDefaultValue(defaultValue);
|
||||
m_components.emplace_back(std::move(widget));
|
||||
return *ptr;
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title,
|
||||
bool defaultValue) {
|
||||
return Add(title, nt::Value::MakeBoolean(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title,
|
||||
double defaultValue) {
|
||||
return Add(title, nt::Value::MakeDouble(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title,
|
||||
int defaultValue) {
|
||||
return Add(title, nt::Value::MakeDouble(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title,
|
||||
const wpi::Twine& defaultValue) {
|
||||
return Add(title, nt::Value::MakeString(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title,
|
||||
wpi::ArrayRef<bool> defaultValue) {
|
||||
return Add(title, nt::Value::MakeBooleanArray(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(const wpi::Twine& title,
|
||||
wpi::ArrayRef<double> defaultValue) {
|
||||
return Add(title, nt::Value::MakeDoubleArray(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::Add(
|
||||
const wpi::Twine& title, wpi::ArrayRef<std::string> defaultValue) {
|
||||
return Add(title, nt::Value::MakeStringArray(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(
|
||||
const wpi::Twine& title, std::shared_ptr<nt::Value> defaultValue) {
|
||||
auto& widget = Add(title, defaultValue);
|
||||
widget.GetEntry().SetPersistent();
|
||||
return widget;
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(const wpi::Twine& title,
|
||||
bool defaultValue) {
|
||||
return AddPersistent(title, nt::Value::MakeBoolean(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(const wpi::Twine& title,
|
||||
double defaultValue) {
|
||||
return AddPersistent(title, nt::Value::MakeDouble(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(const wpi::Twine& title,
|
||||
int defaultValue) {
|
||||
return AddPersistent(title, nt::Value::MakeDouble(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(
|
||||
const wpi::Twine& title, const wpi::Twine& defaultValue) {
|
||||
return AddPersistent(title, nt::Value::MakeString(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(
|
||||
const wpi::Twine& title, wpi::ArrayRef<bool> defaultValue) {
|
||||
return AddPersistent(title, nt::Value::MakeBooleanArray(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(
|
||||
const wpi::Twine& title, wpi::ArrayRef<double> defaultValue) {
|
||||
return AddPersistent(title, nt::Value::MakeDoubleArray(defaultValue));
|
||||
}
|
||||
|
||||
SimpleWidget& ShuffleboardContainer::AddPersistent(
|
||||
const wpi::Twine& title, wpi::ArrayRef<std::string> defaultValue) {
|
||||
return AddPersistent(title, nt::Value::MakeStringArray(defaultValue));
|
||||
}
|
||||
|
||||
void ShuffleboardContainer::EnableIfActuator() {
|
||||
for (auto& component : GetComponents()) {
|
||||
component->EnableIfActuator();
|
||||
}
|
||||
}
|
||||
|
||||
void ShuffleboardContainer::DisableIfActuator() {
|
||||
for (auto& component : GetComponents()) {
|
||||
component->DisableIfActuator();
|
||||
}
|
||||
}
|
||||
|
||||
void ShuffleboardContainer::CheckTitle(const wpi::Twine& title) {
|
||||
wpi::SmallVector<char, 16> storage;
|
||||
auto titleRef = title.toStringRef(storage);
|
||||
if (m_usedTitles.count(titleRef) > 0) {
|
||||
wpi::errs() << "Title is already in use: " << title << "\n";
|
||||
return;
|
||||
}
|
||||
m_usedTitles.insert(titleRef);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/ShuffleboardInstance.h"
|
||||
|
||||
#include <networktables/NetworkTable.h>
|
||||
#include <networktables/NetworkTableInstance.h>
|
||||
#include <wpi/StringMap.h>
|
||||
|
||||
#include "frc/shuffleboard/Shuffleboard.h"
|
||||
|
||||
using namespace frc::detail;
|
||||
|
||||
struct ShuffleboardInstance::Impl {
|
||||
wpi::StringMap<ShuffleboardTab> tabs;
|
||||
|
||||
bool tabsChanged = false;
|
||||
std::shared_ptr<nt::NetworkTable> rootTable;
|
||||
std::shared_ptr<nt::NetworkTable> rootMetaTable;
|
||||
};
|
||||
|
||||
ShuffleboardInstance::ShuffleboardInstance(nt::NetworkTableInstance ntInstance)
|
||||
: m_impl(new Impl) {
|
||||
m_impl->rootTable = ntInstance.GetTable(Shuffleboard::kBaseTableName);
|
||||
m_impl->rootMetaTable = m_impl->rootTable->GetSubTable(".metadata");
|
||||
}
|
||||
|
||||
ShuffleboardInstance::~ShuffleboardInstance() {}
|
||||
|
||||
frc::ShuffleboardTab& ShuffleboardInstance::GetTab(wpi::StringRef title) {
|
||||
if (m_impl->tabs.find(title) == m_impl->tabs.end()) {
|
||||
m_impl->tabs.try_emplace(title, ShuffleboardTab(*this, title));
|
||||
m_impl->tabsChanged = true;
|
||||
}
|
||||
return m_impl->tabs.find(title)->second;
|
||||
}
|
||||
|
||||
void ShuffleboardInstance::Update() {
|
||||
if (m_impl->tabsChanged) {
|
||||
std::vector<std::string> tabTitles;
|
||||
for (auto& entry : m_impl->tabs) {
|
||||
tabTitles.emplace_back(entry.second.GetTitle());
|
||||
}
|
||||
m_impl->rootMetaTable->GetEntry("Tabs").ForceSetStringArray(tabTitles);
|
||||
m_impl->tabsChanged = false;
|
||||
}
|
||||
for (auto& entry : m_impl->tabs) {
|
||||
auto& tab = entry.second;
|
||||
tab.BuildInto(m_impl->rootTable,
|
||||
m_impl->rootMetaTable->GetSubTable(tab.GetTitle()));
|
||||
}
|
||||
}
|
||||
|
||||
void ShuffleboardInstance::EnableActuatorWidgets() {
|
||||
for (auto& entry : m_impl->tabs) {
|
||||
auto& tab = entry.second;
|
||||
for (auto& component : tab.GetComponents()) {
|
||||
component->EnableIfActuator();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ShuffleboardInstance::DisableActuatorWidgets() {
|
||||
for (auto& entry : m_impl->tabs) {
|
||||
auto& tab = entry.second;
|
||||
for (auto& component : tab.GetComponents()) {
|
||||
component->DisableIfActuator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/ShuffleboardLayout.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
ShuffleboardLayout::ShuffleboardLayout(ShuffleboardContainer& parent,
|
||||
const wpi::Twine& name,
|
||||
const wpi::Twine& type)
|
||||
: ShuffleboardValue(name),
|
||||
ShuffleboardComponent(parent, type, name),
|
||||
ShuffleboardContainer(name) {
|
||||
m_isLayout = true;
|
||||
}
|
||||
|
||||
void ShuffleboardLayout::BuildInto(
|
||||
std::shared_ptr<nt::NetworkTable> parentTable,
|
||||
std::shared_ptr<nt::NetworkTable> metaTable) {
|
||||
BuildMetadata(metaTable);
|
||||
auto table = parentTable->GetSubTable(GetTitle());
|
||||
table->GetEntry(".type").SetString("ShuffleboardLayout");
|
||||
for (auto& component : GetComponents()) {
|
||||
component->BuildInto(table, metaTable->GetSubTable(component->GetTitle()));
|
||||
}
|
||||
}
|
||||
25
wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardTab.cpp
Normal file
25
wpilibc/src/main/native/cpp/shuffleboard/ShuffleboardTab.cpp
Normal file
@@ -0,0 +1,25 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/ShuffleboardTab.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
ShuffleboardTab::ShuffleboardTab(ShuffleboardRoot& root, wpi::StringRef title)
|
||||
: ShuffleboardValue(title), ShuffleboardContainer(title), m_root(root) {}
|
||||
|
||||
ShuffleboardRoot& ShuffleboardTab::GetRoot() { return m_root; }
|
||||
|
||||
void ShuffleboardTab::BuildInto(std::shared_ptr<nt::NetworkTable> parentTable,
|
||||
std::shared_ptr<nt::NetworkTable> metaTable) {
|
||||
auto tabTable = parentTable->GetSubTable(GetTitle());
|
||||
tabTable->GetEntry(".type").SetString("ShuffleboardTab");
|
||||
for (auto& component : GetComponents()) {
|
||||
component->BuildInto(tabTable,
|
||||
metaTable->GetSubTable(component->GetTitle()));
|
||||
}
|
||||
}
|
||||
46
wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp
Normal file
46
wpilibc/src/main/native/cpp/shuffleboard/SimpleWidget.cpp
Normal file
@@ -0,0 +1,46 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2018 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
#include "frc/shuffleboard/SimpleWidget.h"
|
||||
|
||||
#include "frc/shuffleboard/Shuffleboard.h"
|
||||
#include "frc/shuffleboard/ShuffleboardLayout.h"
|
||||
#include "frc/shuffleboard/ShuffleboardTab.h"
|
||||
|
||||
using namespace frc;
|
||||
|
||||
SimpleWidget::SimpleWidget(ShuffleboardContainer& parent,
|
||||
const wpi::Twine& title)
|
||||
: ShuffleboardValue(title), ShuffleboardWidget(parent, title) {}
|
||||
|
||||
nt::NetworkTableEntry SimpleWidget::GetEntry() {
|
||||
if (!m_entryInitialized) {
|
||||
ForceGenerate();
|
||||
m_entryInitialized = true;
|
||||
}
|
||||
return m_entry;
|
||||
}
|
||||
|
||||
void SimpleWidget::BuildInto(std::shared_ptr<nt::NetworkTable> parentTable,
|
||||
std::shared_ptr<nt::NetworkTable> metaTable) {
|
||||
BuildMetadata(metaTable);
|
||||
if (!m_entryInitialized) {
|
||||
m_entry = parentTable->GetEntry(GetTitle());
|
||||
m_entryInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleWidget::ForceGenerate() {
|
||||
ShuffleboardContainer* parent = &GetParent();
|
||||
|
||||
while (parent->m_isLayout) {
|
||||
parent = &(static_cast<ShuffleboardLayout*>(parent)->GetParent());
|
||||
}
|
||||
|
||||
auto& tab = *static_cast<ShuffleboardTab*>(parent);
|
||||
tab.GetRoot().Update();
|
||||
}
|
||||
Reference in New Issue
Block a user