SDK:
- Improve FakeApplication, integrate into main application - Add separate execution thread for user code - Add error handling for failing AppStates - Add error handling for failing Controls - AppStateExplorer now works when opening it after the scene git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@10079 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
This commit is contained in:
parent
2c6e8cbb2a
commit
2d1af0f1f8
@ -74,31 +74,27 @@ preferredID = "AppStateExplorerTopComponent")
|
||||
public final class AppStateExplorerTopComponent extends TopComponent implements ExplorerManager.Provider {
|
||||
|
||||
private transient ExplorerManager explorerManager = new ExplorerManager();
|
||||
private FakeApplication fakeApp;
|
||||
private ProjectAssetManager mgr;
|
||||
private SceneRequest currentRequest;
|
||||
//TODO: move to global place
|
||||
private SceneListener listener = new SceneListener() {
|
||||
public void sceneOpened(SceneRequest request) {
|
||||
currentRequest = request;
|
||||
Spatial rootNode = request.getRootNode();
|
||||
if (!(rootNode instanceof com.jme3.scene.Node)) {
|
||||
return;
|
||||
}
|
||||
mgr = request.getManager();
|
||||
AssetManager assetManager = request.getManager();
|
||||
Camera cam = SceneApplication.getApplication().getCamera();
|
||||
com.jme3.scene.Node guiNode = SceneApplication.getApplication().getGuiNode();
|
||||
fakeApp = new FakeApplication((com.jme3.scene.Node) rootNode, guiNode, assetManager, cam);
|
||||
//TODO: ermagherd, hackish
|
||||
SceneApplication.getApplication().setFakeApp(fakeApp);
|
||||
final AppStateManagerNode nod = new AppStateManagerNode(fakeApp.getStateManager());
|
||||
final AppStateManagerNode nod = new AppStateManagerNode(request.getFakeApp().getStateManager());
|
||||
jButton1.setEnabled(true);
|
||||
explorerManager.setRootContext(nod);
|
||||
setActivatedNodes(new Node[]{nod});
|
||||
}
|
||||
|
||||
public void sceneClosed(SceneRequest request) {
|
||||
currentRequest = null;
|
||||
SceneApplication.getApplication().setFakeApp(null);
|
||||
mgr = null;
|
||||
fakeApp = null;
|
||||
jButton1.setEnabled(false);
|
||||
explorerManager.setRootContext(Node.EMPTY);
|
||||
setActivatedNodes(new Node[]{Node.EMPTY});
|
||||
@ -117,6 +113,11 @@ public final class AppStateExplorerTopComponent extends TopComponent implements
|
||||
// map.put("moveup", new MoveUpAction());
|
||||
// map.put("movedown", new MoveDownAction());
|
||||
associateLookup(ExplorerUtils.createLookup(explorerManager, map));
|
||||
//TODO: move to scene listener notify in scene?
|
||||
SceneRequest request = SceneApplication.getApplication().getCurrentSceneRequest();
|
||||
if (request != null) {
|
||||
listener.sceneOpened(request);
|
||||
}
|
||||
SceneApplication.getApplication().addSceneListener(listener);
|
||||
}
|
||||
|
||||
@ -165,9 +166,9 @@ public final class AppStateExplorerTopComponent extends TopComponent implements
|
||||
|
||||
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed
|
||||
ProjectAssetManager projectAssetManager = mgr;
|
||||
FakeApplication fakeApp = this.fakeApp;
|
||||
if (fakeApp != null && mgr != null) {
|
||||
new NewAppStateWizardAction(projectAssetManager, fakeApp).showWizard();
|
||||
SceneRequest currentRequest = this.currentRequest;
|
||||
if (currentRequest != null && mgr != null && currentRequest.getFakeApp() != null) {
|
||||
new NewAppStateWizardAction(projectAssetManager, currentRequest.getFakeApp()).showWizard();
|
||||
}
|
||||
}//GEN-LAST:event_jButton1ActionPerformed
|
||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||
|
@ -32,7 +32,7 @@
|
||||
package com.jme3.gde.core.appstates;
|
||||
|
||||
import com.jme3.app.state.AppState;
|
||||
import com.jme3.gde.core.appstates.FakeApplication.FakeAppStateManager;
|
||||
import com.jme3.gde.core.scene.FakeApplication.FakeAppStateManager;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import org.openide.nodes.AbstractNode;
|
||||
|
@ -48,17 +48,18 @@ public class ApplicationLogHandler extends Handler {
|
||||
JmeFormatter formatter = new JmeFormatter();
|
||||
|
||||
public ApplicationLogHandler() {
|
||||
io.setErrSeparated(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(LogRecord record) {
|
||||
if (record.getLevel().equals(Level.SEVERE)) {
|
||||
io.getErr().println(formatter.formatMessage(record));
|
||||
}
|
||||
else if (record.getLevel().equals(Level.WARNING)) {
|
||||
} else if (record.getLevel().equals(Level.WARNING)) {
|
||||
io.getErr().println(formatter.formatMessage(record));
|
||||
}
|
||||
else {
|
||||
} else if (record.getLevel().equals(Level.INFO)) {
|
||||
io.getOut().println(formatter.formatMessage(record));
|
||||
} else {
|
||||
io.getOut().println(formatter.formatMessage(record));
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package com.jme3.gde.core.appstates;
|
||||
package com.jme3.gde.core.scene;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
@ -38,6 +38,7 @@ import com.jme3.app.state.AppStateManager;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.audio.AudioRenderer;
|
||||
import com.jme3.audio.Listener;
|
||||
import com.jme3.gde.core.appstates.AppStateManagerNode;
|
||||
import com.jme3.input.FlyByCamera;
|
||||
import com.jme3.input.InputManager;
|
||||
import com.jme3.renderer.Camera;
|
||||
@ -45,6 +46,7 @@ import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.Renderer;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.control.Control;
|
||||
import com.jme3.system.AppSettings;
|
||||
import com.jme3.system.JmeContext;
|
||||
import com.jme3.system.JmeContext.Type;
|
||||
@ -54,9 +56,14 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import org.openide.DialogDisplayer;
|
||||
import org.openide.NotifyDescriptor;
|
||||
import org.openide.util.Exceptions;
|
||||
@ -332,9 +339,60 @@ public class FakeApplication extends SimpleApplication {
|
||||
//TODO: also nice messages
|
||||
}
|
||||
|
||||
public static class FakeAppStateManager extends AppStateManager {
|
||||
|
||||
private AppStateManagerNode node;
|
||||
ArrayList<AppState> states = new ArrayList<AppState>();
|
||||
|
||||
public FakeAppStateManager(Application app) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
public List<AppState> getAddedStates() {
|
||||
return states;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean attach(AppState state) {
|
||||
boolean ret = super.attach(state);
|
||||
if (ret) {
|
||||
states.add(state);
|
||||
}
|
||||
if (node != null) {
|
||||
node.refresh();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean detach(AppState state) {
|
||||
boolean ret = super.detach(state);
|
||||
if (ret) {
|
||||
states.remove(state);
|
||||
}
|
||||
if (node != null) {
|
||||
node.refresh();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void setNode(AppStateManagerNode node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Internal
|
||||
*/
|
||||
private ScheduledThreadPoolExecutor fakeAppThread;
|
||||
|
||||
public void startFakeApp() {
|
||||
fakeAppThread = new ScheduledThreadPoolExecutor(1);
|
||||
}
|
||||
|
||||
public void stopFakeApp() {
|
||||
fakeAppThread.shutdown();
|
||||
}
|
||||
|
||||
private void defaultFakeError() {
|
||||
defaultFakeError(false);
|
||||
}
|
||||
@ -366,60 +424,129 @@ public class FakeApplication extends SimpleApplication {
|
||||
NotifyDescriptor.WARNING_MESSAGE));
|
||||
}
|
||||
|
||||
public void updateFake(float tpf) {
|
||||
// System.out.println("UPDATE");
|
||||
private void removeAllStates() {
|
||||
for (Iterator<AppState> it = new ArrayList(appStateManager.getAddedStates()).iterator(); it.hasNext();) {
|
||||
AppState appState = it.next();
|
||||
appStateManager.detach(appState);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean updateFake(final float tpf) {
|
||||
Future fut = fakeAppThread.submit(new Callable<Void>() {
|
||||
public Void call() throws Exception {
|
||||
appStateManager.update(tpf);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
fut.get(1, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
} catch (ExecutionException ex) {
|
||||
removeAllStates();
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Exception in AppState, all AppStates removed."));
|
||||
return false;
|
||||
} catch (TimeoutException ex) {
|
||||
fut.cancel(true);
|
||||
removeAllStates();
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Update loop was blocked for too long, all AppStates removed."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void renderFake() {
|
||||
public boolean renderFake() {
|
||||
Future fut = fakeAppThread.submit(new Callable<Void>() {
|
||||
public Void call() throws Exception {
|
||||
appStateManager.render(renderManager);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class FakeAppStateManager extends AppStateManager {
|
||||
|
||||
private AppStateManagerNode node;
|
||||
ArrayList<AppState> states = new ArrayList<AppState>();
|
||||
|
||||
public FakeAppStateManager(Application app) {
|
||||
super(app);
|
||||
}
|
||||
|
||||
public List<AppState> getAddedStates() {
|
||||
return states;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean attach(AppState state) {
|
||||
boolean ret = super.attach(state);
|
||||
});
|
||||
try {
|
||||
states.add(state);
|
||||
fut.get(1, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
} catch (ExecutionException ex) {
|
||||
removeAllStates();
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Exception in AppState, all AppStates removed."));
|
||||
return false;
|
||||
} catch (TimeoutException ex) {
|
||||
fut.cancel(true);
|
||||
removeAllStates();
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Render loop was blocked for too long, all AppStates removed."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean updateExternalLogicalState(final Node externalNode, final float tpf) {
|
||||
Future fut = fakeAppThread.submit(new Callable<Void>() {
|
||||
public Void call() throws Exception {
|
||||
externalNode.updateLogicalState(tpf);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
fut.get(1, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
} catch (ExecutionException ex) {
|
||||
clearNode(externalNode);
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Exception in Control, scene content removed.\n" + ex.getMessage()));
|
||||
return false;
|
||||
} catch (TimeoutException ex) {
|
||||
fut.cancel(true);
|
||||
clearNode(externalNode);
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Render loop was blocked for too long, scene content removed."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean updateExternalGeometricState(final Node externalNode) {
|
||||
Future fut = fakeAppThread.submit(new Callable<Void>() {
|
||||
public Void call() throws Exception {
|
||||
externalNode.updateGeometricState();
|
||||
return null;
|
||||
}
|
||||
});
|
||||
try {
|
||||
fut.get(1, TimeUnit.MINUTES);
|
||||
} catch (InterruptedException ex) {
|
||||
Exceptions.printStackTrace(ex);
|
||||
} catch (ExecutionException ex) {
|
||||
clearNode(externalNode);
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Exception in Control, scene content removed.\n" + ex.getMessage()));
|
||||
return false;
|
||||
} catch (TimeoutException ex) {
|
||||
fut.cancel(true);
|
||||
clearNode(externalNode);
|
||||
DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message("Render loop was blocked for too long, scene content removed."));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clearNode(final Node externalNode) {
|
||||
while (!externalNode.getChildren().isEmpty()) {
|
||||
try {
|
||||
externalNode.detachAllChildren();
|
||||
} catch (Exception e) {
|
||||
Exceptions.printStackTrace(e);
|
||||
}
|
||||
// DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message(
|
||||
// "attach state",
|
||||
// NotifyDescriptor.WARNING_MESSAGE));
|
||||
if (node != null) {
|
||||
// DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message(
|
||||
// "refresh node",
|
||||
// NotifyDescriptor.WARNING_MESSAGE));
|
||||
node.refresh();
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean detach(AppState state) {
|
||||
try {
|
||||
states.remove(state);
|
||||
} catch (Exception e) {
|
||||
} catch (Error e) {
|
||||
Exceptions.printStackTrace(e);
|
||||
}
|
||||
return super.detach(state);
|
||||
}
|
||||
|
||||
public void setNode(AppStateManagerNode node) {
|
||||
this.node = node;
|
||||
Control control = externalNode.getControl(Control.class);
|
||||
while (control != null) {
|
||||
try {
|
||||
externalNode.removeControl(control);
|
||||
} catch (Exception e) {
|
||||
Exceptions.printStackTrace(e);
|
||||
} catch (Error e) {
|
||||
Exceptions.printStackTrace(e);
|
||||
}
|
||||
control = externalNode.getControl(Control.class);
|
||||
}
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@
|
||||
*/
|
||||
package com.jme3.gde.core.scene;
|
||||
|
||||
import com.jme3.gde.core.appstates.FakeApplication;
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.StatsView;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
@ -171,7 +170,6 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
private void attachPanel() {
|
||||
enqueue(new Callable() {
|
||||
|
||||
public Object call() throws Exception {
|
||||
panel.attachTo(true, viewPort, overlayView, guiViewPort);
|
||||
return null;
|
||||
@ -267,7 +265,7 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
}
|
||||
try {
|
||||
super.update();
|
||||
FakeApplication fakap=fakeApp;
|
||||
FakeApplication fakap = fakeApp;
|
||||
float tpf = timer.getTimePerFrame();
|
||||
camLight.setPosition(cam.getLocation());
|
||||
secondCounter += tpf;
|
||||
@ -277,16 +275,21 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
secondCounter = 0.0f;
|
||||
}
|
||||
getStateManager().update(tpf);
|
||||
if(fakap!=null){
|
||||
toolsNode.updateLogicalState(tpf);
|
||||
if (fakap != null) {
|
||||
fakap.updateFake(tpf);
|
||||
}
|
||||
fakap.updateExternalLogicalState(rootNode, tpf);
|
||||
fakap.updateExternalLogicalState(guiNode, tpf);
|
||||
fakap.updateExternalGeometricState(rootNode);
|
||||
fakap.updateExternalGeometricState(guiNode);
|
||||
} else {
|
||||
rootNode.updateLogicalState(tpf);
|
||||
guiNode.updateLogicalState(tpf);
|
||||
toolsNode.updateLogicalState(tpf);
|
||||
rootNode.updateGeometricState();
|
||||
guiNode.updateGeometricState();
|
||||
}
|
||||
toolsNode.updateGeometricState();
|
||||
if(fakap!=null){
|
||||
if (fakap != null) {
|
||||
fakap.renderFake();
|
||||
}
|
||||
getStateManager().render(renderManager);
|
||||
@ -331,7 +334,6 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
public void notifyPreview(final PreviewRequest request) {
|
||||
java.awt.EventQueue.invokeLater(new Runnable() {
|
||||
|
||||
public void run() {
|
||||
for (Iterator<SceneListener> it = listeners.iterator(); it.hasNext();) {
|
||||
SceneListener sceneViewerListener = it.next();
|
||||
@ -347,12 +349,12 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
/**
|
||||
* method to display the node tree of a plugin (threadsafe)
|
||||
*
|
||||
* @param request
|
||||
*/
|
||||
public void openScene(final SceneRequest request) {
|
||||
closeScene(currentSceneRequest, request);
|
||||
java.awt.EventQueue.invokeLater(new Runnable() {
|
||||
|
||||
public void run() {
|
||||
if (request == null) {
|
||||
return;
|
||||
@ -370,8 +372,10 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
} else {
|
||||
camController.disable();
|
||||
}
|
||||
fakeApp = new FakeApplication(rootNode, guiNode, request.getManager(), cam);
|
||||
fakeApp.startFakeApp();
|
||||
request.setFakeApp(fakeApp);
|
||||
enqueue(new Callable() {
|
||||
|
||||
public Object call() throws Exception {
|
||||
if (request.getManager() != null) {
|
||||
assetManager = request.getManager();
|
||||
@ -396,6 +400,7 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
/**
|
||||
* method to close a scene displayed by a scene request (threadsafe)
|
||||
*
|
||||
* @param request
|
||||
*/
|
||||
public void closeScene(final SceneRequest request) {
|
||||
@ -404,7 +409,6 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
private void closeScene(final SceneRequest oldRequest, final SceneRequest newRequest) {
|
||||
java.awt.EventQueue.invokeLater(new Runnable() {
|
||||
|
||||
public void run() {
|
||||
if (oldRequest == null) {
|
||||
return;
|
||||
@ -425,8 +429,11 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
if (oldRequest.getRequester() instanceof SceneApplication) {
|
||||
camController.disable();
|
||||
}
|
||||
if (fakeApp != null) {
|
||||
fakeApp.stopFakeApp();
|
||||
}
|
||||
fakeApp = null;
|
||||
enqueue(new Callable() {
|
||||
|
||||
public Object call() throws Exception {
|
||||
if (physicsState != null) {
|
||||
physicsState.getPhysicsSpace().removeAll(rootNode);
|
||||
@ -465,7 +472,7 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
req.setModified(false);
|
||||
}
|
||||
}
|
||||
if ((request != null) && (request.getDataObject()instanceof AssetDataObject)){
|
||||
if ((request != null) && (request.getDataObject() instanceof AssetDataObject)) {
|
||||
AssetDataObject obj = (AssetDataObject) request.getDataObject();
|
||||
obj.closeAsset();
|
||||
}
|
||||
@ -501,7 +508,6 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
public void enableCamLight(final boolean enabled) {
|
||||
enqueue(new Callable() {
|
||||
|
||||
public Object call() throws Exception {
|
||||
if (enabled) {
|
||||
rootNode.removeLight(camLight);
|
||||
@ -516,7 +522,6 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
public void enableStats(final boolean enabled) {
|
||||
enqueue(new Callable() {
|
||||
|
||||
public Object call() throws Exception {
|
||||
if (enabled) {
|
||||
guiNode.attachChild(statsGuiNode);
|
||||
@ -530,7 +535,6 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
public void enableWireFrame(final boolean selected) {
|
||||
enqueue(new Callable() {
|
||||
|
||||
public Object call() throws Exception {
|
||||
if (selected) {
|
||||
viewPort.addProcessor(wireProcessor);
|
||||
@ -544,7 +548,6 @@ public class SceneApplication extends Application implements LookupProvider {
|
||||
|
||||
public void setPhysicsEnabled(final boolean enabled) {
|
||||
enqueue(new Callable() {
|
||||
|
||||
public Object call() throws Exception {
|
||||
if (enabled) {
|
||||
if (physicsState == null) {
|
||||
|
@ -54,6 +54,7 @@ public class SceneRequest {
|
||||
private boolean displayed = false;
|
||||
private DataObject dataObject;
|
||||
private HelpCtx helpCtx;
|
||||
private FakeApplication fakeApp;
|
||||
|
||||
public SceneRequest(Object requester, JmeNode rootNode, ProjectAssetManager manager) {
|
||||
this.requester = requester;
|
||||
@ -175,4 +176,13 @@ public class SceneRequest {
|
||||
public void setHelpCtx(HelpCtx helpCtx) {
|
||||
this.helpCtx = helpCtx;
|
||||
}
|
||||
|
||||
public void setFakeApp(FakeApplication fakeApp) {
|
||||
this.fakeApp = fakeApp;
|
||||
}
|
||||
|
||||
public FakeApplication getFakeApp() {
|
||||
return fakeApp;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user