Skip to content
Snippets Groups Projects
Commit 4b5e8c3f authored by Sebastian Pape's avatar Sebastian Pape
Browse files

Initial Commit

parents
Branches
No related tags found
No related merge requests found
Showing
with 1393 additions and 0 deletions
*.lib filter=lfs diff=lfs merge=lfs -text
*.uasset filter=lfs diff=lfs merge=lfs -text
*.umap filter=lfs diff=lfs merge=lfs -text
# Visual Studio 2015 user specific files
.vs/
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
# Compiled Static libraries
*.lai
*.la
*.a
# *.lib
# Executables
*.exe
*.out
*.app
*.ipa
# These project files can be generated by the engine
*.xcodeproj
*.xcworkspace
*.sln
*.suo
*.opensdf
*.sdf
*.VC.db
*.VC.opendb
# Precompiled Assets
SourceArt/**/*.png
SourceArt/**/*.tga
# Binary Files
Binaries/*
Plugins/*/Binaries/*
# Builds
Build/*
# Whitelist PakBlacklist-<BuildConfiguration>.txt files
!Build/*/
Build/*/**
!Build/*/PakBlacklist*.txt
# Don't ignore icon files in Build
!Build/**/*.ico
# Built data for maps
*_BuiltData.uasset
# Configuration files generated by the Editor
Saved/*
# Compiled source files for the engine to use
Intermediate/*
Plugins/*/Intermediate/*
# Cache files for the editor to use
DerivedDataCache/*
/Credentials.json
#Include Files installed by makefile
/Source/ThirdParty/Include
/Source/Thirdparty/build
[submodule "Source/Thirdparty/Dasher/DasherCore"]
path = Source/Thirdparty/Dasher/DasherCore
url = https://github.com/VRGroupRWTH/DasherCore.git
branch = master
[FilterPlugin]
; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and
; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively.
;
; Examples:
; /README.txt
; /Extras/...
; /Binaries/ThirdParty/*.dll
{
"FileVersion": 3,
"Version": 1,
"VersionName": "1.0",
"FriendlyName": "DasherVR",
"Description": "",
"Category": "Other",
"CreatedBy": "",
"CreatedByURL": "",
"DocsURL": "",
"MarketplaceURL": "",
"SupportURL": "",
"CanContainContent": true,
"IsBetaVersion": false,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "DasherVR",
"Type": "Runtime",
"LoadingPhase": "PreDefault"
}
]
}
# Dasher VR
## Linux Installation
Make sure to get all submodules e.g. by using `git submodule update --init --recursive`.
Run the install.sh script or use the cmake file and make install. This should deploy all headers and the library to their corresponding folders.
\ No newline at end of file
Resources/Icon128.png

12.4 KiB

// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class DasherVR : ModuleRules
{
public DasherVR(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PrivateDefinitions.Add("HAVE_OWN_FILEUTILS");
PublicIncludePaths.AddRange(
new string[] {
// ... add public include paths required here ...
}
);
PrivateIncludePaths.AddRange(
new string[] {
// ... add other private include paths required here ...
}
);
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"Dasher",
"InputCore",
"DeveloperSettings"
// ... add other public dependencies that you statically link with here ...
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"UMG",
"RenderCore"
// ... add private dependencies that you statically link with here ...
}
);
DynamicallyLoadedModuleNames.AddRange(
new string[]
{
// ... add any modules that your module loads dynamically here ...
}
);
}
}
#include "CoreMinimal.h"
#include "FileUtils.h"
#include "HAL/FileManagerGeneric.h"
#include "Misc/FileHelper.h"
namespace Dasher
{
int FileUtils::GetFileSize(const std::string& strFileName)
{
return IFileManager::Get().FileSize(UTF8_TO_TCHAR(strFileName.c_str()));
}
//This doesn't actually Parse all files with the pattern, just the file with exactly the name strPattern in the Project directory
void FileUtils::ScanFiles(AbstractParser* parser, const std::string& strPattern)
{
const FString SearchPath = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir(), TEXT("DasherResources"));
//Check for explicit file
if(IFileManager::Get().FileExists(*FPaths::Combine(SearchPath, FString(strPattern.c_str()))))
{
parser->ParseFile(TCHAR_TO_UTF8(*FPaths::Combine(SearchPath, FString(strPattern.c_str()))), true);
return;
}
//Search files
TArray<FString> Filenames;
IFileManager::Get().FindFiles(Filenames, *SearchPath);
FString alteredPattern = FString(strPattern.c_str()).Replace(TEXT("*"), TEXT(".*"));
for(FString Filename : Filenames)
{
if(FRegexMatcher(FRegexPattern(alteredPattern), Filename).FindNext())
{
parser->ParseFile(TCHAR_TO_UTF8(*FPaths::Combine(SearchPath, Filename)), true);
}
}
}
bool FileUtils::WriteUserDataFile(const std::string& filename, const std::string& strNewText, bool append)
{
if (strNewText.length() == 0) return true;
return FFileHelper::SaveStringToFile(FString(strNewText.c_str()), UTF8_TO_TCHAR(filename.c_str()), FFileHelper::EEncodingOptions::AutoDetect, &IFileManager::Get(), (append) ? FILEWRITE_Append : FILEWRITE_None);
}
std::string FileUtils::GetFullFilenamePath(const std::string strFilename){
FString Fullpath = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir(), UTF8_TO_TCHAR(strFilename.c_str()));
return TCHAR_TO_UTF8(*Fullpath);
}
}
#include "DasherInterface.h"
#include "SDasherWidget.h"
namespace Dasher
{
//Constructor
DasherInterface::DasherInterface(CSettingsStore * settings) : CDashIntfScreenMsgs(settings){}
void DasherInterface::ImportTrainingFile(std::string filename)
{
ImportTrainingText(filename);
}
FVector2D DasherInterface::ConvertDasher2Screen(myint InputX, myint InputY)
{
Dasher::screenint x, y;
GetView()->Dasher2Screen(InputX, InputY,x,y);
return FVector2D(x, y);
}
std::string DasherInterface::GetContext(unsigned int iStart, unsigned int iLength)
{
const FString output = Buffer.Mid(iStart, iLength);
return TCHAR_TO_UTF8(*output);
}
//The next functions operate on the buffer
//For now can only return one character around the cursor
std::string DasherInterface::GetTextAroundCursor(Dasher::CControlManager::EditDistance iDist)
{
if (Buffer.Len()>Cursor)
{
if (iDist == Dasher::CControlManager::EditDistance::EDIT_CHAR)
{
const FString Output = Buffer.Mid(Cursor, 1);
return TCHAR_TO_UTF8(*Output);
}
else
{
UE_LOG(LogTemp, Error, TEXT("This is not Implemented"));
return std::string("tried to get more than just a char");
}
}
UE_LOG(LogTemp, Error, TEXT("Cursor out of bounds"));
return std::string("Cursor out of bounds");
}
unsigned int DasherInterface::ctrlMove(bool bForwards, Dasher::CControlManager::EditDistance dist)
{
if (dist == Dasher::CControlManager::EditDistance::EDIT_CHAR)
{
if (bForwards) Cursor++;
else Cursor--;
}
return Cursor;
}
unsigned int DasherInterface::ctrlDelete(bool bForwards, Dasher::CControlManager::EditDistance dist)
{
if (dist == Dasher::CControlManager::EditDistance::EDIT_CHAR)
{
const int Index = Cursor - (bForwards ? 0 : 1);
const FString DeletedChar = Buffer.Mid(Index, 1);
Buffer.RemoveAt(Index, 1, false);
if(!bForwards) Cursor--;
if(CharacterDeleted)
{
CharacterDeleted(DeletedChar, Buffer);
}
}
return Cursor;
}
void DasherInterface::editOutput(const std::string& strText, CDasherNode* pNode)
{
if (static_cast<int>(Buffer.GetAllocatedSize()) - static_cast<int>(strText.length()) - Buffer.Len() < 0)
{
Buffer.Reserve(Buffer.GetAllocatedSize() * 2);
}
const FString Text(UTF8_TO_TCHAR(strText.c_str()));
for(TCHAR StringChar : Text)
{
Buffer.AppendChar(StringChar);
Cursor++;
// Broadcast each character
if(CharacterEntered)
{
CharacterEntered(FString(1, &StringChar), Buffer);
}
}
CDasherInterfaceBase::editOutput(strText, pNode);
}
std::string DasherInterface::GetAllContext()
{
const FString Output = Buffer;
Buffer.Empty();
return std::string(TCHAR_TO_UTF8(*Output));
}
void DasherInterface::editDelete(const std::string& strText, CDasherNode* pNode)
{
const FString Text(UTF8_TO_TCHAR(strText.c_str()));
//Reverse Iterate
for(TCHAR StringChar : Text.Reverse())
{
const FString SingleCharString = FString(1, &StringChar);
if(Buffer.RemoveFromEnd(SingleCharString))
{
Cursor--;
if(CharacterDeleted) CharacterDeleted(SingleCharString, Buffer);
}
}
CDasherInterfaceBase::editDelete(strText, pNode);
}
void DasherInterface::Tick(unsigned long time)
{
NewFrame(time, true);
}
void DasherInterface::SetScreen(SDasherWidget* screen)
{
//set the widget as screen that dasher will be displayed on
ChangeScreen(screen);
//set up stuff that can't be done in the constructor (for explanation see comments in DasherInterfaceBase.h etc. in DasherCore)
Realize(0);
}
FString DasherInterface::GetBuffer() const
{
return Buffer;
}
void DasherInterface::ResetBuffer()
{
Buffer = "";
SetBuffer(0);
}
}
// Copyright Epic Games, Inc. All Rights Reserved.
#include "DasherVR.h"
#include "Engine.h"
#define LOCTEXT_NAMESPACE "FDasherVRModule"
void FDasherVRModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
//Currently nothing has to be done here
}
void FDasherVRModule::ShutdownModule()
{
// This function may be called during shutdown to clean up your module. For modules that support dynamic reloading,
// we call this function before unloading the module.
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FDasherVRModule, DasherVR)
\ No newline at end of file
#include "SDasherWidget.h"
#include "SlateOptMacros.h"
#include "Styling/CoreStyle.h"
#include "Brushes/SlateColorBrush.h"
#include "Rendering/DrawElements.h"
#include "DasherInterface.h"
#include "Components/SlateWrapperTypes.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
// ++ This is needed in order to use the localization macro LOCTEXT
#define LOCTEXT_NAMESPACE "SStandardSlateWidget"
//Set the state if we're in the editor or not.
void SDasherWidget::SetEditor(bool EditorState)
{
IsEditor = EditorState;
}
//Event Handlers
//Mouse position saved for mouse Input
FReply SDasherWidget::HandleMouseMoveEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent)
{
if (CurrentlyUsingVectorInput) return FReply::Unhandled();
//The mouse event only contains the Screen Space Position
CursorPosition = Geometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
return FReply::Handled();
}
FReply SDasherWidget::HandleMouseDownEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent)
{
if (CurrentlyUsingVectorInput)
{
CurrentlyUsingVectorInput = false;
return FReply::Handled().LockMouseToWidget(AsShared());
}
DasherMainInterface->KeyDown(FDateTime::Now().GetSecond() + FDateTime::Now().GetMillisecond(), 100); //100 is the keycode for LMB
MouseDownListeners.ExecuteIfBound();
return FReply::Handled().LockMouseToWidget(AsShared());
}
FReply SDasherWidget::HandleMouseUpEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent)
{
if (CurrentlyUsingVectorInput)
{
CurrentlyUsingVectorInput = false;
return FReply::Handled().ReleaseMouseLock();
}
DasherMainInterface->KeyUp(FDateTime::Now().GetSecond() + FDateTime::Now().GetMillisecond(), 100); //100 is the keycode for LMB
MouseUpListeners.ExecuteIfBound();
return FReply::Handled().ReleaseMouseLock();
}
FReply SDasherWidget::HandleMouseDoubleClickEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent)
{
if(CurrentlyUsingVectorInput)
{
CurrentlyUsingVectorInput = false;
return FReply::Handled();
}
DasherMainInterface->KeyUp(FDateTime::Now().GetSecond() + FDateTime::Now().GetMillisecond(), 100); //100 is the keycode for LMB
return FReply::Handled();
}
//Set in the HandleMouseMoveEvent or via Set InputVector
//See tick for geometry things that could be use to modify this.
FVector2D SDasherWidget::GetCursorPosition()
{
return CursorPosition;
}
bool SDasherWidget::GetScreenCoords(screenint& iX, screenint& iY, Dasher::CDasherView* pView)
{
const FVector2D Position = GetCursorPosition();
iX = Position.X;
iY = Position.Y;
return true;
}
void SDasherWidget::InputVector(const FVector2D InputVector)
{
if (!CurrentlyUsingVectorInput) return;
CursorPosition = DasherMainInterface->ConvertDasher2Screen(Dasher::CDasherModel::ORIGIN_X, Dasher::CDasherModel::ORIGIN_Y)
+ InputVector * FVector2D(1.0f,-1.0f) * (GetHeight() / 2);
}
void SDasherWidget::InputButton(bool Pressed)
{
CurrentlyUsingVectorInput = true;
if(Pressed)
{
DasherMainInterface->KeyDown(FDateTime::Now().GetSecond() + FDateTime::Now().GetMillisecond(), 100); //100 is the keycode for LMB
}
else {
DasherMainInterface->KeyUp(FDateTime::Now().GetSecond() + FDateTime::Now().GetMillisecond(), 100); //100 is the keycode for LMB
}
}
//The construction of the Dasher Widget, Dasher parameters are set here, a function to set them outside is not provided, but can be easily implemented
void SDasherWidget::Construct(const FArguments& InArgs)
{
//Initial resize, needed for setup
Width = InArgs._width;
Height = InArgs._height;
resize(Width, Height);
//initialize the font measuring service.
FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
//Setting up Dasher
static Dasher::XMLErrorDisplay display;
Dasher::XmlSettingsStore* Settings = new Dasher::XmlSettingsStore("Settings.xml"/*, &fileUtils*/, &display); //Gets deleted somewhere else
Settings->Load();
Settings->Save();
DasherMainInterface = MakeShared<Dasher::DasherInterface>(Settings);
DasherMainInterface->SetDefaultInputDevice(this);
//change dasher parameters
DasherMainInterface->SetBoolParameter(BP_AUTO_SPEEDCONTROL, false); //Auto Speed Control
DasherMainInterface->SetLongParameter(LP_DASHER_FONTSIZE, 18);
DasherMainInterface->SetLongParameter(LP_MAX_BITRATE, 400); //Maximum Speed
DasherMainInterface->SetStringParameter(SP_INPUT_FILTER, "Stylus Control"); //On Hold
DasherMainInterface->SetStringParameter(SP_ALPHABET_ID, "German without punctuation");
DasherMainInterface->SetLongParameter(LP_MIN_NODE_SIZE, 15);
DasherMainInterface->SetScreen(this);
DasherMainInterface->SetBuffer(0);
DasherMainInterface->SetCharEnteredCallback([this](FString Char, FString Buffer){CharacterEntered.ExecuteIfBound(Char, Buffer);});
DasherMainInterface->SetCharDeletedCallback([this](FString Char, FString Buffer){CharacterDeleted.ExecuteIfBound(Char, Buffer);});
//Set up the Event Handlers for Mouse Movement etc.
SetOnMouseMove(FPointerEventHandler::CreateLambda([this](const FGeometry& Geometry, const FPointerEvent& MouseEvent) {return HandleMouseMoveEvent(Geometry, MouseEvent) ; }));
SetOnMouseButtonDown(FPointerEventHandler::CreateLambda([this](const FGeometry& Geometry, const FPointerEvent& MouseEvent) {return HandleMouseDownEvent(Geometry, MouseEvent); }));
SetOnMouseButtonUp(FPointerEventHandler::CreateLambda([this](const FGeometry& Geometry, const FPointerEvent& MouseEvent) {return HandleMouseUpEvent(Geometry, MouseEvent); }));
SetOnMouseDoubleClick(FPointerEventHandler::CreateLambda([this](const FGeometry& Geometry, const FPointerEvent& MouseEvent) {return HandleMouseDoubleClickEvent(Geometry, MouseEvent); })); //We treat a double click of the mouse as a lift of the mouse button.
//You could add Slate Code here for child slots etc.
}
void SDasherWidget::SetBoolParamter(FString ParameterName, bool Value)
{
for(Dasher::Settings::bp_table Setting : Dasher::Settings::boolparamtable)
{
if(FString(Setting.regName).Compare(ParameterName, ESearchCase::IgnoreCase))
{
DasherMainInterface->SetBoolParameter(Setting.key, Value);
return;
}
}
}
void SDasherWidget::SetLongParamter(FString ParameterName, int64 Value)
{
for(Dasher::Settings::bp_table Setting : Dasher::Settings::boolparamtable)
{
if(FString(Setting.regName).Compare(ParameterName, ESearchCase::IgnoreCase))
{
DasherMainInterface->SetLongParameter(Setting.key, Value);
return;
}
}
}
void SDasherWidget::SetStringParamter(FString ParameterName, FString Value)
{
for(Dasher::Settings::bp_table Setting : Dasher::Settings::boolparamtable)
{
if(FString(Setting.regName).Compare(ParameterName, ESearchCase::IgnoreCase))
{
DasherMainInterface->SetStringParameter(Setting.key, TCHAR_TO_UTF8(*Value));
return;
}
}
}
//Set the colour Scheme for dasher
void SDasherWidget::SetColourScheme(const Dasher::CColourIO::ColourInfo* pcolours) {
ColorPalette = pcolours;
}
//paints our stuff, then lets compoundwidget (super::) draw its stuff
//This draws from bottom to top rectangles->lines->labels, this is enough for our use, but for more complex scenarios a proper layering system might have to be implemented
int32 SDasherWidget::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
//this doesn't draw anything red, we just need a brush. Could probably find a better "empty" brush
auto MyBrush = FSlateColorBrush(FColor::Red);
FFilledRect* RectObject;
FWriting* WritingObject;
FPolyLine* LineObject;
FVector2D Size;
FString Text;
FSlateFontInfo Font;
TArray<TUniquePtr<DasherDrawGeometry>>& GeometryBuffer = *FrontBuffer;
for(TUniquePtr<DasherDrawGeometry>& GeneralObject : GeometryBuffer) {
switch(GeneralObject->Type)
{
case Rectangle:
RectObject = static_cast<FFilledRect*>(GeneralObject.Get());
Size = {FMath::Abs(RectObject->top.X - RectObject->bottom.X), FMath::Abs(RectObject->top.Y - RectObject->bottom.Y)};
FSlateDrawElement::MakeBox(OutDrawElements, LayerId++, AllottedGeometry.ToPaintGeometry(Size, FSlateLayoutTransform(1, RectObject->top)), &MyBrush, ESlateDrawEffect::None, RectObject->color); //actually adds the box to the geometry
break;
case Writing:
WritingObject = static_cast<FWriting*>(GeneralObject.Get());
Text = FString(UTF8_TO_TCHAR(WritingObject->label->m_strText.c_str()));
Font = FCoreStyle::GetDefaultFontStyle("Roboto", WritingObject->size, FFontOutlineSettings::NoOutline); //get the font
FSlateDrawElement::MakeText(OutDrawElements, LayerId++, AllottedGeometry.ToPaintGeometry(FSlateLayoutTransform(1, FVector2D(WritingObject->pos.X, WritingObject->pos.Y))), Text , Font, ESlateDrawEffect::None, WritingObject->color);
break;
case PolyLine:
LineObject = static_cast<FPolyLine*>(GeneralObject.Get());
FSlateDrawElement::MakeLines(OutDrawElements, LayerId++, AllottedGeometry.ToPaintGeometry(), LineObject->points, ESlateDrawEffect::None, LineObject->color, true, LineObject->linewidth);
break;
default: break;
}
}
return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); //call the parent onPaint
}
//The tick function, we tick dasher in it and update the screen size for dasher
void SDasherWidget::Tick(const FGeometry& AllotedGeometry, const double InCurrentTime, const float InDeltaTime) {
//don't tick in the editor
if (!IsEditor) {
SCompoundWidget::Tick(AllotedGeometry, InCurrentTime, InDeltaTime);
DasherMainInterface->Tick(static_cast<unsigned long>(InCurrentTime * 1000.0)); //we need to provide ticks in milliseconds
//This probably doesn't have to be done every tick, but it does not seem to have a huge hit on performance
const FGeometry Geometry = GetTickSpaceGeometry();
const FIntPoint AbsoluteSize = Geometry.Size.IntPoint();
if (Width != AbsoluteSize.X || Height != AbsoluteSize.Y) {
Width = AbsoluteSize.X;
Height = AbsoluteSize.Y;
resize(Width, Height);
//tell dasher we resized the screen, but only if there's actually a screen
if (Width && Height) DasherMainInterface->ScreenResized(this);
};
}
}
std::pair<SDasherWidget::screenint, SDasherWidget::screenint> SDasherWidget::TextSize(CDasherScreen::Label* Label, unsigned Size)
{
const FSlateFontInfo Font = FCoreStyle::GetDefaultFontStyle("Roboto", Size, FFontOutlineSettings::NoOutline); //get the font
const FVector2D TextSize = FontMeasureService->Measure(FString(UTF8_TO_TCHAR(Label->m_strText.c_str())), Font, 1); //get the real size of the text, using the fontmeasuring service
return {static_cast<screenint>(TextSize.X), static_cast<screenint>(TextSize.Y)};
}
//Double Buffers are rotated here.
void SDasherWidget::Display() {
std::swap(FrontBuffer, BackBuffer);
BackBuffer->Empty();
}
//Functions for Drawing
void SDasherWidget::DrawRectangle(Dasher::screenint x1, Dasher::screenint y1, Dasher::screenint x2, Dasher::screenint y2, int colour, int iOutlineColour, int iThickness)
{
const FVector2D Top(x1, y1);
const FVector2D Bottom(x2, y2);
FLinearColor RectColor;
if (ColorPalette) {
RectColor = FLinearColor(ColorPalette->Colors[colour].Red/255.0, ColorPalette->Colors[colour].Green / 255.0, ColorPalette->Colors[colour].Blue / 255.0);
}
else {
RectColor = FLinearColor::Blue;
}
BackBuffer->Add(MakeUnique<FFilledRect>(Top, Bottom, RectColor));
}
void SDasherWidget::DrawString(CDasherScreen::Label* lab, screenint x1, screenint y1, unsigned int iSize, int colour) {
const FVector2D Pos(x1, y1);
FLinearColor TextColor;
if (ColorPalette) {
TextColor = FLinearColor(ColorPalette->Colors[colour].Red / 255.0, ColorPalette->Colors[colour].Green / 255.0, ColorPalette->Colors[colour].Blue / 255.0);
}
else {
TextColor = FLinearColor::Black;
}
BackBuffer->Add(MakeUnique<FWriting>(lab, Pos, static_cast<int>(iSize), TextColor));
}
void SDasherWidget::Polyline(CDasherScreen::point* points, int number, int iwidth, int colour) {
FLinearColor LinearColor;
if (ColorPalette) {
LinearColor = FLinearColor(ColorPalette->Colors[colour].Red / 255.0, ColorPalette->Colors[colour].Green / 255.0, ColorPalette->Colors[colour].Blue / 255.0);
}
else {
LinearColor = FLinearColor::Blue;
}
TArray<FVector2D> PointArray;
for (int i = 0; i < number; i++) {
FVector2D Point(points[i].x, points[i].y);
PointArray.Add(Point);
}
BackBuffer->Add(MakeUnique<FPolyLine>(PointArray, static_cast<float>(iwidth), LinearColor));
}
//techincally polygons are just multiple polylines. Dasher doesn't actually draw polygons in our case.
void SDasherWidget::Polygon(CDasherScreen::point* points, int number, int fillcolour, int outlinecolour, int iwidth) {
FLinearColor LinearColor;
if (ColorPalette) {
LinearColor = FLinearColor(ColorPalette->Colors[outlinecolour].Red / 255.0, ColorPalette->Colors[outlinecolour].Green / 255.0, ColorPalette->Colors[outlinecolour].Blue / 255.0);
}
else {
LinearColor = FLinearColor::Blue;
}
TArray<FVector2D> PointArray;
for (int i = 0; i < number; i++) {
FVector2D Point(points[i].x, points[i].y);
PointArray.Add(Point);
}
PointArray.Add(FVector2D(points[0].x, points[0].y));
BackBuffer->Add(MakeUnique<FPolyLine>(PointArray, static_cast<float>(iwidth), LinearColor));
}
//We pass through the contents of the dasher buffer
FString SDasherWidget::GetBuffer() const
{
return DasherMainInterface->GetBuffer();
}
void SDasherWidget::ResetBuffer()
{
DasherMainInterface->ResetBuffer();
}
void SDasherWidget::StartTraining(FString PathToTextFile)
{
DasherMainInterface->ImportTrainingFile(TCHAR_TO_UTF8(*FPaths::ConvertRelativePathToFull(PathToTextFile)));
}
// ++ We need to undefine this namespace after we finish creating the Slate widget
#undef LOCTEXT_NAMESPACE
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
/*
Adapted from JoySoftEdgeImage by Rama https://nerivec.github.io/old-ue4-wiki/pages/umg-custom-widget-components-and-render-code-usable-in-umg-designer.html
*/
#include "UDasherWidget.h"
//LOCTEXT
#define LOCTEXT_NAMESPACE "UMG"
/////////////////////////////////////////////////////
// UDasherWidget
UDasherWidget::UDasherWidget(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
//Default Values Set Here, see above
}
//Rebuild using custom Slate Widget
TSharedRef<SWidget> UDasherWidget::RebuildWidget()
{
if (!DasherScreen)
{
DasherScreen = SNew(SDasherWidget).height(1080).width(1920);
}
return DasherScreen.ToSharedRef();
}
void UDasherWidget::SynchronizeProperties()
{
Super::SynchronizeProperties();
//Check if we're in Editor
DasherScreen->SetEditor(IsDesignTime());
DasherScreen->CharacterEntered.BindLambda([this](FString Char, FString Buffer)
{
CharacterEntered.Broadcast(Char, Buffer);
BufferAltered.Broadcast(Buffer);
});
DasherScreen->CharacterDeleted.BindLambda([this](FString Char, FString Buffer)
{
CharacterDeleted.Broadcast(Char, Buffer);
BufferAltered.Broadcast(Buffer);
});
DasherScreen->MouseUpListeners.BindLambda([this]() {MouseEvent.Broadcast(false); });
DasherScreen->MouseDownListeners.BindLambda([this]() {MouseEvent.Broadcast(true); });
}
void UDasherWidget::ReleaseSlateResources(bool bReleaseChildren)
{
Super::ReleaseSlateResources(bReleaseChildren);
DasherScreen.Reset();
}
FString UDasherWidget::GetBuffer()
{
return DasherScreen->GetBuffer();
}
void UDasherWidget::ResetBuffer()
{
DasherScreen->ResetBuffer();
}
void UDasherWidget::StartTraining(FString PathToTextFile)
{
DasherScreen->StartTraining(PathToTextFile);
}
void UDasherWidget::SetBoolParamter(FString ParameterName, bool Value)
{
DasherScreen->SetBoolParamter(ParameterName, Value);
}
void UDasherWidget::SetLongParamter(FString ParameterName, int64 Value)
{
DasherScreen->SetLongParamter(ParameterName, Value);
}
void UDasherWidget::SetStringParamter(FString ParameterName, FString Value)
{
DasherScreen->SetStringParamter(ParameterName, Value);
}
void UDasherWidget::InputButton(bool Pressed)
{
DasherScreen->InputButton(Pressed);
}
void UDasherWidget::InputVector(FVector2D InputVector)
{
DasherScreen->InputVector(InputVector);
}
#if WITH_EDITOR
//const FSlateBrush* UDasherWidget::GetEditorIcon()
//{
// return FUMGStyle::Get().GetBrush("Widget.Image");
//}
const FText UDasherWidget::GetPaletteCategory()
{
return LOCTEXT("Common", "Common");
}
#endif
/////////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE
\ No newline at end of file
#pragma once
//Includes wrapped for all the DasherCore functionality used in the plugin
#pragma warning(push)
//clang specific warnings
#if PLATFORM_LINUX
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Woverloaded-virtual"
#pragma clang diagnostic ignored "-Wdefaulted-function-deleted"
#endif
THIRD_PARTY_INCLUDES_START
//included first to stop linker errors
#include "SettingsStore.h"
#include "AbstractXMLParser.h"
#include "DasherInterfaceBase.h"
#include "DasherScreen.h"
#include "DashIntfScreenMsgs.h"
#include "DasherInput.h"
#include "UserLog.h"
#include "XmlSettingsStore.h"
#include "Messages.h"
#include "ModuleManager.h"
#include "AppSettingsHeader.h"
THIRD_PARTY_INCLUDES_END
#if PLATFORM_LINUX
#pragma clang diagnostic push
#endif
#pragma warning(pop)
\ No newline at end of file
#pragma once
#include <functional>
#include "CoreMinimal.h"
#include "DasherCoreWrapper.h"
class SDasherWidget;
namespace Dasher
{
//Just a function to Log XML errors
class XMLErrorDisplay : public CMessageDisplay
{
public:
void Message(const std::string& strText, bool bInterrupt) override
{
UE_LOG(LogTemp, Log, TEXT("%s"), UTF8_TO_TCHAR(strText.c_str()));
}
};
//Implementation of abstract class CDashIntfScreenMsgs
class DasherInterface : public CDashIntfScreenMsgs
{
public:
//Might need empty constructor+ init method
DasherInterface(CSettingsStore* settings);
~DasherInterface(void) {}
/*void Main();*/
//Subclasses should return the contents of (the specified subrange of) the edit buffer
virtual std::string GetContext(unsigned int iStart, unsigned int iLength) override;
/// Subclasses should return character, word, sentence, ... at current text cursor position.
/// For character around cursor decision is arbitrary. Let's settle for character before cursor.
/// TODO. Consistently name functions dealing with dasher context, versus functions dealing with editor text.
/// I.E. GetAllContext should be named GetAllTtext
virtual std::string GetTextAroundCursor(Dasher::CControlManager::EditDistance) override;
///Called to execute a control-mode "move" command.
///\param bForwards true to move forwards (right), false for backwards
///\param dist how far to move: character, word, line, file. (Usually defined
/// by OS, e.g. for non-european languages)
///\return the offset, into the edit buffer of the cursor *after* the move.
virtual unsigned int ctrlMove(bool bForwards, Dasher::CControlManager::EditDistance dist) override;
///Called to execute a control-mode "delete" command.
///\param bForwards true to delete forwards (right), false for backwards
///\param dist how much to delete: character, word, line, file. (Usually defined
/// by OS, e.g. for non-european languages)
///\return the offset, into the edit buffer, of the cursor *after* the delete
/// (for forwards deletion, this will be the same as the offset *before
virtual unsigned int ctrlDelete(bool bForwards, Dasher::CControlManager::EditDistance dist) override;
///Clears all written text from edit buffer and rebuilds the model. The default
/// implementation does this using the control mode editDelete mechanism
/// (one call forward, one back), followed by a call to SetBuffer(0). Subclasses
/// may (optionally) override with more efficient / easier implementations, but
/// should make the same call to SetBuffer.
virtual std::string GetAllContext() override;
///// Seems to be the only way to delete a symbol
virtual void editDelete(const std::string& strText, CDasherNode* pNode) override;
virtual void editOutput(const std::string& strText, CDasherNode* pNode) override;
/// Subclasses should return the length of whole text. In letters, not bytes.
virtual int GetAllContextLenght() override { return Buffer.Len(); }
//tick function
void Tick(unsigned long time);
// set the screen, needed for maximum spaghetti, to make changeScreen accessible to the outside aka SDasherWidget
virtual void SetScreen(SDasherWidget* screen);
//Sets the Training Filename
virtual void ImportTrainingFile(std::string filename);
virtual FVector2D ConvertDasher2Screen(myint InputX, myint InputY);
//return Buffer
FString GetBuffer() const;
//delete Buffer
void ResetBuffer();
void SetCharEnteredCallback(std::function<void(FString, FString)> Callback)
{
CharacterEntered = Callback;
}
void SetCharDeletedCallback(std::function<void(FString, FString)> Callback)
{
CharacterDeleted = Callback;
}
private:
//Cursor position in the output buffer
int Cursor = 0;
//Output Buffer
FString Buffer = "";
//Dasher Settings Object
CSettingsStore* settings;
//Callbacks
std::function<void(FString, FString)> CharacterEntered = nullptr;
std::function<void(FString, FString)> CharacterDeleted = nullptr;
};
}
\ No newline at end of file
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "DasherCoreWrapper.h"
class FDasherVRModule : public IModuleInterface
{
public:
/** IModuleInterface implementation */
virtual void StartupModule() override;
virtual void ShutdownModule() override;
};
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
#include "DasherCoreWrapper.h"
#include <utility>
#include "DasherInterface.h"
#include "Math/Vector2D.h"
#include "Fonts/FontMeasure.h"
//using namespace Dasher;
//Structs to hold the elements making up the UI
enum GeometryType
{
Rectangle,
Writing,
PolyLine
};
struct DasherDrawGeometry
{
GeometryType Type;
DasherDrawGeometry(GeometryType Type) : Type(Type) {}
virtual ~DasherDrawGeometry() {};
};
struct FFilledRect : DasherDrawGeometry{
FVector2D top;
FVector2D bottom;
FLinearColor color;
FFilledRect(FVector2D Top, FVector2D Bottom, FLinearColor Color) : DasherDrawGeometry(Rectangle), top(Top), bottom(Bottom), color(Color) {}
};
struct FWriting : DasherDrawGeometry{
Dasher::CDasherScreen::Label *label;
FVector2D pos;
int size;
FLinearColor color;
FWriting(Dasher::CDasherScreen::Label *Label, FVector2D Pos, int Size, FLinearColor Color) : DasherDrawGeometry(Writing), label(Label), pos(Pos), size(Size), color(Color) {}
};
struct FPolyLine : DasherDrawGeometry{
TArray<FVector2D> points;
float linewidth;
FLinearColor color;
FPolyLine(TArray<FVector2D> Points, float LineWidth, FLinearColor Color): DasherDrawGeometry(PolyLine), points(Points), linewidth(LineWidth), color(Color) {}
};
DECLARE_DELEGATE(FDasherMouseUpDelegate);
DECLARE_DELEGATE(FDasherMouseDownDelegate);
DECLARE_DELEGATE_TwoParams(FBufferManiputlationDelegate, FString, FString);
class DASHERVR_API SDasherWidget : public SCompoundWidget, public Dasher::CDasherScreen, public Dasher::CScreenCoordInput
{
public:
SLATE_BEGIN_ARGS(SDasherWidget)
{}
SLATE_ARGUMENT(int, height);
SLATE_ARGUMENT(int, width);
SLATE_END_ARGS()
typedef Dasher::screenint screenint;
SDasherWidget(): CDasherScreen(0,0), CScreenCoordInput(0, _("Mouse Input")) {}
// Constructs this widget with InArgs. Needed for every widget. Builds this widget and any of its children
void Construct(const FArguments& InArgs);
void SetBoolParamter(FString ParameterName, bool Value);
void SetLongParamter(FString ParameterName, int64 Value);
void SetStringParamter(FString ParameterName, FString Value);
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
//set of functions inherited from Dasherscreen used for displaying, see DasherScreen for further info
virtual void SetColourScheme(const Dasher::CColourIO::ColourInfo* pcolours) override;
virtual std::pair<screenint, screenint> TextSize(CDasherScreen::Label* Label, unsigned int Size) override;
virtual void DrawString(CDasherScreen::Label *label, screenint x1, screenint y1, unsigned int size, int colour) override;
virtual void DrawRectangle(Dasher::screenint x1, Dasher::screenint y1, Dasher::screenint x2, Dasher::screenint y2, int colour, int ioutlinecolour, int ithickness) override;
virtual void DrawCircle(screenint icx, screenint icy, screenint ir, int ifillcolour, int ilinecolour, int ithickness) override {} //we don't really need to draw circles, so it's not implemented
virtual void Polyline(CDasherScreen::point* points, int number, int iwidth, int colour) override;
virtual void Polygon(CDasherScreen::point* points, int number, int fillcolour, int outlinecolour, int iwidth) override;
virtual void Display() override;
virtual void SendMarker(int imarker) override {}
virtual bool IsPointVisible(screenint x, screenint y) override { return true; }
virtual bool MultiSizeFonts() override {return true;}
//Pass-me-down returning Buffer
FString GetBuffer() const;
void ResetBuffer();
void StartTraining(FString PathToTextFile);
//Tick function inherited from SCompoundWidget
virtual void Tick(const FGeometry& AllotedGeometry, const double InCurrentTime, const float InDeltaTime) override;
//Function to set if the widget is in editor or not
void SetEditor(bool EditorState);
//mouse handling function
FReply HandleMouseMoveEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent);
FReply HandleMouseDownEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent);
FReply HandleMouseUpEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent);
FReply HandleMouseDoubleClickEvent(const FGeometry& Geometry, const FPointerEvent& MouseEvent);
virtual bool SupportsKeyboardFocus() const override {return true;};
virtual bool GetScreenCoords(screenint& iX, screenint& iY, Dasher::CDasherView* pView) override;
void InputVector(FVector2D InputVector);
void InputButton(bool Pressed);
FVector2D GetCursorPosition();
FDasherMouseUpDelegate MouseUpListeners;
FDasherMouseUpDelegate MouseDownListeners;
FBufferManiputlationDelegate CharacterEntered;
FBufferManiputlationDelegate CharacterDeleted;
private:
//Buffers to store geometry that is drawn in OnPaint
TArray<TUniquePtr<DasherDrawGeometry>> GeometryBufferA;
TArray<TUniquePtr<DasherDrawGeometry>> GeometryBufferB;
TArray<TUniquePtr<DasherDrawGeometry>>* BackBuffer = &GeometryBufferA;
TArray<TUniquePtr<DasherDrawGeometry>>* FrontBuffer = &GeometryBufferB;
int Height;
int Width;
bool HasBeenPainted = false;
bool CurrentlyUsingVectorInput = false;
FVector2D CursorPosition;
//are we in the Editor?
bool IsEditor = true;
//set up the font measure service to ... measure fonts.
TSharedPtr<FSlateFontMeasure> FontMeasureService;
protected:
// stores color information
const Dasher::CColourIO::ColourInfo* ColorPalette = nullptr;
TSharedPtr<Dasher::DasherInterface> DasherMainInterface;
};
#pragma once
#include "CoreMinimal.h"
//~~~~~~~~~~~~ UMG ~~~~~~~~~~~~~~~~
#include "Runtime/UMG/Public/UMG.h"
#include "Runtime/UMG/Public/UMGStyle.h"
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//Custom Slate Element
#include "SDasherWidget.h"
#include "UDasherWidget.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FCharManipulatedEvent, FString, Char, FString, Buffer);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FBufferManipulatedEvent, FString, Buffer);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FMouseEvent, bool, IsMouseDown);
UCLASS(BlueprintType)
class DASHERVR_API UDasherWidget : public UWidget
{
GENERATED_UCLASS_BODY()
private:
//Custom Slate Element
TSharedPtr<SDasherWidget> DasherScreen;
public:
//Functions to get and reset the Dasher Text Buffer
UFUNCTION(BlueprintPure) FString GetBuffer();
UFUNCTION(BlueprintCallable) void ResetBuffer();
UFUNCTION(BlueprintCallable) void StartTraining(FString PathToTextFile);
UFUNCTION(BlueprintCallable) void SetBoolParamter(FString ParameterName, bool Value);
UFUNCTION(BlueprintCallable) void SetLongParamter(FString ParameterName, int64 Value);
UFUNCTION(BlueprintCallable) void SetStringParamter(FString ParameterName, FString Value);
UFUNCTION(BlueprintCallable) void InputButton(bool Pressed);
UFUNCTION(BlueprintCallable) void InputVector(FVector2D InputVector);
UPROPERTY(BlueprintAssignable) FCharManipulatedEvent CharacterEntered;
UPROPERTY(BlueprintAssignable) FCharManipulatedEvent CharacterDeleted;
UPROPERTY(BlueprintAssignable) FBufferManipulatedEvent BufferAltered;
UPROPERTY(BlueprintAssignable) FMouseEvent MouseEvent;
public:
// UWidget interface
virtual void SynchronizeProperties() override;
// End of UWidget interface
// UVisual interface
virtual void ReleaseSlateResources(bool bReleaseChildren) override;
// End of UVisual interface
#if WITH_EDITOR
// UWidget interface
//virtual const FSlateBrush* GetEditorIcon() override;
virtual const FText GetPaletteCategory() override;
// End UWidget interface
#endif
protected:
// UWidget interface
virtual TSharedRef<SWidget> RebuildWidget() override;
// End of UWidget interface
};
\ No newline at end of file
########################
# CURRENTLY LINUX ONLY!#
########################
cmake_minimum_required(VERSION 3.10)
project("DasherLib")
############################################################################################################
# How to use this cmake:
# Make sure to update and init your git submodules recursively (to get expat)
# make a build directory right next to this CMakeLists.txt
# Run cmake and make in the build directory. Then run make install to get the library in the correct folder
# If using visual studio, make sure to select the release configuration if it is not your default
############################################################################################################
############################################################################################################
# Set this to your libc++ path included with Unreal
############################################################################################################
set(LIBC_PATH "/home/unreal/UE4_Build_426/Engine/Source/ThirdParty/Linux/LibCxx/")
if (UNIX)
############################################################################################################
# Flags needed for clang to use unreal's libc++
############################################################################################################
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_compile_definitions(HAVE_ROUND XML_STATIC _CRT_SECURE_NO_WARNINGS HAVE_OWN_FILEUTILS HAVE_OWN_FILELOGGER)
set(CMAKE_SUPPRESS_REGENERATION true)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -std=c++17 -stdlib=libc++ -nostdinc++ -I${LIBC_PATH}include/c++/v1 -L${LIBC_PATH}lib -Wl -rpath ${LIBC_PATH}lib")
set(CMAKE_EXE_LINKER_FLAGS "-stdlib=libc++")
############################################################################################################
#Silence Warnings stemming from giving linker arguments to the compiler and vice versa
############################################################################################################
add_definitions(-Wno-unused-command-line-argument -Wno-defaulted-function-deleted -Wno-inconsistent-missing-override -Wno-unknown-warning-option)
endif (UNIX)
############################################################################################################
#We use include since install doesn't like subdirectory targets
############################################################################################################
include(${CMAKE_CURRENT_SOURCE_DIR}/Dasher/DasherCore/CMakeLists.txt)
############################################################################################################
#Set the appropriate name and directory for the output
#Install header files during make install
############################################################################################################
set(CMAKE_BUILD_TYPE Release)
set_target_properties(DasherCore PROPERTIES PREFIX "")
set_target_properties(DasherCore PROPERTIES OUTPUT_NAME "DasherCore")
set_target_properties(DasherCore PROPERTIES ARCHIVE_OUTPUT_DIRECTORY_RELEASE ${PROJECT_BINARY_DIR}/../Dasher/Lib)
if(WIN32)
set_target_properties(DasherCore PROPERTIES MSVC_RUNTIME_LIBRARY MultiThreaded)
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
endif(WIN32)
// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using UnrealBuildTool;
public class Dasher : ModuleRules
{
public Dasher(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
Type = ModuleType.External;
PrivateDefinitions.Add("HAVE_OWN_FILEUTILS");
PublicIncludePaths.AddRange(new string[] {});
PrivateIncludePaths.AddRange(new string[] {});
PublicDependencyModuleNames.AddRange(new string[]{"Core"});
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore"
}
);
PublicDefinitions.Add("HAVE_ROUND=1");
DynamicallyLoadedModuleNames.AddRange(new string[]{});
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src", "Common"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src", "Common", "Allocators"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src", "Common", "Platform"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src", "Common", "Unicode"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src", "DasherCore"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src", "DasherCore", "Alphabet"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Src", "DasherCore", "LanguageModelling"));
PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "DasherCore", "Thirdparty", "pugixml", "src"));
if (Target.Platform == UnrealTargetPlatform.Win64)
{
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "Lib", "DasherCore.lib"));
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "Lib", "pugixml.lib"));
}
if (Target.Platform == UnrealTargetPlatform.Linux)
{
PublicAdditionalLibraries.Add(Path.Combine(ModuleDirectory, "Lib", "DasherCore.a"));
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment