Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
V
vadere
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
110
Issues
110
List
Boards
Labels
Service Desk
Milestones
Iterations
Merge Requests
4
Merge Requests
4
Requirements
Requirements
List
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Test Cases
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Code Review
Insights
Issue
Repository
Value Stream
Wiki
Wiki
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
vadere
vadere
Commits
bf8cca5d
Commit
bf8cca5d
authored
Jun 29, 2020
by
Stefan Schuhbaeck
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add help text annotation processor to show JavaDoc in GUI
parent
75ee4375
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
385 additions
and
4 deletions
+385
-4
VadereAnnotation/src/org/vadere/annotation/ImportScanner.java
...reAnnotation/src/org/vadere/annotation/ImportScanner.java
+56
-0
VadereAnnotation/src/org/vadere/annotation/helptext/HelpTextAnnotationProcessor.java
...dere/annotation/helptext/HelpTextAnnotationProcessor.java
+129
-0
VadereGui/resources/messages.properties
VadereGui/resources/messages.properties
+1
-0
VadereGui/resources/messages_de_DE.properties
VadereGui/resources/messages_de_DE.properties
+1
-0
VadereGui/src/org/vadere/gui/components/control/HelpTextView.java
...i/src/org/vadere/gui/components/control/HelpTextView.java
+82
-0
VadereGui/src/org/vadere/gui/projectview/view/DataProcessingView.java
...c/org/vadere/gui/projectview/view/DataProcessingView.java
+3
-2
VadereGui/src/org/vadere/gui/projectview/view/ScenarioPanel.java
...ui/src/org/vadere/gui/projectview/view/ScenarioPanel.java
+13
-0
VadereGui/src/org/vadere/gui/projectview/view/TextView.java
VadereGui/src/org/vadere/gui/projectview/view/TextView.java
+15
-0
VadereGui/src/org/vadere/gui/topographycreator/view/JLabelObserver.java
...org/vadere/gui/topographycreator/view/JLabelObserver.java
+36
-2
VadereGui/src/org/vadere/gui/topographycreator/view/JLinkLabel.java
...src/org/vadere/gui/topographycreator/view/JLinkLabel.java
+49
-0
No files found.
VadereAnnotation/src/org/vadere/annotation/ImportScanner.java
0 → 100644
View file @
bf8cca5d
package
org.vadere.annotation
;
import
java.util.HashSet
;
import
java.util.Set
;
import
javax.lang.model.element.ExecutableElement
;
import
javax.lang.model.element.TypeElement
;
import
javax.lang.model.element.TypeParameterElement
;
import
javax.lang.model.element.VariableElement
;
import
javax.lang.model.type.TypeKind
;
import
javax.lang.model.type.TypeMirror
;
import
javax.lang.model.util.ElementScanner7
;
// see: https://stackoverflow.com/a/18777229
public
class
ImportScanner
extends
ElementScanner7
<
Void
,
Void
>
{
private
Set
<
String
>
types
=
new
HashSet
<>();
public
Set
<
String
>
getImportedTypes
()
{
return
types
;
}
@Override
public
Void
visitType
(
TypeElement
e
,
Void
p
)
{
for
(
TypeMirror
interfaceType
:
e
.
getInterfaces
())
{
types
.
add
(
interfaceType
.
toString
());
}
types
.
add
(
e
.
getSuperclass
().
toString
());
return
super
.
visitType
(
e
,
p
);
}
@Override
public
Void
visitExecutable
(
ExecutableElement
e
,
Void
p
)
{
if
(
e
.
getReturnType
().
getKind
()
==
TypeKind
.
DECLARED
)
{
types
.
add
(
e
.
getReturnType
().
toString
());
}
return
super
.
visitExecutable
(
e
,
p
);
}
@Override
public
Void
visitTypeParameter
(
TypeParameterElement
e
,
Void
p
)
{
if
(
e
.
asType
().
getKind
()
==
TypeKind
.
DECLARED
)
{
types
.
add
(
e
.
asType
().
toString
());
}
return
super
.
visitTypeParameter
(
e
,
p
);
}
@Override
public
Void
visitVariable
(
VariableElement
e
,
Void
p
)
{
if
(
e
.
asType
().
getKind
()
==
TypeKind
.
DECLARED
)
{
types
.
add
(
e
.
asType
().
toString
());
}
return
super
.
visitVariable
(
e
,
p
);
}
}
\ No newline at end of file
VadereAnnotation/src/org/vadere/annotation/helptext/HelpTextAnnotationProcessor.java
0 → 100644
View file @
bf8cca5d
package
org.vadere.annotation.helptext
;
import
com.google.auto.service.AutoService
;
import
org.vadere.annotation.ImportScanner
;
import
java.io.PrintWriter
;
import
java.util.ArrayList
;
import
java.util.Set
;
import
java.util.function.Function
;
import
java.util.regex.Matcher
;
import
java.util.regex.Pattern
;
import
java.util.stream.Collectors
;
import
javax.annotation.processing.AbstractProcessor
;
import
javax.annotation.processing.Processor
;
import
javax.annotation.processing.RoundEnvironment
;
import
javax.annotation.processing.SupportedAnnotationTypes
;
import
javax.annotation.processing.SupportedSourceVersion
;
import
javax.lang.model.SourceVersion
;
import
javax.lang.model.element.Element
;
import
javax.lang.model.element.TypeElement
;
import
javax.tools.FileObject
;
import
javax.tools.StandardLocation
;
@SupportedAnnotationTypes
({
"*"
})
// run for all annotations. process must return false so annotations are not consumed
@SupportedSourceVersion
(
SourceVersion
.
RELEASE_11
)
@AutoService
(
Processor
.
class
)
public
class
HelpTextAnnotationProcessor
extends
AbstractProcessor
{
ArrayList
<
Function
<
String
,
String
>>
pattern
;
Set
<
String
>
importedTypes
;
@Override
public
boolean
process
(
Set
<?
extends
TypeElement
>
annotations
,
RoundEnvironment
roundEnv
)
{
initPattern
();
ImportScanner
scanner
=
new
ImportScanner
();
scanner
.
scan
(
roundEnv
.
getRootElements
(),
null
);
importedTypes
=
scanner
.
getImportedTypes
();
for
(
Element
e:
roundEnv
.
getRootElements
()){
if
(
e
.
getKind
().
isClass
()
&&
e
.
asType
().
toString
().
startsWith
(
"org.vadere."
))
{
try
{
String
comment
=
processingEnv
.
getElementUtils
().
getDocComment
(
e
);
String
relname
=
buildHelpTextPath
(
e
.
asType
().
toString
());
FileObject
file
=
processingEnv
.
getFiler
().
createResource
(
StandardLocation
.
CLASS_OUTPUT
,
""
,
relname
);
try
(
PrintWriter
w
=
new
PrintWriter
(
file
.
openWriter
()))
{
w
.
println
(
"<h1> "
+
e
.
getSimpleName
()
+
"</h1>"
);
w
.
println
();
printComment
(
w
,
comment
);
w
.
println
();
printMemberDocString
(
e
,
w
);
}
}
catch
(
Exception
ex
)
{
ex
.
printStackTrace
();
}
}
}
return
false
;
// allow further processing
}
private
String
buildHelpTextPath
(
String
className
)
{
className
=
className
.
replace
(
"<"
,
"_"
);
className
=
className
.
replace
(
">"
,
"_"
);
return
"helpText/"
+
className
+
".html"
;
}
private
void
initPattern
()
{
pattern
=
new
ArrayList
<>();
pattern
.
add
(
e
->
{
Pattern
r
=
Pattern
.
compile
(
"(\\{@link\\s+#)(.*?)(})"
);
Matcher
m
=
r
.
matcher
(
e
);
while
(
m
.
find
()){
e
=
m
.
replaceFirst
(
"<span class='local_link'>$2</span>"
);
m
=
r
.
matcher
(
e
);
}
return
e
;
});
pattern
.
add
(
e
->
{
Pattern
r
=
Pattern
.
compile
(
"(\\{@link\\s+)(.*?)(})"
);
Matcher
m
=
r
.
matcher
(
e
);
while
(
m
.
find
()){
String
linkId
=
findFullPath
(
m
.
group
(
2
));
e
=
m
.
replaceFirst
(
String
.
format
(
"<a href='%s' class='class_link'>$2</a>"
,
linkId
));
m
=
r
.
matcher
(
e
);
}
return
e
;
});
}
private
String
findFullPath
(
String
className
){
String
n
=
importedTypes
.
stream
().
filter
(
e
->
e
.
endsWith
(
className
)).
findFirst
().
orElse
(
"rel_/"
+
className
);
return
"/helpText/"
+
n
+
".html"
;
}
private
void
printComment
(
PrintWriter
w
,
String
multiLine
){
if
(
multiLine
==
null
){
w
.
println
(
"<p>---</p>"
);
}
else
{
w
.
println
(
"<p>"
);
multiLine
.
lines
().
map
(
String:
:
strip
).
map
(
this
::
applyMatcher
).
forEach
(
w:
:
println
);
w
.
println
(
"</p>"
);
}
}
private
String
applyMatcher
(
String
line
){
for
(
Function
<
String
,
String
>
p
:
pattern
){
line
=
p
.
apply
(
line
);
}
return
line
;
}
private
void
printMemberDocString
(
Element
e
,
PrintWriter
w
)
{
Set
<?
extends
Element
>
fields
=
e
.
getEnclosedElements
()
.
stream
()
.
filter
(
o
->
o
.
getKind
().
isField
())
.
collect
(
Collectors
.
toSet
());
for
(
Element
field
:
fields
){
w
.
println
(
"<hr>"
);
w
.
println
(
"<h2> Field: "
+
field
.
getSimpleName
()
+
"</h2>"
);
String
comment
=
processingEnv
.
getElementUtils
().
getDocComment
(
field
);
printComment
(
w
,
comment
);
w
.
println
();
}
}
}
VadereGui/resources/messages.properties
View file @
bf8cca5d
...
...
@@ -377,6 +377,7 @@ Tab.Model.loadTemplateMenu.title=Load template
Tab.Model.confirmLoadTemplate.title
=
Continue?
Tab.Model.confirmLoadTemplate.text
=
This replaces the content of the text field.
Tab.Model.addAttributesMenu.title
=
Add Attributes
Tab.Model.helpAttributesMenu.title
=
Help
Tab.Model.insertModelNameMenu.title
=
Insert model name
Tab.Model.insertModelNameSubMenu.title
=
Main models
Tab.Pedestrians.title
=
Pedestrians
...
...
VadereGui/resources/messages_de_DE.properties
View file @
bf8cca5d
...
...
@@ -367,6 +367,7 @@ Tab.Model.loadTemplateMenu.title=Voreinstellung laden
Tab.Model.confirmLoadTemplate.title
=
Fortfahren?
Tab.Model.confirmLoadTemplate.text
=
Der Inhalt des Textfeldes wird ersetzt.
Tab.Model.addAttributesMenu.title
=
Attributes hinzuf
\u
00fcgen
Tab.Model.helpAttributesMenu.title
=
Hilfe
Tab.Model.insertModelNameMenu.title
=
Model-Name einf
\u
00fcgen
Tab.Model.insertModelNameSubMenu.title
=
Hauptmodelle
Tab.Pedestrians.title
=
Fu
\u
00dfg
\u
00e4nger
...
...
VadereGui/src/org/vadere/gui/components/control/HelpTextView.java
0 → 100644
View file @
bf8cca5d
package
org.vadere.gui.components.control
;
import
java.io.BufferedReader
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.InputStreamReader
;
import
java.util.ArrayList
;
import
javax.swing.*
;
import
javax.swing.event.HyperlinkEvent
;
import
javax.swing.text.Document
;
import
javax.swing.text.html.HTMLEditorKit
;
import
javax.swing.text.html.StyleSheet
;
public
class
HelpTextView
extends
JEditorPane
{
private
ArrayList
<
String
>
filenames
;
public
static
HelpTextView
create
(
String
className
){
HelpTextView
view
=
new
HelpTextView
();
view
.
loadHelpFromClass
(
className
);
return
view
;
}
public
HelpTextView
()
{
setContentType
(
"text/html"
);
setEditable
(
false
);
addHyperlinkListener
(
e
->
{
if
(
e
.
getEventType
()
==
HyperlinkEvent
.
EventType
.
ACTIVATED
){
String
link
=
e
.
getDescription
();
if
(
link
.
startsWith
(
"/helpText/rel_/"
)){
String
clsName
=
link
.
split
(
"/"
)[
3
].
strip
();
for
(
String
f
:
filenames
){
if
(
f
.
endsWith
(
clsName
)){
link
=
f
;
break
;
}
}
}
loadHelpText
(
link
);
}
});
filenames
=
new
ArrayList
<>();
try
(
InputStream
in
=
getClass
().
getResourceAsStream
(
"/helpText"
);
BufferedReader
br
=
new
BufferedReader
(
new
InputStreamReader
(
in
)))
{
String
resource
;
while
((
resource
=
br
.
readLine
())
!=
null
)
{
filenames
.
add
(
resource
);
}
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
}
public
void
loadHelpFromClass
(
String
fullClassName
){
loadHelpText
(
"/helpText/"
+
fullClassName
+
".html"
);
}
public
void
loadHelpText
(
String
helpTextId
){
System
.
out
.
println
(
helpTextId
);
String
text
=
null
;
try
{
InputStream
url
=
getClass
().
getResourceAsStream
(
helpTextId
);
text
=
new
String
(
url
.
readAllBytes
());
}
catch
(
Exception
ignored
)
{
text
=
"No Help found."
;
}
HTMLEditorKit
htmlEditorKit
=
new
HTMLEditorKit
();
StyleSheet
sheet
=
htmlEditorKit
.
getStyleSheet
();
sheet
.
addRule
(
".local_link {font-style: italic; text-decoration: underline;}"
);
sheet
.
addRule
(
".class_link {color: blue; font-style: italic; text-decoration: underline;}"
);
sheet
.
addRule
(
"p { padding-bottom: 5px;}"
);
Document
doc
=
htmlEditorKit
.
createDefaultDocument
();
setDocument
(
doc
);
setText
(
text
);
}
}
VadereGui/src/org/vadere/gui/projectview/view/DataProcessingView.java
View file @
bf8cca5d
...
...
@@ -10,6 +10,7 @@ import org.vadere.gui.components.utils.Messages;
import
org.vadere.gui.components.view.JComboCheckBox
;
import
org.vadere.gui.projectview.model.IScenarioChecker
;
import
org.vadere.gui.projectview.utils.SimpleDocumentListener
;
import
org.vadere.gui.topographycreator.view.JLinkLabel
;
import
org.vadere.simulator.projects.Scenario
;
import
org.vadere.simulator.projects.dataprocessing.DataProcessingJsonManager
;
import
org.vadere.simulator.projects.dataprocessing.outputfile.OutputFile
;
...
...
@@ -636,7 +637,7 @@ class DataProcessingView extends JPanel implements IJsonView {
c
.
gridx
=
0
;
c
.
gridy
=
0
;
c
.
gridwidth
=
3
;
panel
.
add
(
new
JL
abel
(
"<html><b>"
+
dataProcessor
.
getSimpleProcessorTypeName
()
+
"</b></html>"
),
c
);
panel
.
add
(
new
JL
inkLabel
(
dataProcessor
.
getClass
().
getName
(),
"<html><b>"
,
"</b></html>"
),
c
);
c
.
gridwidth
=
1
;
c
.
gridx
=
0
;
...
...
@@ -645,7 +646,7 @@ class DataProcessingView extends JPanel implements IJsonView {
c
.
gridx
=
1
;
c
.
gridy
=
1
;
panel
.
add
(
new
JL
abel
(
extractSimpleName
(
getDataKeyForDataProcessor
(
dataProcessor
)
)),
c
);
panel
.
add
(
new
JL
inkLabel
(
getDataKeyForDataProcessor
(
dataProcessor
).
getTypeName
(
)),
c
);
c
.
gridx
=
2
;
c
.
gridy
=
1
;
...
...
VadereGui/src/org/vadere/gui/projectview/view/ScenarioPanel.java
View file @
bf8cca5d
package
org.vadere.gui.projectview.view
;
import
org.vadere.gui.components.control.HelpTextView
;
import
org.vadere.gui.components.utils.Messages
;
import
org.vadere.gui.onlinevisualization.OnlineVisualization
;
import
org.vadere.gui.postvisualization.view.PostvisualizationWindow
;
...
...
@@ -156,6 +157,18 @@ public class ScenarioPanel extends JPanel implements IProjectChangeListener, Pro
}
}
})));
JMenu
mnHelpAttributesMenu
=
new
JMenu
(
Messages
.
getString
(
"Tab.Model.helpAttributesMenu.title"
));
presetMenuBar
.
add
(
mnHelpAttributesMenu
);
menusInTabs
.
add
(
mnHelpAttributesMenu
);
attributeFactory
.
sortedAttributeStream
().
forEach
(
attributesClassName
->
mnHelpAttributesMenu
.
add
(
new
JMenuItem
(
new
AbstractAction
(
attributesClassName
)
{
@Override
public
void
actionPerformed
(
ActionEvent
e
)
{
VDialogManager
.
showMessageDialogWithBodyAndTextEditorPane
(
"Help"
,
attributesClassName
,
HelpTextView
.
create
(
attributesClassName
),
JOptionPane
.
INFORMATION_MESSAGE
);
}
})));
JMenu
mnModelNameMenu
=
new
JMenu
(
Messages
.
getString
(
"Tab.Model.insertModelNameMenu.title"
));
presetMenuBar
.
add
(
mnModelNameMenu
);
...
...
VadereGui/src/org/vadere/gui/projectview/view/TextView.java
View file @
bf8cca5d
...
...
@@ -8,6 +8,7 @@ import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import
org.fife.ui.rsyntaxtextarea.Theme
;
import
org.fife.ui.rtextarea.RTextScrollPane
;
import
org.jetbrains.annotations.NotNull
;
import
org.vadere.gui.components.control.HelpTextView
;
import
org.vadere.gui.components.utils.Messages
;
import
org.vadere.gui.components.utils.Resources
;
import
org.vadere.gui.projectview.model.IScenarioChecker
;
...
...
@@ -246,6 +247,20 @@ public class TextView extends JPanel implements IJsonView {
}
})));
JMenu
mnPresetHelpMenu
=
new
JMenu
(
Messages
.
getString
(
"ProjectView.mnHelp.text"
));
presetMenuBar
.
add
(
mnPresetHelpMenu
);
StimulusPresettings
.
PRESETTINGS_MAP
.
forEach
(
(
clazz
,
jsonString
)
->
mnPresetHelpMenu
.
add
(
new
JMenuItem
(
new
AbstractAction
(
clazz
.
getSimpleName
())
{
private
static
final
long
serialVersionUID
=
1L
;
@Override
public
void
actionPerformed
(
ActionEvent
e
)
{
VDialogManager
.
showMessageDialogWithBodyAndTextEditorPane
(
"Help"
,
clazz
.
getName
(),
HelpTextView
.
create
(
clazz
.
getName
()),
JOptionPane
.
INFORMATION_MESSAGE
);
}
})));
panelTop
.
add
(
presetMenuBar
);
}
}
...
...
VadereGui/src/org/vadere/gui/topographycreator/view/JLabelObserver.java
View file @
bf8cca5d
package
org.vadere.gui.topographycreator.view
;
import
java.awt.*
;
import
java.awt.event.MouseAdapter
;
import
java.awt.event.MouseEvent
;
import
java.util.Observable
;
import
java.util.Observer
;
import
javax.swing.
JLabel
;
import
javax.swing.
*
;
import
org.vadere.gui.components.control.HelpTextView
;
import
org.vadere.gui.components.utils.Messages
;
import
org.vadere.gui.projectview.view.VDialogManager
;
import
org.vadere.gui.topographycreator.model.IDrawPanelModel
;
import
org.vadere.state.scenario.ScenarioElement
;
...
...
@@ -15,11 +20,36 @@ public class JLabelObserver extends JLabel implements Observer {
private
static
final
long
serialVersionUID
=
9011952047793438028L
;
private
IDrawPanelModel
panelModel
;
private
String
selectedElementAttrFQN
;
public
JLabelObserver
(
String
labelText
)
{
super
(
labelText
);
setForeground
(
Color
.
BLUE
.
darker
());
setCursor
(
Cursor
.
getPredefinedCursor
(
Cursor
.
HAND_CURSOR
));
addMouseListener
(
new
MouseAdapter
()
{
@Override
public
void
mouseClicked
(
MouseEvent
e
)
{
if
(!
selectedElementAttrFQN
.
equals
(
""
)){
String
body
=
getText
()
+
": Help and Field Description"
;
VDialogManager
.
showMessageDialogWithBodyAndTextEditorPane
(
"Help"
,
body
,
HelpTextView
.
create
(
selectedElementAttrFQN
),
JOptionPane
.
INFORMATION_MESSAGE
);
}
}
@Override
public
void
mouseEntered
(
MouseEvent
e
)
{
super
.
mouseEntered
(
e
);
}
@Override
public
void
mouseExited
(
MouseEvent
e
)
{
super
.
mouseExited
(
e
);
}
});
}
public
void
setPanelModel
(
IDrawPanelModel
panelModel
)
{
this
.
panelModel
=
panelModel
;
}
...
...
@@ -30,8 +60,12 @@ public class JLabelObserver extends JLabel implements Observer {
ScenarioElement
selectedElement
=
panelModel
.
getSelectedElement
();
String
newText
=
DEFAULT_TEXT
;
if
(
selectedElement
!=
null
)
if
(
selectedElement
!=
null
)
{
newText
=
selectedElement
.
getClass
().
getSimpleName
().
toString
();
selectedElementAttrFQN
=
selectedElement
.
getAttributes
().
getClass
().
getName
();
}
else
{
selectedElementAttrFQN
=
""
;
}
setText
(
newText
);
}
...
...
VadereGui/src/org/vadere/gui/topographycreator/view/JLinkLabel.java
0 → 100644
View file @
bf8cca5d
package
org.vadere.gui.topographycreator.view
;
import
org.vadere.gui.components.control.HelpTextView
;
import
org.vadere.gui.projectview.view.VDialogManager
;
import
java.awt.*
;
import
java.awt.event.MouseAdapter
;
import
java.awt.event.MouseEvent
;
import
javax.swing.*
;
public
class
JLinkLabel
extends
JLabel
{
private
String
fullName
;
private
String
shortName
;
public
JLinkLabel
(
String
fullName
){
this
(
fullName
,
""
,
""
);
}
public
JLinkLabel
(
String
fullName
,
String
prefix
,
String
suffix
){
super
();
this
.
fullName
=
fullName
;
String
[]
tmp
=
fullName
.
split
(
"\\."
);
this
.
shortName
=
tmp
[
tmp
.
length
-
1
];
setText
(
prefix
+
shortName
+
suffix
);
setForeground
(
Color
.
BLUE
.
darker
());
setCursor
(
Cursor
.
getPredefinedCursor
(
Cursor
.
HAND_CURSOR
));
addMouseListener
(
new
MouseAdapter
()
{
@Override
public
void
mouseClicked
(
MouseEvent
e
)
{
String
body
=
shortName
+
": Help and Field Description"
;
VDialogManager
.
showMessageDialogWithBodyAndTextEditorPane
(
"Help"
,
body
,
HelpTextView
.
create
(
fullName
),
JOptionPane
.
INFORMATION_MESSAGE
);
}
@Override
public
void
mouseEntered
(
MouseEvent
e
)
{
super
.
mouseEntered
(
e
);
}
@Override
public
void
mouseExited
(
MouseEvent
e
)
{
super
.
mouseExited
(
e
);
}
});
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment