[HW15] Pass events instead of reading them from the transaction
It's probably cleaner to just pass the events rather than read them from a transaction, especially when a transaction may be associated with several events in a chain. Change-Id: I27830f0f3f3f32fe77ea8b9cad505b7ebee648af
This commit is contained in:
parent
203ba408d7
commit
f69bb4c7a3
2 changed files with 52 additions and 43 deletions
|
@ -33,7 +33,7 @@ public class InputTransaction {
|
||||||
|
|
||||||
// Initial conditions
|
// Initial conditions
|
||||||
public final SettingsValues mSettingsValues;
|
public final SettingsValues mSettingsValues;
|
||||||
public final Event mEvent;
|
private final Event mEvent;
|
||||||
public final long mTimestamp;
|
public final long mTimestamp;
|
||||||
public final int mSpaceState;
|
public final int mSpaceState;
|
||||||
public final int mShiftState;
|
public final int mShiftState;
|
||||||
|
|
|
@ -427,11 +427,12 @@ public final class InputLogic {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedEvent.isConsumed()) {
|
if (processedEvent.isConsumed()) {
|
||||||
handleConsumedEvent(inputTransaction);
|
handleConsumedEvent(processedEvent, inputTransaction);
|
||||||
} else if (processedEvent.isFunctionalKeyEvent()) {
|
} else if (processedEvent.isFunctionalKeyEvent()) {
|
||||||
handleFunctionalEvent(inputTransaction, currentKeyboardScriptId, handler);
|
handleFunctionalEvent(processedEvent, inputTransaction, currentKeyboardScriptId,
|
||||||
|
handler);
|
||||||
} else {
|
} else {
|
||||||
handleNonFunctionalEvent(inputTransaction, handler);
|
handleNonFunctionalEvent(processedEvent, inputTransaction, handler);
|
||||||
}
|
}
|
||||||
if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
|
if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT
|
||||||
&& processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
|
&& processedEvent.mKeyCode != Constants.CODE_CAPSLOCK
|
||||||
|
@ -584,13 +585,14 @@ public final class InputLogic {
|
||||||
* Consumed events represent events that have already been consumed, typically by the
|
* Consumed events represent events that have already been consumed, typically by the
|
||||||
* combining chain.
|
* combining chain.
|
||||||
*
|
*
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
*/
|
*/
|
||||||
private void handleConsumedEvent(final InputTransaction inputTransaction) {
|
private void handleConsumedEvent(final Event event, final InputTransaction inputTransaction) {
|
||||||
// A consumed event may have text to commit and an update to the composing state, so
|
// A consumed event may have text to commit and an update to the composing state, so
|
||||||
// we evaluate both. With some combiners, it's possible than an event contains both
|
// we evaluate both. With some combiners, it's possible than an event contains both
|
||||||
// and we enter both of the following if clauses.
|
// and we enter both of the following if clauses.
|
||||||
final CharSequence textToCommit = inputTransaction.mEvent.getTextToCommit();
|
final CharSequence textToCommit = event.getTextToCommit();
|
||||||
if (!TextUtils.isEmpty(textToCommit)) {
|
if (!TextUtils.isEmpty(textToCommit)) {
|
||||||
mConnection.commitText(textToCommit, 1);
|
mConnection.commitText(textToCommit, 1);
|
||||||
inputTransaction.setDidAffectContents();
|
inputTransaction.setDidAffectContents();
|
||||||
|
@ -611,15 +613,15 @@ public final class InputLogic {
|
||||||
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
|
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
|
||||||
* any key that results in multiple code points like the ".com" key.
|
* any key that results in multiple code points like the ".com" key.
|
||||||
*
|
*
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
*/
|
*/
|
||||||
private void handleFunctionalEvent(final InputTransaction inputTransaction,
|
private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction,
|
||||||
// TODO: remove these arguments
|
// TODO: remove these arguments
|
||||||
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
|
final int currentKeyboardScriptId, final LatinIME.UIHandler handler) {
|
||||||
final Event event = inputTransaction.mEvent;
|
|
||||||
switch (event.mKeyCode) {
|
switch (event.mKeyCode) {
|
||||||
case Constants.CODE_DELETE:
|
case Constants.CODE_DELETE:
|
||||||
handleBackspace(inputTransaction, currentKeyboardScriptId);
|
handleBackspaceEvent(event, inputTransaction, currentKeyboardScriptId);
|
||||||
// Backspace is a functional key, but it affects the contents of the editor.
|
// Backspace is a functional key, but it affects the contents of the editor.
|
||||||
inputTransaction.setDidAffectContents();
|
inputTransaction.setDidAffectContents();
|
||||||
break;
|
break;
|
||||||
|
@ -670,11 +672,7 @@ public final class InputLogic {
|
||||||
// TODO: remove this object
|
// TODO: remove this object
|
||||||
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
|
final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER,
|
||||||
event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
|
event.mKeyCode, event.mX, event.mY, event.isKeyRepeat());
|
||||||
final InputTransaction tmpTransaction = new InputTransaction(
|
handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler);
|
||||||
inputTransaction.mSettingsValues, tmpEvent,
|
|
||||||
inputTransaction.mTimestamp, inputTransaction.mSpaceState,
|
|
||||||
inputTransaction.mShiftState);
|
|
||||||
handleNonSpecialCharacter(tmpTransaction, handler);
|
|
||||||
// Shift + Enter is treated as a functional key but it results in adding a new
|
// Shift + Enter is treated as a functional key but it results in adding a new
|
||||||
// line, so that does affect the contents of the editor.
|
// line, so that does affect the contents of the editor.
|
||||||
inputTransaction.setDidAffectContents();
|
inputTransaction.setDidAffectContents();
|
||||||
|
@ -690,12 +688,13 @@ public final class InputLogic {
|
||||||
* These events are generally events that cause input, but in some cases they may do other
|
* These events are generally events that cause input, but in some cases they may do other
|
||||||
* things like trigger an editor action.
|
* things like trigger an editor action.
|
||||||
*
|
*
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
*/
|
*/
|
||||||
private void handleNonFunctionalEvent(final InputTransaction inputTransaction,
|
private void handleNonFunctionalEvent(final Event event,
|
||||||
|
final InputTransaction inputTransaction,
|
||||||
// TODO: remove this argument
|
// TODO: remove this argument
|
||||||
final LatinIME.UIHandler handler) {
|
final LatinIME.UIHandler handler) {
|
||||||
final Event event = inputTransaction.mEvent;
|
|
||||||
inputTransaction.setDidAffectContents();
|
inputTransaction.setDidAffectContents();
|
||||||
switch (event.mCodePoint) {
|
switch (event.mCodePoint) {
|
||||||
case Constants.CODE_ENTER:
|
case Constants.CODE_ENTER:
|
||||||
|
@ -718,11 +717,11 @@ public final class InputLogic {
|
||||||
} else {
|
} else {
|
||||||
// No action label, and the action from imeOptions is NONE: this is a regular
|
// No action label, and the action from imeOptions is NONE: this is a regular
|
||||||
// enter key that should input a carriage return.
|
// enter key that should input a carriage return.
|
||||||
handleNonSpecialCharacter(inputTransaction, handler);
|
handleNonSpecialCharacterEvent(event, inputTransaction, handler);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
handleNonSpecialCharacter(inputTransaction, handler);
|
handleNonSpecialCharacterEvent(event, inputTransaction, handler);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -735,16 +734,18 @@ public final class InputLogic {
|
||||||
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
|
* manage keyboard-related stuff like shift, language switch, settings, layout switch, or
|
||||||
* any key that results in multiple code points like the ".com" key.
|
* any key that results in multiple code points like the ".com" key.
|
||||||
*
|
*
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
*/
|
*/
|
||||||
private void handleNonSpecialCharacter(final InputTransaction inputTransaction,
|
private void handleNonSpecialCharacterEvent(final Event event,
|
||||||
|
final InputTransaction inputTransaction,
|
||||||
// TODO: remove this argument
|
// TODO: remove this argument
|
||||||
final LatinIME.UIHandler handler) {
|
final LatinIME.UIHandler handler) {
|
||||||
final int codePoint = inputTransaction.mEvent.mCodePoint;
|
final int codePoint = event.mCodePoint;
|
||||||
mSpaceState = SpaceState.NONE;
|
mSpaceState = SpaceState.NONE;
|
||||||
if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
|
if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
|
||||||
|| Character.getType(codePoint) == Character.OTHER_SYMBOL) {
|
|| Character.getType(codePoint) == Character.OTHER_SYMBOL) {
|
||||||
handleSeparator(inputTransaction, handler);
|
handleSeparatorEvent(event, inputTransaction, handler);
|
||||||
} else {
|
} else {
|
||||||
if (SpaceState.PHANTOM == inputTransaction.mSpaceState) {
|
if (SpaceState.PHANTOM == inputTransaction.mSpaceState) {
|
||||||
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
|
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
|
||||||
|
@ -756,21 +757,23 @@ public final class InputLogic {
|
||||||
commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR);
|
commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleNonSeparator(inputTransaction.mSettingsValues, inputTransaction);
|
handleNonSeparatorEvent(event, inputTransaction.mSettingsValues, inputTransaction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a non-separator.
|
* Handle a non-separator.
|
||||||
|
* @param event The event to handle.
|
||||||
* @param settingsValues The current settings values.
|
* @param settingsValues The current settings values.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
*/
|
*/
|
||||||
private void handleNonSeparator(final SettingsValues settingsValues,
|
private void handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues,
|
||||||
final InputTransaction inputTransaction) {
|
final InputTransaction inputTransaction) {
|
||||||
final int codePoint = inputTransaction.mEvent.mCodePoint;
|
final int codePoint = event.mCodePoint;
|
||||||
// TODO: refactor this method to stop flipping isComposingWord around all the time, and
|
// TODO: refactor this method to stop flipping isComposingWord around all the time, and
|
||||||
// make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
|
// make it shorter (possibly cut into several pieces). Also factor
|
||||||
// which has the same name as other handle* methods but is not the same.
|
// handleNonSpecialCharacterEvent which has the same name as other handle* methods but is
|
||||||
|
// not the same.
|
||||||
boolean isComposingWord = mWordComposer.isComposingWord();
|
boolean isComposingWord = mWordComposer.isComposingWord();
|
||||||
|
|
||||||
// TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
|
// TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
|
||||||
|
@ -817,7 +820,7 @@ public final class InputLogic {
|
||||||
resetComposingState(false /* alsoResetLastComposedWord */);
|
resetComposingState(false /* alsoResetLastComposedWord */);
|
||||||
}
|
}
|
||||||
if (isComposingWord) {
|
if (isComposingWord) {
|
||||||
mWordComposer.applyProcessedEvent(inputTransaction.mEvent);
|
mWordComposer.applyProcessedEvent(event);
|
||||||
// If it's the first letter, make note of auto-caps state
|
// If it's the first letter, make note of auto-caps state
|
||||||
if (mWordComposer.isSingleLetter()) {
|
if (mWordComposer.isSingleLetter()) {
|
||||||
mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState);
|
mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState);
|
||||||
|
@ -825,10 +828,10 @@ public final class InputLogic {
|
||||||
mConnection.setComposingText(getTextWithUnderline(
|
mConnection.setComposingText(getTextWithUnderline(
|
||||||
mWordComposer.getTypedWord()), 1);
|
mWordComposer.getTypedWord()), 1);
|
||||||
} else {
|
} else {
|
||||||
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(
|
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
|
||||||
inputTransaction);
|
inputTransaction);
|
||||||
|
|
||||||
if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) {
|
if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
|
||||||
mSpaceState = SpaceState.WEAK;
|
mSpaceState = SpaceState.WEAK;
|
||||||
} else {
|
} else {
|
||||||
sendKeyCodePoint(settingsValues, codePoint);
|
sendKeyCodePoint(settingsValues, codePoint);
|
||||||
|
@ -841,12 +844,13 @@ public final class InputLogic {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle input of a separator code point.
|
* Handle input of a separator code point.
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
*/
|
*/
|
||||||
private void handleSeparator(final InputTransaction inputTransaction,
|
private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction,
|
||||||
// TODO: remove this argument
|
// TODO: remove this argument
|
||||||
final LatinIME.UIHandler handler) {
|
final LatinIME.UIHandler handler) {
|
||||||
final int codePoint = inputTransaction.mEvent.mCodePoint;
|
final int codePoint = event.mCodePoint;
|
||||||
final SettingsValues settingsValues = inputTransaction.mSettingsValues;
|
final SettingsValues settingsValues = inputTransaction.mSettingsValues;
|
||||||
final boolean wasComposingWord = mWordComposer.isComposingWord();
|
final boolean wasComposingWord = mWordComposer.isComposingWord();
|
||||||
// We avoid sending spaces in languages without spaces if we were composing.
|
// We avoid sending spaces in languages without spaces if we were composing.
|
||||||
|
@ -872,7 +876,7 @@ public final class InputLogic {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(
|
final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event,
|
||||||
inputTransaction);
|
inputTransaction);
|
||||||
|
|
||||||
final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint
|
final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint
|
||||||
|
@ -897,10 +901,10 @@ public final class InputLogic {
|
||||||
promotePhantomSpace(settingsValues);
|
promotePhantomSpace(settingsValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tryPerformDoubleSpacePeriod(inputTransaction)) {
|
if (tryPerformDoubleSpacePeriod(event, inputTransaction)) {
|
||||||
mSpaceState = SpaceState.DOUBLE;
|
mSpaceState = SpaceState.DOUBLE;
|
||||||
inputTransaction.setRequiresUpdateSuggestions();
|
inputTransaction.setRequiresUpdateSuggestions();
|
||||||
} else if (swapWeakSpace && trySwapSwapperAndSpace(inputTransaction)) {
|
} else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) {
|
||||||
mSpaceState = SpaceState.SWAP_PUNCTUATION;
|
mSpaceState = SpaceState.SWAP_PUNCTUATION;
|
||||||
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
|
mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
|
||||||
} else if (Constants.CODE_SPACE == codePoint) {
|
} else if (Constants.CODE_SPACE == codePoint) {
|
||||||
|
@ -947,12 +951,12 @@ public final class InputLogic {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a press on the backspace key.
|
* Handle a press on the backspace key.
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
*/
|
*/
|
||||||
private void handleBackspace(final InputTransaction inputTransaction,
|
private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction,
|
||||||
// TODO: remove this argument, put it into settingsValues
|
// TODO: remove this argument, put it into settingsValues
|
||||||
final int currentKeyboardScriptId) {
|
final int currentKeyboardScriptId) {
|
||||||
final Event event = inputTransaction.mEvent;
|
|
||||||
mSpaceState = SpaceState.NONE;
|
mSpaceState = SpaceState.NONE;
|
||||||
mDeleteCount++;
|
mDeleteCount++;
|
||||||
|
|
||||||
|
@ -1103,16 +1107,18 @@ public final class InputLogic {
|
||||||
*
|
*
|
||||||
* This method will check that there are two characters before the cursor and that the first
|
* This method will check that there are two characters before the cursor and that the first
|
||||||
* one is a space before it does the actual swapping.
|
* one is a space before it does the actual swapping.
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
* @return true if the swap has been performed, false if it was prevented by preliminary checks.
|
* @return true if the swap has been performed, false if it was prevented by preliminary checks.
|
||||||
*/
|
*/
|
||||||
private boolean trySwapSwapperAndSpace(final InputTransaction inputTransaction) {
|
private boolean trySwapSwapperAndSpace(final Event event,
|
||||||
|
final InputTransaction inputTransaction) {
|
||||||
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
|
final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
|
||||||
if (Constants.CODE_SPACE != codePointBeforeCursor) {
|
if (Constants.CODE_SPACE != codePointBeforeCursor) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
mConnection.deleteSurroundingText(1, 0);
|
mConnection.deleteSurroundingText(1, 0);
|
||||||
final String text = inputTransaction.mEvent.getTextToCommit() + " ";
|
final String text = event.getTextToCommit() + " ";
|
||||||
mConnection.commitText(text, 1);
|
mConnection.commitText(text, 1);
|
||||||
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
|
inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
|
||||||
return true;
|
return true;
|
||||||
|
@ -1120,13 +1126,14 @@ public final class InputLogic {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Strip a trailing space if necessary and returns whether it's a swap weak space situation.
|
* Strip a trailing space if necessary and returns whether it's a swap weak space situation.
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
* @return whether we should swap the space instead of removing it.
|
* @return whether we should swap the space instead of removing it.
|
||||||
*/
|
*/
|
||||||
private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(
|
private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event,
|
||||||
final InputTransaction inputTransaction) {
|
final InputTransaction inputTransaction) {
|
||||||
final int codePoint = inputTransaction.mEvent.mCodePoint;
|
final int codePoint = event.mCodePoint;
|
||||||
final boolean isFromSuggestionStrip = inputTransaction.mEvent.isSuggestionStripPress();
|
final boolean isFromSuggestionStrip = event.isSuggestionStripPress();
|
||||||
if (Constants.CODE_ENTER == codePoint &&
|
if (Constants.CODE_ENTER == codePoint &&
|
||||||
SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
|
SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) {
|
||||||
mConnection.removeTrailingSpace();
|
mConnection.removeTrailingSpace();
|
||||||
|
@ -1171,14 +1178,16 @@ public final class InputLogic {
|
||||||
* these conditions are fulfilled, this method applies the transformation and returns true.
|
* these conditions are fulfilled, this method applies the transformation and returns true.
|
||||||
* Otherwise, it does nothing and returns false.
|
* Otherwise, it does nothing and returns false.
|
||||||
*
|
*
|
||||||
|
* @param event The event to handle.
|
||||||
* @param inputTransaction The transaction in progress.
|
* @param inputTransaction The transaction in progress.
|
||||||
* @return true if we applied the double-space-to-period transformation, false otherwise.
|
* @return true if we applied the double-space-to-period transformation, false otherwise.
|
||||||
*/
|
*/
|
||||||
private boolean tryPerformDoubleSpacePeriod(final InputTransaction inputTransaction) {
|
private boolean tryPerformDoubleSpacePeriod(final Event event,
|
||||||
|
final InputTransaction inputTransaction) {
|
||||||
// Check the setting, the typed character and the countdown. If any of the conditions is
|
// Check the setting, the typed character and the countdown. If any of the conditions is
|
||||||
// not fulfilled, return false.
|
// not fulfilled, return false.
|
||||||
if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod
|
if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod
|
||||||
|| Constants.CODE_SPACE != inputTransaction.mEvent.mCodePoint
|
|| Constants.CODE_SPACE != event.mCodePoint
|
||||||
|| !isDoubleSpacePeriodCountdownActive(inputTransaction)) {
|
|| !isDoubleSpacePeriodCountdownActive(inputTransaction)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue