Skip to content

Commit

Permalink
Quotes (TGX-Android#642)
Browse files Browse the repository at this point in the history
* Quotes

* Samsung keyboard fix

* Emoji fix

* Remove background, message layout fixes

* Fix

* Fixes

* More fixes

* Input filter fix

* Update ThemeCustom.java

* Fix

* Color Updates

* TextFormattingUpdate

* Update Text.java

* Update

* Uodate

* Update MessagesController.java

* Fix

* Add Copyright and remove unused code

* Update

* Fiz

* [Experimental] NoClipEditText fix for actual Android versions.

* Update
  • Loading branch information
Arseny271 authored Jul 26, 2024
1 parent 509b7a4 commit d6adbda
Show file tree
Hide file tree
Showing 34 changed files with 2,507 additions and 372 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
import org.thunderdog.challegram.filegen.PhotoGenerationInfo;
import org.thunderdog.challegram.helper.FoundUrls;
import org.thunderdog.challegram.helper.InlineSearchContext;
import org.thunderdog.challegram.helper.editable.EditableHelper;
import org.thunderdog.challegram.loader.ComplexReceiver;
import org.thunderdog.challegram.navigation.LocaleChanger;
import org.thunderdog.challegram.navigation.RtlCheckListener;
Expand All @@ -110,6 +111,7 @@
import org.thunderdog.challegram.util.TextSelection;
import org.thunderdog.challegram.util.text.Text;
import org.thunderdog.challegram.util.text.TextColorSets;
import org.thunderdog.challegram.util.text.quotes.QuoteSpan;
import org.thunderdog.challegram.widget.InputWrapperWrapper;
import org.thunderdog.challegram.widget.NoClipEditText;

Expand Down Expand Up @@ -294,6 +296,9 @@ public boolean onCreateActionMode (ActionMode mode, Menu menu) {
} else if (itemId == R.id.btn_link) {
overrideResId = R.string.TextFormatLink;
type = null;
} else if (itemId == R.id.btn_quote) {
overrideResId = R.string.TextFormatQuote;
type = new TdApi.TextEntityTypeBlockQuote();
} else {
if (BuildConfig.DEBUG) {
Log.i("Menu item: %s %s", UI.getAppContext().getResources().getResourceName(item.getItemId()), item.getTitle());
Expand Down Expand Up @@ -399,6 +404,8 @@ public boolean setSpan (@IdRes int id) {
if (id == R.id.btn_plain) {
clearSpans(selection.start, selection.end);
return true;
} else if (id == R.id.btn_quote) {
type = new TdApi.TextEntityTypeBlockQuote();
} else if (id == R.id.btn_bold) {
type = new TdApi.TextEntityTypeBold();
} else if (id == R.id.btn_italic) {
Expand All @@ -423,6 +430,19 @@ public boolean setSpan (@IdRes int id) {
return true;
}

public boolean setSpanLink (String link) {
TextSelection selection = getTextSelection();
if (selection == null || selection.isEmpty()) {
return false;
}

URLSpan[] existingSpans = getText().getSpans(selection.start, selection.end, URLSpan.class);
URLSpan existingSpan = existingSpans != null && existingSpans.length > 0 ? existingSpans[0] : null;
createTextUrl(existingSpan, link, selection.start, selection.end);

return true;
}

public void removeSpan (TdApi.TextEntityType type) {
TextSelection selection = getTextSelection();
if (selection != null && !selection.isEmpty()) {
Expand Down Expand Up @@ -455,6 +475,7 @@ private void clearSpans (int start, int end, @Nullable TdApi.TextEntityType type
Editable editable = getText();
Object[] spans = editable.getSpans(start, end, Object.class);
boolean updated = false;
boolean updateQuotes = false;
if (spans != null) {
for (Object existingSpan : spans) {
if (existingSpan instanceof NoCopySpan || existingSpan instanceof EmojiSpan || isComposingSpan(editable, existingSpan) || !TD.canConvertToEntityType(existingSpan)) {
Expand All @@ -481,10 +502,11 @@ private void clearSpans (int start, int end, @Nullable TdApi.TextEntityType type
int existingSpanEnd = editable.getSpanEnd(existingSpan);
boolean reused = false;

editable.removeSpan(existingSpan);
EditableHelper.removeSpan(editable, existingSpan);

boolean keepSpanBeforeStart = start > existingSpanStart;
boolean keepSpanAfterEnd = existingSpanEnd > end;
final boolean isQuoteSpan = QuoteSpan.isQuoteSpan(existingSpan);
boolean keepSpanBeforeStart = !isQuoteSpan && start > existingSpanStart;
boolean keepSpanAfterEnd = !isQuoteSpan && existingSpanEnd > end;

if (keepSpanBeforeStart && keepSpanAfterEnd) {
editable.setSpan(TD.cloneSpan(existingSpan), existingSpanStart, start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Expand All @@ -501,10 +523,14 @@ private void clearSpans (int start, int end, @Nullable TdApi.TextEntityType type
((Destroyable) existingSpan).performDestroy();
}
updated = true;
updateQuotes |= isQuoteSpan;
}
}
setSelection(start, end);
if (updated) {
if (updateQuotes) {
invalidateQuotes(true);
}
inlineContext.forceCheck();
if (spanChangeListener != null) {
spanChangeListener.onSpansChanged(this);
Expand All @@ -520,6 +546,7 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) {
if (end - start <= 0 || !TD.canConvertToSpan(newType)) {
return false;
}
final boolean isQuoteSpan = newType.getConstructor() == TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR;
Object newSpan = TD.toSpan(newType);
Editable editable = getText();
Object[] existingSpansArray = editable.getSpans(start, end, Object.class);
Expand Down Expand Up @@ -556,13 +583,18 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) {
if (existingTypes.length == 1 || matchingStyleSpans) {
if (start < existingSpanStart || end > existingSpanEnd) {
// Medium path: extend existing span indexes if needed
editable.removeSpan(existingSpan);
EditableHelper.removeSpan(editable, existingSpan);
editable.setSpan(
existingSpan,
Math.min(start, existingSpanStart),
Math.max(end, existingSpanEnd),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
if (isQuoteSpan) {
QuoteSpan.normalizeQuotes(editable);
invalidateQuotes(true);
resetFontMetricsCache();
}
return true;
}
// Easy path: do nothing, because entire selection already has the same entity
Expand All @@ -587,6 +619,11 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) {
if (existingSpans == null || existingSpans.isEmpty()) {
// Easy path: just set new span at start .. end
editable.setSpan(newSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (isQuoteSpan) {
QuoteSpan.normalizeQuotes(editable);
invalidateQuotes(true);
resetFontMetricsCache();
}
return true;
}
boolean canBeNested = Td.canBeNested(newType);
Expand All @@ -610,6 +647,9 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) {
}
continue;
}
if (QuoteSpan.isQuoteSpan(existingSpan)) {
continue;
}
boolean moveExistingEntity = !canBeNested;
for (TdApi.TextEntityType existingType : existingTypes) {
if (!Td.canBeNested(existingType) || (Td.isTextUrl(existingType) && Td.isTextUrl(newType))) {
Expand Down Expand Up @@ -651,15 +691,45 @@ private boolean setSpanImpl (int start, int end, TdApi.TextEntityType newType) {
}
}
editable.setSpan(newSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (isQuoteSpan) {
QuoteSpan.normalizeQuotes(editable);
invalidateQuotes(true);
resetFontMetricsCache();
}
return true;
}

public int getLength () {
Editable text = getText();
return text != null ? text.length() : 0;
}

private void setSpan (int start, int end, TdApi.TextEntityType newType) {
if (!TD.canConvertToSpan(newType)) {
return;
}
final int oldLength = getLength();
boolean spansChanged = setSpanImpl(start, end, newType);
setSelection(start, end);
boolean lengthChanged = oldLength != getLength();

boolean selectionUpdated = false;
if (lengthChanged && newType.getConstructor() == TdApi.TextEntityTypeBlockQuote.CONSTRUCTOR) {
final Editable text = getText();
if (text != null) {
final Object[] spans = text.getSpans(start, end, QuoteSpan.class);
if (spans != null && spans.length == 1) {
int newStart = text.getSpanStart(spans[0]);
int newEnd = text.getSpanEnd(spans[0]);
setSelection(newStart, newEnd);
selectionUpdated = true;
}
}
}

if (!selectionUpdated) {
setSelection(start, end);
}

if (spansChanged) {
inlineContext.forceCheck();
if (spanChangeListener != null) {
Expand Down Expand Up @@ -696,6 +766,7 @@ public void createTextUrl (URLSpan existingSpan, int start, int end) {
}
return true;
} else if (Strings.isValidLink(result)) {
clearSpans(start, end, new TdApi.TextEntityTypeTextUrl(result)); // todo: remove after fix setSpanImpl
setSpan(start, end, new TdApi.TextEntityTypeTextUrl(result));
return true;
} else {
Expand All @@ -705,6 +776,27 @@ public void createTextUrl (URLSpan existingSpan, int start, int end) {
}
}

public void createTextUrl (URLSpan existingSpan, String result, int start, int end) {
if (start < 0 || end < 0 || start > getText().length() || end > getText().length()) {
return;
}
ViewController<?> c = controller;
if (c == null && inputListener instanceof ViewController<?>) {
c = (ViewController<?>) inputListener;
}
if (c != null) {
if (StringUtils.isEmpty(result)) {
if (existingSpan != null) {
getText().removeSpan(existingSpan);
inlineContext.forceCheck();
}
} else if (Strings.isValidLink(result)) {
clearSpans(start, end, new TdApi.TextEntityTypeTextUrl(result)); // todo: remove after fix setSpanImpl
setSpan(start, end, new TdApi.TextEntityTypeTextUrl(result));
}
}
}

public void restartTextChange () {
CharSequence cs = getText();
String str = cs.toString();
Expand Down Expand Up @@ -1268,6 +1360,25 @@ protected void onDraw (Canvas c) {
}
}

invalidateQuotes(false);
if (!quoteBlocks.isEmpty()) {
final boolean needSave = !noClippingWorks();
final int scrollY = getScrollY();
final int s;
if (needSave) {
s = Views.save(c);
c.clipRect(0, scrollY + getPaddingTop(), getMeasuredWidth(), scrollY + getMeasuredHeight() - getPaddingBottom());
} else {
s = -1;
}
for (int i = 0; i < quoteBlocks.size(); ++i) {
quoteBlocks.get(i).draw(c, getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingLeft() - getPaddingRight());
}
if (needSave) {
Views.restore(c, s);
}
}

super.onDraw(c);
drawEmojiOverlay(c);
if (this.displaySuffix.length() > 0 && this.prefix.length() > 0 && getLineCount() == 1) {
Expand Down Expand Up @@ -1578,9 +1689,23 @@ private void doBugfix () {
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
invalidateQuotes(false);
doBugfix();
}

@Override
protected void onLayout (boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
invalidateQuotes(false);
}

@Override
protected void onTextChanged (CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
invalidateQuotes(true);
invalidate();
}

@Override
public void setEnabled(boolean enabled) {
this.mEnabled = enabled;
Expand Down Expand Up @@ -1630,4 +1755,35 @@ public void getSymbolUnderCursorPosition (int[] coordinates) {
coordinates[0] = (cords1[0] + cords2[0]) / 2;
coordinates[1] = cords1[1];
}

private ArrayList<QuoteSpan.Block> quoteBlocks = new ArrayList<>();
private int lastText2Length;
private int quoteUpdatesTries;
private boolean[] quoteUpdateLayout;

public void invalidateQuotes(boolean force) {
int newTextLength = (getLayout() == null || getLayout().getText() == null) ? 0 : getLayout().getText().length();
if (force || lastText2Length != newTextLength) {
quoteUpdatesTries = 2;
lastText2Length = newTextLength;
}
if (quoteUpdatesTries > 0) {
if (quoteUpdateLayout == null) {
quoteUpdateLayout = new boolean[1];
}
quoteUpdateLayout[0] = false;
quoteBlocks = QuoteSpan.updateQuoteBlocks(getLayout(), quoteBlocks, quoteUpdateLayout);
if (quoteUpdateLayout[0]) {
resetFontMetricsCache();
}
quoteUpdatesTries--;
}
}

// really dirty workaround to reset fontmetrics cache (lineheightspan.chooseheight works only when text is inserted into the respected line)
protected void resetFontMetricsCache() {
float originalTextSize = getTextSize();
setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize + 1);
setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import android.widget.RelativeLayout;

import org.thunderdog.challegram.ui.MessagesController;
import org.thunderdog.challegram.widget.EmojiLayout;
import org.thunderdog.challegram.widget.KeyboardFrameLayout;

import me.vkryl.android.animator.Animated;

Expand All @@ -40,7 +40,7 @@ public void setController (MessagesController controller) {
@Override
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) {
boolean emojiState = controller.getEmojiState();
EmojiLayout emojiLayout = controller.getEmojiLayout();
KeyboardFrameLayout emojiLayout = controller.getEmojiKeyboardLayout();

boolean commandsState = controller.getCommandsState();
CommandKeyboardLayout keyboardLayout = controller.getKeyboardLayout();
Expand All @@ -63,7 +63,7 @@ protected void onLayout (boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if (changedMin) {
boolean emojiState = controller.getEmojiState();
EmojiLayout emojiLayout = controller.getEmojiLayout();
KeyboardFrameLayout emojiLayout = controller.getEmojiKeyboardLayout();

boolean commandsState = controller.getCommandsState();
CommandKeyboardLayout keyboardLayout = controller.getKeyboardLayout();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ public static boolean isThemeDoc (TdApi.Document doc) {

public static final boolean REQUIRE_FIREBASE_SERVICES_FOR_SAFETYNET = false;

public static final boolean USE_INPUT_VIEW_CLIPPING_FIX = false;

public static final int VOIP_CONNECTION_MIN_LAYER = 65;
public static final boolean FORCE_DIRECT_TGVOIP = false;

Expand Down
Loading

0 comments on commit d6adbda

Please sign in to comment.