mirror of
https://github.com/Ellpeck/MLEM.git
synced 2024-11-22 12:58:33 +01:00
changed text input style to work on all devices including web
This commit is contained in:
parent
e635f23c1b
commit
053724e4f8
8 changed files with 208 additions and 57 deletions
|
@ -2,23 +2,27 @@ using Android.App;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
|
using MLEM.Input;
|
||||||
|
|
||||||
namespace Demos.Android {
|
namespace Demos.Android {
|
||||||
[Activity(Label = "Demos.Android"
|
[Activity(Label = "Demos.Android"
|
||||||
, MainLauncher = true
|
, MainLauncher = true
|
||||||
, Icon = "@drawable/icon"
|
, Icon = "@drawable/icon"
|
||||||
, Theme = "@style/Theme.Splash"
|
|
||||||
, AlwaysRetainTaskState = true
|
, AlwaysRetainTaskState = true
|
||||||
, LaunchMode = LaunchMode.SingleInstance
|
, LaunchMode = LaunchMode.SingleInstance
|
||||||
, ScreenOrientation = ScreenOrientation.UserLandscape
|
, ScreenOrientation = ScreenOrientation.FullUser
|
||||||
, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.ScreenSize)]
|
, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.Keyboard | ConfigChanges.KeyboardHidden | ConfigChanges.ScreenSize)]
|
||||||
public class Activity1 : Microsoft.Xna.Framework.AndroidGameActivity {
|
public class Activity1 : Microsoft.Xna.Framework.AndroidGameActivity {
|
||||||
|
|
||||||
protected override void OnCreate(Bundle bundle) {
|
protected override void OnCreate(Bundle bundle) {
|
||||||
base.OnCreate(bundle);
|
base.OnCreate(bundle);
|
||||||
var g = new GameImpl();
|
var g = new GameImpl();
|
||||||
// disable mouse handling for android to make emulator behavior more coherent
|
g.OnLoadContent += game => {
|
||||||
g.OnLoadContent += game => game.InputHandler.HandleMouse = false;
|
// disable mouse handling for android to make emulator behavior more coherent
|
||||||
|
game.InputHandler.HandleMouse = false;
|
||||||
|
// enable android text input style
|
||||||
|
game.InputHandler.TextInputStyle = new TextInputStyle.Mobile();
|
||||||
|
};
|
||||||
this.SetContentView((View) g.Services.GetService(typeof(View)));
|
this.SetContentView((View) g.Services.GetService(typeof(View)));
|
||||||
g.Run();
|
g.Run();
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,9 @@ namespace MLEM.Startup {
|
||||||
|
|
||||||
protected override void LoadContent() {
|
protected override void LoadContent() {
|
||||||
this.SpriteBatch = new SpriteBatch(this.GraphicsDevice);
|
this.SpriteBatch = new SpriteBatch(this.GraphicsDevice);
|
||||||
this.InputHandler = new InputHandler();
|
this.InputHandler = new InputHandler(this, textInputStyle: new TextInputStyle.DesktopGl());
|
||||||
this.Components.Add(this.InputHandler);
|
this.Components.Add(this.InputHandler);
|
||||||
this.UiSystem = new UiSystem(this.Window, this.GraphicsDevice, new UntexturedStyle(this.SpriteBatch), this.InputHandler);
|
this.UiSystem = new UiSystem(this, this.GraphicsDevice, new UntexturedStyle(this.SpriteBatch), this.InputHandler);
|
||||||
this.Components.Add(this.UiSystem);
|
this.Components.Add(this.UiSystem);
|
||||||
this.OnLoadContent?.Invoke(this);
|
this.OnLoadContent?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,13 +47,27 @@ namespace MLEM.Ui.Elements {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private bool init;
|
||||||
|
|
||||||
public TextField(Anchor anchor, Vector2 size, Rule rule = null, IGenericFont font = null) : base(anchor, size) {
|
public TextField(Anchor anchor, Vector2 size, Rule rule = null, IGenericFont font = null) : base(anchor, size) {
|
||||||
this.InputRule = rule ?? DefaultRule;
|
this.InputRule = rule ?? DefaultRule;
|
||||||
if (font != null)
|
if (font != null)
|
||||||
this.Font.Set(font);
|
this.Font.Set(font);
|
||||||
|
}
|
||||||
|
|
||||||
if (WindowExtensions.SupportsTextInput()) {
|
public override void ForceUpdateArea() {
|
||||||
|
if (!this.init) {
|
||||||
|
this.init = true;
|
||||||
|
if (this.Input.TextInputStyle.RequiresOnScreenKeyboard()) {
|
||||||
|
this.OnPressed += async e => {
|
||||||
|
if (!KeyboardInput.IsVisible) {
|
||||||
|
var title = this.MobileTitle ?? this.PlaceholderText;
|
||||||
|
var result = await KeyboardInput.Show(title, this.MobileDescription, this.Text);
|
||||||
|
if (result != null)
|
||||||
|
this.SetText(result.Replace('\n', ' '), true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
this.OnTextInput += (element, key, character) => {
|
this.OnTextInput += (element, key, character) => {
|
||||||
if (!this.IsSelected || this.IsHidden)
|
if (!this.IsSelected || this.IsHidden)
|
||||||
return;
|
return;
|
||||||
|
@ -68,18 +82,10 @@ namespace MLEM.Ui.Elements {
|
||||||
this.InsertText(character);
|
this.InsertText(character);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
this.OnDeselected += e => this.CaretPos = 0;
|
||||||
this.OnPressed += async e => {
|
this.OnSelected += e => this.CaretPos = this.text.Length;
|
||||||
if (!KeyboardInput.IsVisible) {
|
|
||||||
var title = this.MobileTitle ?? this.PlaceholderText;
|
|
||||||
var result = await KeyboardInput.Show(title, this.MobileDescription, this.Text);
|
|
||||||
if (result != null)
|
|
||||||
this.SetText(result.Replace('\n', ' '), true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
this.OnDeselected += e => this.CaretPos = 0;
|
base.ForceUpdateArea();
|
||||||
this.OnSelected += e => this.CaretPos = this.text.Length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleTextChange(bool textChanged = true) {
|
private void HandleTextChange(bool textChanged = true) {
|
||||||
|
|
|
@ -36,7 +36,7 @@ namespace MLEM.Ui {
|
||||||
|
|
||||||
public UiControls(UiSystem system, InputHandler inputHandler = null) {
|
public UiControls(UiSystem system, InputHandler inputHandler = null) {
|
||||||
this.System = system;
|
this.System = system;
|
||||||
this.Input = inputHandler ?? new InputHandler();
|
this.Input = inputHandler ?? new InputHandler(system.Game);
|
||||||
this.IsInputOurs = inputHandler == null;
|
this.IsInputOurs = inputHandler == null;
|
||||||
|
|
||||||
// enable all required gestures
|
// enable all required gestures
|
||||||
|
|
|
@ -67,21 +67,21 @@ namespace MLEM.Ui {
|
||||||
public RootCallback OnRootAdded;
|
public RootCallback OnRootAdded;
|
||||||
public RootCallback OnRootRemoved;
|
public RootCallback OnRootRemoved;
|
||||||
|
|
||||||
public UiSystem(GameWindow window, GraphicsDevice device, UiStyle style, InputHandler inputHandler = null) : base(null) {
|
public UiSystem(Game game, GraphicsDevice device, UiStyle style, InputHandler inputHandler = null) : base(game) {
|
||||||
this.Controls = new UiControls(this, inputHandler);
|
this.Controls = new UiControls(this, inputHandler);
|
||||||
this.GraphicsDevice = device;
|
this.GraphicsDevice = device;
|
||||||
this.Window = window;
|
this.Window = game.Window;
|
||||||
this.style = style;
|
this.style = style;
|
||||||
this.Viewport = device.Viewport.Bounds;
|
this.Viewport = device.Viewport.Bounds;
|
||||||
this.AutoScaleReferenceSize = this.Viewport.Size;
|
this.AutoScaleReferenceSize = this.Viewport.Size;
|
||||||
|
|
||||||
window.ClientSizeChanged += (sender, args) => {
|
game.Window.ClientSizeChanged += (sender, args) => {
|
||||||
this.Viewport = device.Viewport.Bounds;
|
this.Viewport = device.Viewport.Bounds;
|
||||||
foreach (var root in this.rootElements)
|
foreach (var root in this.rootElements)
|
||||||
root.Element.ForceUpdateArea();
|
root.Element.ForceUpdateArea();
|
||||||
};
|
};
|
||||||
|
|
||||||
window.AddTextInputListener((sender, key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character)));
|
this.Controls.Input.OnTextInput += (key, character) => this.ApplyToAll(e => e.OnTextInput?.Invoke(e, key, character));
|
||||||
this.OnMousedElementChanged = e => this.ApplyToAll(t => t.OnMousedElementChanged?.Invoke(t, e));
|
this.OnMousedElementChanged = e => this.ApplyToAll(t => t.OnMousedElementChanged?.Invoke(t, e));
|
||||||
this.OnSelectedElementChanged = e => this.ApplyToAll(t => t.OnSelectedElementChanged?.Invoke(t, e));
|
this.OnSelectedElementChanged = e => this.ApplyToAll(t => t.OnSelectedElementChanged?.Invoke(t, e));
|
||||||
this.OnSelectedElementDrawn = (element, time, batch, alpha) => {
|
this.OnSelectedElementDrawn = (element, time, batch, alpha) => {
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.Xna.Framework;
|
|
||||||
using Microsoft.Xna.Framework.Input;
|
|
||||||
|
|
||||||
namespace MLEM.Extensions {
|
|
||||||
public static class WindowExtensions {
|
|
||||||
|
|
||||||
private static readonly bool TextInputSupported = typeof(GameWindow).GetEvent("TextInput") != null;
|
|
||||||
|
|
||||||
public static bool AddTextInputListener(this GameWindow window, TextInputCallback callback) {
|
|
||||||
if (!SupportsTextInput())
|
|
||||||
return false;
|
|
||||||
TextInputAdder.Add(window, callback);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool SupportsTextInput() {
|
|
||||||
return TextInputSupported;
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void TextInputCallback(object sender, Keys key, char character);
|
|
||||||
|
|
||||||
private static class TextInputAdder {
|
|
||||||
|
|
||||||
public static void Add(GameWindow window, TextInputCallback callback) {
|
|
||||||
window.TextInput += (sender, args) => callback(sender, args.Key, args.Character);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -16,6 +16,16 @@ namespace MLEM.Input {
|
||||||
public Keys[] PressedKeys { get; private set; }
|
public Keys[] PressedKeys { get; private set; }
|
||||||
public bool HandleKeyboard;
|
public bool HandleKeyboard;
|
||||||
|
|
||||||
|
private TextInputStyle textInputStyle;
|
||||||
|
public TextInputStyle TextInputStyle {
|
||||||
|
get => this.textInputStyle;
|
||||||
|
set {
|
||||||
|
this.textInputStyle = value;
|
||||||
|
value?.Initialize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public Action<Keys, char> OnTextInput;
|
||||||
|
|
||||||
public MouseState LastMouseState { get; private set; }
|
public MouseState LastMouseState { get; private set; }
|
||||||
public MouseState MouseState { get; private set; }
|
public MouseState MouseState { get; private set; }
|
||||||
public Point MousePosition => this.MouseState.Position;
|
public Point MousePosition => this.MouseState.Position;
|
||||||
|
@ -50,11 +60,12 @@ namespace MLEM.Input {
|
||||||
private readonly bool[] triggerGamepadButtonRepeat = new bool[GamePad.MaximumGamePadCount];
|
private readonly bool[] triggerGamepadButtonRepeat = new bool[GamePad.MaximumGamePadCount];
|
||||||
private readonly Buttons?[] heldGamepadButtons = new Buttons?[GamePad.MaximumGamePadCount];
|
private readonly Buttons?[] heldGamepadButtons = new Buttons?[GamePad.MaximumGamePadCount];
|
||||||
|
|
||||||
public InputHandler(bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true) : base(null) {
|
public InputHandler(Game game, bool handleKeyboard = true, bool handleMouse = true, bool handleGamepads = true, bool handleTouch = true, TextInputStyle textInputStyle = null) : base(game) {
|
||||||
this.HandleKeyboard = handleKeyboard;
|
this.HandleKeyboard = handleKeyboard;
|
||||||
this.HandleMouse = handleMouse;
|
this.HandleMouse = handleMouse;
|
||||||
this.HandleGamepads = handleGamepads;
|
this.HandleGamepads = handleGamepads;
|
||||||
this.HandleTouch = handleTouch;
|
this.HandleTouch = handleTouch;
|
||||||
|
this.TextInputStyle = textInputStyle;
|
||||||
this.Gestures = this.gestures.AsReadOnly();
|
this.Gestures = this.gestures.AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,6 +107,9 @@ namespace MLEM.Input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.TextInputStyle != null)
|
||||||
|
this.TextInputStyle.Update();
|
||||||
|
|
||||||
if (this.HandleMouse) {
|
if (this.HandleMouse) {
|
||||||
this.LastMouseState = this.MouseState;
|
this.LastMouseState = this.MouseState;
|
||||||
this.MouseState = Mouse.GetState();
|
this.MouseState = Mouse.GetState();
|
||||||
|
|
160
MLEM/Input/TextInputStyle.cs
Normal file
160
MLEM/Input/TextInputStyle.cs
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Input;
|
||||||
|
|
||||||
|
namespace MLEM.Input {
|
||||||
|
public abstract class TextInputStyle {
|
||||||
|
|
||||||
|
private InputHandler handler;
|
||||||
|
|
||||||
|
public abstract bool RequiresOnScreenKeyboard();
|
||||||
|
|
||||||
|
public abstract void Update();
|
||||||
|
|
||||||
|
public virtual void Initialize(InputHandler handler) {
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DesktopGl : TextInputStyle {
|
||||||
|
|
||||||
|
private static readonly EventInfo TextInput = typeof(GameWindow).GetEvent("TextInput");
|
||||||
|
private static readonly MethodInfo Callback = typeof(DesktopGl).GetMethod(nameof(OnTextInput));
|
||||||
|
private static PropertyInfo key;
|
||||||
|
private static PropertyInfo character;
|
||||||
|
|
||||||
|
public override bool RequiresOnScreenKeyboard() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Initialize(InputHandler handler) {
|
||||||
|
base.Initialize(handler);
|
||||||
|
if (TextInput != null)
|
||||||
|
TextInput.AddEventHandler(handler.Game.Window, Delegate.CreateDelegate(TextInput.EventHandlerType, this, Callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnTextInput(object sender, EventArgs args) {
|
||||||
|
if (key == null)
|
||||||
|
key = args.GetType().GetProperty("Key");
|
||||||
|
if (character == null)
|
||||||
|
character = args.GetType().GetProperty("Character");
|
||||||
|
this.handler.OnTextInput?.Invoke((Keys) key.GetValue(args), (char) character.GetValue(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Mobile : TextInputStyle {
|
||||||
|
|
||||||
|
public override bool RequiresOnScreenKeyboard() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class American : TextInputStyle {
|
||||||
|
|
||||||
|
public override bool RequiresOnScreenKeyboard() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update() {
|
||||||
|
var pressed = this.handler.KeyboardState.GetPressedKeys().Except(this.handler.LastKeyboardState.GetPressedKeys());
|
||||||
|
var shift = this.handler.IsModifierKeyDown(ModifierKey.Shift);
|
||||||
|
foreach (var key in pressed) {
|
||||||
|
var c = GetChar(key, shift);
|
||||||
|
if (c.HasValue)
|
||||||
|
this.handler.OnTextInput?.Invoke(key, c.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static char? GetChar(Keys key, bool shift) {
|
||||||
|
if (key == Keys.A) return shift ? 'A' : 'a';
|
||||||
|
if (key == Keys.B) return shift ? 'B' : 'b';
|
||||||
|
if (key == Keys.C) return shift ? 'C' : 'c';
|
||||||
|
if (key == Keys.D) return shift ? 'D' : 'd';
|
||||||
|
if (key == Keys.E) return shift ? 'E' : 'e';
|
||||||
|
if (key == Keys.F) return shift ? 'F' : 'f';
|
||||||
|
if (key == Keys.G) return shift ? 'G' : 'g';
|
||||||
|
if (key == Keys.H) return shift ? 'H' : 'h';
|
||||||
|
if (key == Keys.I) return shift ? 'I' : 'i';
|
||||||
|
if (key == Keys.J) return shift ? 'J' : 'j';
|
||||||
|
if (key == Keys.K) return shift ? 'K' : 'k';
|
||||||
|
if (key == Keys.L) return shift ? 'L' : 'l';
|
||||||
|
if (key == Keys.M) return shift ? 'M' : 'm';
|
||||||
|
if (key == Keys.N) return shift ? 'N' : 'n';
|
||||||
|
if (key == Keys.O) return shift ? 'O' : 'o';
|
||||||
|
if (key == Keys.P) return shift ? 'P' : 'p';
|
||||||
|
if (key == Keys.Q) return shift ? 'Q' : 'q';
|
||||||
|
if (key == Keys.R) return shift ? 'R' : 'r';
|
||||||
|
if (key == Keys.S) return shift ? 'S' : 's';
|
||||||
|
if (key == Keys.T) return shift ? 'T' : 't';
|
||||||
|
if (key == Keys.U) return shift ? 'U' : 'u';
|
||||||
|
if (key == Keys.V) return shift ? 'V' : 'v';
|
||||||
|
if (key == Keys.W) return shift ? 'W' : 'w';
|
||||||
|
if (key == Keys.X) return shift ? 'X' : 'x';
|
||||||
|
if (key == Keys.Y) return shift ? 'Y' : 'y';
|
||||||
|
if (key == Keys.Z) return shift ? 'Z' : 'z';
|
||||||
|
if (key == Keys.D0 && !shift || key == Keys.NumPad0) return '0';
|
||||||
|
if (key == Keys.D1 && !shift || key == Keys.NumPad1) return '1';
|
||||||
|
if (key == Keys.D2 && !shift || key == Keys.NumPad2) return '2';
|
||||||
|
if (key == Keys.D3 && !shift || key == Keys.NumPad3) return '3';
|
||||||
|
if (key == Keys.D4 && !shift || key == Keys.NumPad4) return '4';
|
||||||
|
if (key == Keys.D5 && !shift || key == Keys.NumPad5) return '5';
|
||||||
|
if (key == Keys.D6 && !shift || key == Keys.NumPad6) return '6';
|
||||||
|
if (key == Keys.D7 && !shift || key == Keys.NumPad7) return '7';
|
||||||
|
if (key == Keys.D8 && !shift || key == Keys.NumPad8) return '8';
|
||||||
|
if (key == Keys.D9 && !shift || key == Keys.NumPad9) return '9';
|
||||||
|
if (key == Keys.D0 && shift) return ')';
|
||||||
|
if (key == Keys.D1 && shift) return '!';
|
||||||
|
if (key == Keys.D2 && shift) return '@';
|
||||||
|
if (key == Keys.D3 && shift) return '#';
|
||||||
|
if (key == Keys.D4 && shift) return '$';
|
||||||
|
if (key == Keys.D5 && shift) return '%';
|
||||||
|
if (key == Keys.D6 && shift) return '^';
|
||||||
|
if (key == Keys.D7 && shift) return '&';
|
||||||
|
if (key == Keys.D8 && shift) return '*';
|
||||||
|
if (key == Keys.D9 && shift) return '(';
|
||||||
|
if (key == Keys.Space) return ' ';
|
||||||
|
if (key == Keys.Tab) return '\t';
|
||||||
|
if (key == Keys.Add) return '+';
|
||||||
|
if (key == Keys.Decimal) return '.';
|
||||||
|
if (key == Keys.Divide) return '/';
|
||||||
|
if (key == Keys.Multiply) return '*';
|
||||||
|
if (key == Keys.OemBackslash) return '\\';
|
||||||
|
if (key == Keys.OemComma && !shift) return ',';
|
||||||
|
if (key == Keys.OemComma && shift) return '<';
|
||||||
|
if (key == Keys.OemOpenBrackets && !shift) return '[';
|
||||||
|
if (key == Keys.OemOpenBrackets && shift) return '{';
|
||||||
|
if (key == Keys.OemCloseBrackets && !shift) return ']';
|
||||||
|
if (key == Keys.OemCloseBrackets && shift) return '}';
|
||||||
|
if (key == Keys.OemPeriod && !shift) return '.';
|
||||||
|
if (key == Keys.OemPeriod && shift) return '>';
|
||||||
|
if (key == Keys.OemPipe && !shift) return '\\';
|
||||||
|
if (key == Keys.OemPipe && shift) return '|';
|
||||||
|
if (key == Keys.OemPlus && !shift) return '=';
|
||||||
|
if (key == Keys.OemPlus && shift) return '+';
|
||||||
|
if (key == Keys.OemMinus && !shift) return '-';
|
||||||
|
if (key == Keys.OemMinus && shift) return '_';
|
||||||
|
if (key == Keys.OemQuestion && !shift) return '/';
|
||||||
|
if (key == Keys.OemQuestion && shift) return '?';
|
||||||
|
if (key == Keys.OemQuotes && !shift) return '\'';
|
||||||
|
if (key == Keys.OemQuotes && shift) return '"';
|
||||||
|
if (key == Keys.OemSemicolon && !shift) return ';';
|
||||||
|
if (key == Keys.OemSemicolon && shift) return ':';
|
||||||
|
if (key == Keys.OemTilde && !shift) return '`';
|
||||||
|
if (key == Keys.OemTilde && shift) return '~';
|
||||||
|
if (key == Keys.Subtract) return '-';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue