Merge "Refactor PointerTracker and MainKeyboardView"
This commit is contained in:
commit
8e32700914
5 changed files with 112 additions and 122 deletions
|
@ -269,13 +269,9 @@ public final class MainKeyboardAccessibilityDelegate
|
|||
eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
|
||||
// Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
|
||||
tracker.processMotionEvent(downEvent, mKeyDetector);
|
||||
// The above fake down event triggers an unnecessary long press timer that should be
|
||||
// canceled.
|
||||
tracker.cancelLongPressTimer();
|
||||
downEvent.recycle();
|
||||
// Invoke {@link MainKeyboardView#onLongPress(PointerTracker)} as if a long press timeout
|
||||
// has passed.
|
||||
mKeyboardView.onLongPress(tracker);
|
||||
// Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
|
||||
tracker.onLongPressed();
|
||||
// If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
|
||||
// or a key invokes IME switcher dialog, we should just ignore the next
|
||||
// {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
|
||||
|
|
|
@ -461,12 +461,17 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
|||
windowContentView.addView(mDrawingPreviewPlacerView);
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy#onKeyPressed(Key,boolean)}.
|
||||
@Override
|
||||
public void showKeyPreview(@Nonnull final Key key) {
|
||||
// If the key is invalid or has no key preview, we must not show key preview.
|
||||
if (key.noKeyPreview()) {
|
||||
return;
|
||||
public void onKeyPressed(@Nonnull final Key key, final boolean withPreview) {
|
||||
key.onPressed();
|
||||
invalidateKey(key);
|
||||
if (withPreview && !key.noKeyPreview()) {
|
||||
showKeyPreview(key);
|
||||
}
|
||||
}
|
||||
|
||||
private void showKeyPreview(@Nonnull final Key key) {
|
||||
final Keyboard keyboard = getKeyboard();
|
||||
if (keyboard == null) {
|
||||
return;
|
||||
|
@ -483,15 +488,26 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
|||
getWidth(), mOriginCoords, mDrawingPreviewPlacerView, isHardwareAccelerated());
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy#dismissKeyPreviewWithoutDelay(Key)}.
|
||||
@Override
|
||||
public void dismissKeyPreviewWithoutDelay(@Nonnull final Key key) {
|
||||
private void dismissKeyPreviewWithoutDelay(@Nonnull final Key key) {
|
||||
mKeyPreviewChoreographer.dismissKeyPreview(key, false /* withAnimation */);
|
||||
invalidateKey(key);
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy#onKeyReleased(Key,boolean)}.
|
||||
@Override
|
||||
public void dismissKeyPreview(@Nonnull final Key key) {
|
||||
public void onKeyReleased(@Nonnull final Key key, final boolean withAnimation) {
|
||||
key.onReleased();
|
||||
invalidateKey(key);
|
||||
if (!key.noKeyPreview()) {
|
||||
if (withAnimation) {
|
||||
dismissKeyPreview(key);
|
||||
} else {
|
||||
dismissKeyPreviewWithoutDelay(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dismissKeyPreview(@Nonnull final Key key) {
|
||||
if (isHardwareAccelerated()) {
|
||||
mKeyPreviewChoreographer.dismissKeyPreview(key, true /* withAnimation */);
|
||||
return;
|
||||
|
@ -574,7 +590,11 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
|||
mDrawingPreviewPlacerView.removeAllViews();
|
||||
}
|
||||
|
||||
private MoreKeysPanel onCreateMoreKeysPanel(final Key key, final Context context) {
|
||||
// Implements {@link DrawingProxy@showMoreKeysKeyboard(Key,PointerTracker)}.
|
||||
@Override
|
||||
@Nullable
|
||||
public MoreKeysPanel showMoreKeysKeyboard(@Nonnull final Key key,
|
||||
@Nonnull final PointerTracker tracker) {
|
||||
final MoreKeySpec[] moreKeys = key.getMoreKeys();
|
||||
if (moreKeys == null) {
|
||||
return null;
|
||||
|
@ -590,7 +610,7 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
|||
&& !key.noKeyPreview() && moreKeys.length == 1
|
||||
&& mKeyPreviewDrawParams.getVisibleWidth() > 0;
|
||||
final MoreKeysKeyboard.Builder builder = new MoreKeysKeyboard.Builder(
|
||||
context, key, getKeyboard(), isSingleMoreKeyWithPreview,
|
||||
getContext(), key, getKeyboard(), isSingleMoreKeyWithPreview,
|
||||
mKeyPreviewDrawParams.getVisibleWidth(),
|
||||
mKeyPreviewDrawParams.getVisibleHeight(), newLabelPaint(key));
|
||||
moreKeysKeyboard = builder.build();
|
||||
|
@ -603,50 +623,6 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
|||
(MoreKeysKeyboardView)container.findViewById(R.id.more_keys_keyboard_view);
|
||||
moreKeysKeyboardView.setKeyboard(moreKeysKeyboard);
|
||||
container.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
return moreKeysKeyboardView;
|
||||
}
|
||||
|
||||
// Implements {@link DrawingProxy@onLongPress(PointerTracker)}.
|
||||
/**
|
||||
* Called when a key is long pressed.
|
||||
* @param tracker the pointer tracker which pressed the parent key
|
||||
*/
|
||||
@Override
|
||||
public void onLongPress(@Nonnull final PointerTracker tracker) {
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
return;
|
||||
}
|
||||
final Key key = tracker.getKey();
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
final KeyboardActionListener listener = mKeyboardActionListener;
|
||||
if (key.hasNoPanelAutoMoreKey()) {
|
||||
final int moreKeyCode = key.getMoreKeys()[0].mCode;
|
||||
tracker.onLongPressed();
|
||||
listener.onPressKey(moreKeyCode, 0 /* repeatCount */, true /* isSinglePointer */);
|
||||
listener.onCodeInput(moreKeyCode, Constants.NOT_A_COORDINATE,
|
||||
Constants.NOT_A_COORDINATE, false /* isKeyRepeat */);
|
||||
listener.onReleaseKey(moreKeyCode, false /* withSliding */);
|
||||
return;
|
||||
}
|
||||
final int code = key.getCode();
|
||||
if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
|
||||
// Long pressing the space key invokes IME switcher dialog.
|
||||
if (listener.onCustomRequest(Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)) {
|
||||
tracker.onLongPressed();
|
||||
listener.onReleaseKey(code, false /* withSliding */);
|
||||
return;
|
||||
}
|
||||
}
|
||||
openMoreKeysPanel(key, tracker);
|
||||
}
|
||||
|
||||
private void openMoreKeysPanel(final Key key, final PointerTracker tracker) {
|
||||
final MoreKeysPanel moreKeysPanel = onCreateMoreKeysPanel(key, getContext());
|
||||
if (moreKeysPanel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int[] lastCoords = CoordinateUtils.newInstance();
|
||||
tracker.getLastCoordinates(lastCoords);
|
||||
|
@ -664,10 +640,8 @@ public final class MainKeyboardView extends KeyboardView implements DrawingProxy
|
|||
// {@code mPreviewVisibleOffset} has been set appropriately in
|
||||
// {@link KeyboardView#showKeyPreview(PointerTracker)}.
|
||||
final int pointY = key.getY() + mKeyPreviewDrawParams.getVisibleOffset();
|
||||
moreKeysPanel.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
|
||||
tracker.onShowMoreKeysPanel(moreKeysPanel);
|
||||
// TODO: Implement zoom in animation of more keys panel.
|
||||
mKeyPreviewChoreographer.dismissKeyPreview(key, false /* withAnimation */);
|
||||
moreKeysKeyboardView.showMoreKeysPanel(this, this, pointX, pointY, mKeyboardActionListener);
|
||||
return moreKeysKeyboardView;
|
||||
}
|
||||
|
||||
public boolean isInDraggingFinger() {
|
||||
|
|
|
@ -222,7 +222,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
final int trackersSize = sTrackers.size();
|
||||
for (int i = 0; i < trackersSize; ++i) {
|
||||
final PointerTracker tracker = sTrackers.get(i);
|
||||
tracker.setReleasedKeyGraphics(tracker.getKey());
|
||||
tracker.setReleasedKeyGraphics(tracker.getKey(), true /* withAnimation */);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -382,19 +382,18 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
return mKeyDetector.detectHitKey(x, y);
|
||||
}
|
||||
|
||||
private void setReleasedKeyGraphics(@Nullable final Key key) {
|
||||
private void setReleasedKeyGraphics(@Nullable final Key key, final boolean withAnimation) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
sDrawingProxy.dismissKeyPreview(key);
|
||||
// Even if the key is disabled, update the key release graphics just in case.
|
||||
updateReleaseKeyGraphics(key);
|
||||
sDrawingProxy.onKeyReleased(key, withAnimation);
|
||||
|
||||
if (key.isShift()) {
|
||||
for (final Key shiftKey : mKeyboard.mShiftKeys) {
|
||||
if (shiftKey != key) {
|
||||
updateReleaseKeyGraphics(shiftKey);
|
||||
sDrawingProxy.onKeyReleased(shiftKey, false /* withAnimation */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -403,11 +402,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
final int altCode = key.getAltCode();
|
||||
final Key altKey = mKeyboard.getKey(altCode);
|
||||
if (altKey != null) {
|
||||
updateReleaseKeyGraphics(altKey);
|
||||
sDrawingProxy.onKeyReleased(altKey, false /* withAnimation */);
|
||||
}
|
||||
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
|
||||
if (k != key && k.getAltCode() == altCode) {
|
||||
updateReleaseKeyGraphics(k);
|
||||
sDrawingProxy.onKeyReleased(k, false /* withAnimation */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -418,7 +417,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
return sTypingTimeRecorder.needsToSuppressKeyPreviewPopup(eventTime);
|
||||
}
|
||||
|
||||
private void setPressedKeyGraphics(final Key key, final long eventTime) {
|
||||
private void setPressedKeyGraphics(@Nullable final Key key, final long eventTime) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -430,15 +429,13 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
return;
|
||||
}
|
||||
|
||||
if (!key.noKeyPreview() && !sInGesture && !needsToSuppressKeyPreviewPopup(eventTime)) {
|
||||
sDrawingProxy.showKeyPreview(key);
|
||||
}
|
||||
updatePressKeyGraphics(key);
|
||||
final boolean noKeyPreview = sInGesture || needsToSuppressKeyPreviewPopup(eventTime);
|
||||
sDrawingProxy.onKeyPressed(key, !noKeyPreview);
|
||||
|
||||
if (key.isShift()) {
|
||||
for (final Key shiftKey : mKeyboard.mShiftKeys) {
|
||||
if (shiftKey != key) {
|
||||
updatePressKeyGraphics(shiftKey);
|
||||
sDrawingProxy.onKeyPressed(shiftKey, false /* withPreview */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -447,26 +444,16 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
final int altCode = key.getAltCode();
|
||||
final Key altKey = mKeyboard.getKey(altCode);
|
||||
if (altKey != null) {
|
||||
updatePressKeyGraphics(altKey);
|
||||
sDrawingProxy.onKeyPressed(altKey, false /* withPreview */);
|
||||
}
|
||||
for (final Key k : mKeyboard.mAltCodeKeysWhileTyping) {
|
||||
if (k != key && k.getAltCode() == altCode) {
|
||||
updatePressKeyGraphics(k);
|
||||
sDrawingProxy.onKeyPressed(k, false /* withPreview */);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void updateReleaseKeyGraphics(final Key key) {
|
||||
key.onReleased();
|
||||
sDrawingProxy.invalidateKey(key);
|
||||
}
|
||||
|
||||
private static void updatePressKeyGraphics(final Key key) {
|
||||
key.onPressed();
|
||||
sDrawingProxy.invalidateKey(key);
|
||||
}
|
||||
|
||||
public GestureStrokeDrawingPoints getGestureStrokeDrawingPoints() {
|
||||
return mGestureStrokeDrawingPoints;
|
||||
}
|
||||
|
@ -837,7 +824,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
}
|
||||
|
||||
private void processDraggingFingerOutFromOldKey(final Key oldKey) {
|
||||
setReleasedKeyGraphics(oldKey);
|
||||
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
|
||||
callListenerOnRelease(oldKey, oldKey.getCode(), true /* withSliding */);
|
||||
startKeySelectionByDraggingFinger(oldKey);
|
||||
sTimerProxy.cancelKeyTimersOf(this);
|
||||
|
@ -880,12 +867,12 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
}
|
||||
onUpEvent(x, y, eventTime);
|
||||
cancelTrackingForAction();
|
||||
setReleasedKeyGraphics(oldKey);
|
||||
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
|
||||
} else {
|
||||
if (!mIsDetectingGesture) {
|
||||
cancelTrackingForAction();
|
||||
}
|
||||
setReleasedKeyGraphics(oldKey);
|
||||
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -913,7 +900,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
onGestureMoveEvent(x, y, eventTime, true /* isMajorEvent */, newKey);
|
||||
if (sInGesture) {
|
||||
mCurrentKey = null;
|
||||
setReleasedKeyGraphics(oldKey);
|
||||
setReleasedKeyGraphics(oldKey, true /* withAnimation */);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -978,7 +965,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
final int currentRepeatingKeyCode = mCurrentRepeatingKeyCode;
|
||||
mCurrentRepeatingKeyCode = Constants.NOT_A_CODE;
|
||||
// Release the last pressed key.
|
||||
setReleasedKeyGraphics(currentKey);
|
||||
setReleasedKeyGraphics(currentKey, true /* withAnimation */);
|
||||
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
if (!mIsTrackingForActionDisabled) {
|
||||
|
@ -1015,14 +1002,6 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
}
|
||||
}
|
||||
|
||||
public void onShowMoreKeysPanel(final MoreKeysPanel panel) {
|
||||
setReleasedKeyGraphics(mCurrentKey);
|
||||
final int translatedX = panel.translateX(mLastX);
|
||||
final int translatedY = panel.translateY(mLastY);
|
||||
panel.onDownEvent(translatedX, translatedY, mPointerId, SystemClock.uptimeMillis());
|
||||
mMoreKeysPanel = panel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelTrackingForAction() {
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
|
@ -1035,14 +1014,49 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
return !mIsTrackingForActionDisabled;
|
||||
}
|
||||
|
||||
public void cancelLongPressTimer() {
|
||||
public void onLongPressed() {
|
||||
sTimerProxy.cancelLongPressTimersOf(this);
|
||||
if (isShowingMoreKeysPanel()) {
|
||||
return;
|
||||
}
|
||||
final Key key = getKey();
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
if (key.hasNoPanelAutoMoreKey()) {
|
||||
cancelKeyTracking();
|
||||
final int moreKeyCode = key.getMoreKeys()[0].mCode;
|
||||
sListener.onPressKey(moreKeyCode, 0 /* repeatCont */, true /* isSinglePointer */);
|
||||
sListener.onCodeInput(moreKeyCode, Constants.NOT_A_COORDINATE,
|
||||
Constants.NOT_A_COORDINATE, false /* isKeyRepeat */);
|
||||
sListener.onReleaseKey(moreKeyCode, false /* withSliding */);
|
||||
return;
|
||||
}
|
||||
final int code = key.getCode();
|
||||
if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
|
||||
// Long pressing the space key invokes IME switcher dialog.
|
||||
if (sListener.onCustomRequest(Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)) {
|
||||
cancelKeyTracking();
|
||||
sListener.onReleaseKey(code, false /* withSliding */);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void onLongPressed() {
|
||||
setReleasedKeyGraphics(key, false /* withAnimation */);
|
||||
final MoreKeysPanel moreKeysPanel = sDrawingProxy.showMoreKeysKeyboard(key, this);
|
||||
if (moreKeysPanel == null) {
|
||||
return;
|
||||
}
|
||||
final int translatedX = moreKeysPanel.translateX(mLastX);
|
||||
final int translatedY = moreKeysPanel.translateY(mLastY);
|
||||
moreKeysPanel.onDownEvent(translatedX, translatedY, mPointerId, SystemClock.uptimeMillis());
|
||||
mMoreKeysPanel = moreKeysPanel;
|
||||
}
|
||||
|
||||
private void cancelKeyTracking() {
|
||||
resetKeySelectionByDraggingFinger();
|
||||
cancelTrackingForAction();
|
||||
setReleasedKeyGraphics(mCurrentKey);
|
||||
setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */);
|
||||
sPointerTrackerQueue.remove(this);
|
||||
}
|
||||
|
||||
|
@ -1059,7 +1073,7 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
|
|||
|
||||
private void onCancelEventInternal() {
|
||||
sTimerProxy.cancelKeyTimersOf(this);
|
||||
setReleasedKeyGraphics(mCurrentKey);
|
||||
setReleasedKeyGraphics(mCurrentKey, true /* withAnimation */);
|
||||
resetKeySelectionByDraggingFinger();
|
||||
dismissMoreKeysPanel();
|
||||
}
|
||||
|
|
|
@ -17,29 +17,36 @@
|
|||
package com.android.inputmethod.keyboard.internal;
|
||||
|
||||
import com.android.inputmethod.keyboard.Key;
|
||||
import com.android.inputmethod.keyboard.MoreKeysPanel;
|
||||
import com.android.inputmethod.keyboard.PointerTracker;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public interface DrawingProxy {
|
||||
// TODO: Remove this method.
|
||||
public void invalidateKey(@Nullable Key key);
|
||||
|
||||
// TODO: Rename this method to onKeyPressed.
|
||||
public void showKeyPreview(@Nonnull Key key);
|
||||
|
||||
// TODO: Rename this method to onKeyReleased.
|
||||
public void dismissKeyPreview(@Nonnull Key key);
|
||||
/**
|
||||
* Called when a key is being pressed.
|
||||
* @param key the {@link Key} that is being pressed.
|
||||
* @param withPreview true if key popup preview should be displayed.
|
||||
*/
|
||||
public void onKeyPressed(@Nonnull Key key, boolean withPreview);
|
||||
|
||||
/**
|
||||
* Dismiss a key preview visual without delay.
|
||||
* @param key the key whose preview visual should be dismissed.
|
||||
* Called when a key is being released.
|
||||
* @param key the {@link Key} that is being released.
|
||||
* @param withAnimation when true, key popup preview should be dismissed with animation.
|
||||
*/
|
||||
public void dismissKeyPreviewWithoutDelay(@Nonnull Key key);
|
||||
public void onKeyReleased(@Nonnull Key key, boolean withAnimation);
|
||||
|
||||
// TODO: Rename this method to onKeyLongPressed.
|
||||
public void onLongPress(@Nonnull PointerTracker tracker);
|
||||
/**
|
||||
* Start showing more keys keyboard of a key that is being long pressed.
|
||||
* @param key the {@link Key} that is being long pressed and showing more keys keyboard.
|
||||
* @param tracker the {@link PointerTracker} that detects this long pressing.
|
||||
* @return {@link MoreKeysPanel} that is being shown. null if there is no need to show more keys
|
||||
* keyboard.
|
||||
*/
|
||||
@Nullable
|
||||
public MoreKeysPanel showMoreKeysKeyboard(@Nonnull Key key, @Nonnull PointerTracker tracker);
|
||||
|
||||
/**
|
||||
* Start a while-typing-animation.
|
||||
|
|
|
@ -66,7 +66,7 @@ public final class TimerHandler extends LeakGuardHandlerWrapper<DrawingProxy>
|
|||
case MSG_LONGPRESS_SHIFT_KEY:
|
||||
cancelLongPressTimers();
|
||||
final PointerTracker tracker2 = (PointerTracker) msg.obj;
|
||||
drawingProxy.onLongPress(tracker2);
|
||||
tracker2.onLongPressed();
|
||||
break;
|
||||
case MSG_UPDATE_BATCH_INPUT:
|
||||
final PointerTracker tracker3 = (PointerTracker) msg.obj;
|
||||
|
@ -74,8 +74,7 @@ public final class TimerHandler extends LeakGuardHandlerWrapper<DrawingProxy>
|
|||
startUpdateBatchInputTimer(tracker3);
|
||||
break;
|
||||
case MSG_DISMISS_KEY_PREVIEW:
|
||||
final Key key = (Key) msg.obj;
|
||||
drawingProxy.dismissKeyPreviewWithoutDelay(key);
|
||||
drawingProxy.onKeyReleased((Key) msg.obj, false /* withAnimation */);
|
||||
break;
|
||||
case MSG_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT:
|
||||
drawingProxy.dismissGestureFloatingPreviewTextWithoutDelay();
|
||||
|
|
Loading…
Reference in a new issue