Transfer SmartLengthInputFilter license to Tusky (#1384)
* Transfer SmartLengthInputFilter in-header license Transfer license for file "SmartLengthInputFilter.java" from me (Diego Rossi) to Tusky and therefore also change it from the original Apache 2.0 to currently GPLv3. This was a mistake that has been sitting around for way too long. * Rewrite SmartLengthInputFilter from Java to Kotlin This has been done by hand, without the custom copy-paste tool. * Fix bad references in Java files using SmartLengthInputFilter features * Shorten code in Java classes referencing SmartLengthInputFilter instance * Refactor SmartLengthInputFilter from class to singleton Kotlin object * Move hasBadRatio to become a toplevel function * Patch up all the files affected by SmartLengthInputFilter changes * Length in SmartLengthInputFilter is const 500, simplify code accordingly * More meaningful name for toplevel function for checking trimming ability * Add missing license headermain
parent
246956bee1
commit
4ec0c182f7
@ -1,154 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright 2018 Diego Rossi (@_HellPie) |
|
||||||
* |
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||||
* you may not use this file except in compliance with the License. |
|
||||||
* You may obtain a copy of the License at |
|
||||||
* |
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
* |
|
||||||
* Unless required by applicable law or agreed to in writing, software |
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||||
* See the License for the specific language governing permissions and |
|
||||||
* limitations under the License. |
|
||||||
*/ |
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util; |
|
||||||
|
|
||||||
import android.text.InputFilter; |
|
||||||
import android.text.SpannableStringBuilder; |
|
||||||
import android.text.Spanned; |
|
||||||
|
|
||||||
/** |
|
||||||
* A customized version of {@link android.text.InputFilter.LengthFilter} which allows smarter |
|
||||||
* constraints and adds better visuals such as: |
|
||||||
* <ul> |
|
||||||
* <li>Ellipsis at the end of the constrained text to show continuation.</li> |
|
||||||
* <li>Trimming of invisible characters (new lines, spaces, etc.) from the constrained text.</li> |
|
||||||
* <li>Constraints end at the end of the last "word", before a whitespace.</li> |
|
||||||
* <li>Expansion of the limit by up to 10 characters to facilitate the previous constraint.</li> |
|
||||||
* <li>Constraints are not applied if the percentage of hidden content is too small.</li> |
|
||||||
* </ul> |
|
||||||
* |
|
||||||
* Some of these features are configurable through at instancing time. |
|
||||||
*/ |
|
||||||
public class SmartLengthInputFilter implements InputFilter { |
|
||||||
|
|
||||||
/** |
|
||||||
* Defines how many characters to extend beyond the limit to cut at the end of the word on the |
|
||||||
* boundary of it rather than cutting at the word preceding that one. |
|
||||||
*/ |
|
||||||
private static final int RUNWAY = 10; |
|
||||||
|
|
||||||
/** |
|
||||||
* Default for maximum status length on Mastodon and default collapsing length on Pleroma. |
|
||||||
*/ |
|
||||||
public static final int LENGTH_DEFAULT = 500; |
|
||||||
|
|
||||||
/** |
|
||||||
* Stores a reusable singleton instance of a {@link SmartLengthInputFilter} already configured |
|
||||||
* to the default maximum length of {@value #LENGTH_DEFAULT}. |
|
||||||
*/ |
|
||||||
public static final SmartLengthInputFilter INSTANCE = new SmartLengthInputFilter(LENGTH_DEFAULT); |
|
||||||
|
|
||||||
private final int max; |
|
||||||
private final boolean allowRunway; |
|
||||||
private final boolean skipIfBadRatio; |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a new {@link SmartLengthInputFilter} instance with a predefined maximum length and |
|
||||||
* all the smart constraint features this class supports. |
|
||||||
* |
|
||||||
* @param max The maximum length before trimming. May change based on other constraints. |
|
||||||
*/ |
|
||||||
public SmartLengthInputFilter(int max) { |
|
||||||
this(max, true, true); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Fully configures a new {@link SmartLengthInputFilter} to fine tune the state of the supported |
|
||||||
* smart constraints this class supports. |
|
||||||
* |
|
||||||
* @param max The maximum length before trimming. |
|
||||||
* @param allowRunway Whether to extend {@param max} by an extra 10 characters |
|
||||||
* and trim precisely at the end of the closest word. |
|
||||||
* @param skipIfBadRatio Whether to skip trimming entirely if the trimmed content |
|
||||||
* will be less than 25% of the shown content. |
|
||||||
*/ |
|
||||||
public SmartLengthInputFilter(int max, boolean allowRunway, boolean skipIfBadRatio) { |
|
||||||
this.max = max; |
|
||||||
this.allowRunway = allowRunway; |
|
||||||
this.skipIfBadRatio = skipIfBadRatio; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Calculates if it's worth trimming the message at a specific limit or if the content that will |
|
||||||
* be hidden will not be enough to justify the operation. |
|
||||||
* |
|
||||||
* @param message The message to trim. |
|
||||||
* @param limit The maximum length after trimming. |
|
||||||
* @return Whether the message should be trimmed or not. |
|
||||||
*/ |
|
||||||
public static boolean hasBadRatio(Spanned message, int limit) { |
|
||||||
return (double) limit / message.length() > 0.75; |
|
||||||
} |
|
||||||
|
|
||||||
/** {@inheritDoc} */ |
|
||||||
@Override |
|
||||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { |
|
||||||
// Code originally imported from InputFilter.LengthFilter but heavily customized.
|
|
||||||
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175
|
|
||||||
|
|
||||||
int sourceLength = source.length(); |
|
||||||
int keep = max - (dest.length() - (dend - dstart)); |
|
||||||
if (keep <= 0) return ""; |
|
||||||
if (keep >= end - start) return null; // keep original
|
|
||||||
|
|
||||||
keep += start; |
|
||||||
|
|
||||||
// Enable skipping trimming if the ratio is not good enough
|
|
||||||
if (skipIfBadRatio && (double)keep / sourceLength > 0.75) |
|
||||||
return null; |
|
||||||
|
|
||||||
// Enable trimming at the end of the closest word if possible
|
|
||||||
if (allowRunway && Character.isLetterOrDigit(source.charAt(keep))) { |
|
||||||
int boundary; |
|
||||||
|
|
||||||
// Android N+ offer a clone of the ICU APIs in Java for better internationalization and
|
|
||||||
// unicode support. Using the ICU version of BreakIterator grants better support for
|
|
||||||
// those without having to add the ICU4J library at a minimum Api trade-off.
|
|
||||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { |
|
||||||
android.icu.text.BreakIterator iterator = android.icu.text.BreakIterator.getWordInstance(); |
|
||||||
iterator.setText(source.toString()); |
|
||||||
boundary = iterator.following(keep); |
|
||||||
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep); |
|
||||||
} else { |
|
||||||
java.text.BreakIterator iterator = java.text.BreakIterator.getWordInstance(); |
|
||||||
iterator.setText(source.toString()); |
|
||||||
boundary = iterator.following(keep); |
|
||||||
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep); |
|
||||||
} |
|
||||||
|
|
||||||
keep = boundary; |
|
||||||
} else { |
|
||||||
|
|
||||||
// If no runway is allowed simply remove whitespaces if present
|
|
||||||
while (Character.isWhitespace(source.charAt(keep - 1))) { |
|
||||||
--keep; |
|
||||||
if (keep == start) return ""; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (Character.isHighSurrogate(source.charAt(keep - 1))) { |
|
||||||
--keep; |
|
||||||
if (keep == start) return ""; |
|
||||||
} |
|
||||||
|
|
||||||
if (source instanceof Spanned) { |
|
||||||
return new SpannableStringBuilder(source, start, keep).append("…"); |
|
||||||
} else { |
|
||||||
return source.subSequence(start, keep) + "…"; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,111 @@ |
|||||||
|
/* Copyright 2019 Tusky contributors |
||||||
|
* |
||||||
|
* This file is a part of Tusky. |
||||||
|
* |
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the |
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the |
||||||
|
* License, or (at your option) any later version. |
||||||
|
* |
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
||||||
|
* Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not, |
||||||
|
* see <http://www.gnu.org/licenses>. */ |
||||||
|
|
||||||
|
package com.keylesspalace.tusky.util |
||||||
|
|
||||||
|
import android.text.InputFilter |
||||||
|
import android.text.SpannableStringBuilder |
||||||
|
import android.text.Spanned |
||||||
|
|
||||||
|
/** |
||||||
|
* Defines how many characters to extend beyond the limit to cut at the end of the word on the |
||||||
|
* boundary of it rather than cutting at the word preceding that one. |
||||||
|
*/ |
||||||
|
private const val RUNWAY = 10 |
||||||
|
|
||||||
|
/** |
||||||
|
* Default for maximum status length on Mastodon and default collapsing length on Pleroma. |
||||||
|
*/ |
||||||
|
private const val LENGTH_DEFAULT = 500 |
||||||
|
|
||||||
|
/** |
||||||
|
* Calculates if it's worth trimming the message at a specific limit or if the content that will |
||||||
|
* be hidden will not be enough to justify the operation. |
||||||
|
* |
||||||
|
* @param message The message to trim. |
||||||
|
* @return Whether the message should be trimmed or not. |
||||||
|
*/ |
||||||
|
fun shouldTrimStatus(message: Spanned): Boolean { |
||||||
|
return LENGTH_DEFAULT / message.length > 0.75 |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* A customized version of {@link android.text.InputFilter.LengthFilter} which allows smarter |
||||||
|
* constraints and adds better visuals such as: |
||||||
|
* <ul> |
||||||
|
* <li>Ellipsis at the end of the constrained text to show continuation.</li> |
||||||
|
* <li>Trimming of invisible characters (new lines, spaces, etc.) from the constrained text.</li> |
||||||
|
* <li>Constraints end at the end of the last "word", before a whitespace.</li> |
||||||
|
* <li>Expansion of the limit by up to 10 characters to facilitate the previous constraint.</li> |
||||||
|
* <li>Constraints are not applied if the percentage of hidden content is too small.</li> |
||||||
|
* </ul> |
||||||
|
*/ |
||||||
|
object SmartLengthInputFilter : InputFilter { |
||||||
|
/** {@inheritDoc} */ |
||||||
|
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? { |
||||||
|
// Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin. |
||||||
|
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175 |
||||||
|
|
||||||
|
val sourceLength = source.length |
||||||
|
var keep = LENGTH_DEFAULT - dest.length - dend - dstart |
||||||
|
if (keep <= 0) return "" |
||||||
|
if (keep >= end - start) return null // Keep original |
||||||
|
|
||||||
|
keep += start |
||||||
|
|
||||||
|
// Skip trimming if the ratio doesn't warrant it |
||||||
|
if (keep.toDouble() / sourceLength > 0.75) return null |
||||||
|
|
||||||
|
// Enable trimming at the end of the closest word if possible |
||||||
|
if (source[keep].isLetterOrDigit()) { |
||||||
|
var boundary: Int |
||||||
|
|
||||||
|
// Android N+ offer a clone of the ICU APIs in Java for better internationalization and |
||||||
|
// unicode support. Using the ICU version of BreakIterator grants better support for |
||||||
|
// those without having to add the ICU4J library at a minimum Api trade-off. |
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { |
||||||
|
val iterator = android.icu.text.BreakIterator.getWordInstance() |
||||||
|
iterator.setText(source.toString()) |
||||||
|
boundary = iterator.following(keep) |
||||||
|
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) |
||||||
|
} else { |
||||||
|
val iterator = java.text.BreakIterator.getWordInstance() |
||||||
|
iterator.setText(source.toString()) |
||||||
|
boundary = iterator.following(keep) |
||||||
|
if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) |
||||||
|
} |
||||||
|
|
||||||
|
keep = boundary |
||||||
|
} else { |
||||||
|
|
||||||
|
// If no runway is allowed simply remove whitespaces if present |
||||||
|
while(source[keep - 1].isWhitespace()) { |
||||||
|
--keep |
||||||
|
if (keep == start) return "" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (source[keep - 1].isHighSurrogate()) { |
||||||
|
--keep |
||||||
|
if (keep == start) return "" |
||||||
|
} |
||||||
|
|
||||||
|
return if (source is Spanned) { |
||||||
|
SpannableStringBuilder(source, start, keep).append("…") |
||||||
|
} else { |
||||||
|
"${source.subSequence(start, keep)}…" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue