[Messages/Compose] Add text styling support. (#85)

* [UI/Messages] Add draft text styling support.

* [UI/Messages] Improve mid-word span styling. Restore subscript and superscript styles.

* [UI/Messages] Replace framework spans with custom classes on replying.

* [Messages/Compose] Move UI-related code to separate classes.

* [UI/Messages] Disable text style buttons when not in focus.

* [Messages/Compose] Disable text styling on Vulcan.

* [UI/Messages] Add hint toasts to text style toggles.

* [UI/Messages] Add button to clear text styling.

* [Messages/Compose] Fix XML formatting.
This commit is contained in:
Kuba Szczodrzyński 2021-10-05 20:28:29 +02:00 committed by GitHub
parent d59286bb05
commit 692555732d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 744 additions and 254 deletions

View File

@ -1308,3 +1308,41 @@ fun Profile.getSchoolYearConstrains(): CalendarConstraints {
.setEnd(dateYearEnd.inMillisUtc)
.build()
}
fun CharSequence.getWordBounds(position: Int, onlyInWord: Boolean = false): Pair<Int, Int>? {
if (length == 0)
return null
// only if cursor between letters
if (onlyInWord) {
if (position < 1)
return null
if (position == length)
return null
val charBefore = this[position - 1]
if (!charBefore.isLetterOrDigit())
return null
val charAfter = this[position]
if (!charAfter.isLetterOrDigit())
return null
}
var rangeStart = substring(0 until position).indexOfLast { !it.isLetterOrDigit() }
if (rangeStart == -1) // no whitespace, set to first index
rangeStart = 0
else // cut the leading whitespace
rangeStart += 1
var rangeEnd = substring(position).indexOfFirst { !it.isLetterOrDigit() }
if (rangeEnd == -1) // no whitespace, set to last index
rangeEnd = length
else // append the substring offset
rangeEnd += position
if (!onlyInWord && rangeStart == rangeEnd)
return null
return rangeStart to rangeEnd
}
infix fun Int.hasSet(what: Int) = this and what == what

View File

@ -0,0 +1,141 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-4.
*/
package pl.szczodrzynski.edziennik.ui.modules.messages.compose
import android.content.Context
import android.graphics.drawable.BitmapDrawable
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.widget.Toast
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hootsuite.nachos.ChipConfiguration
import com.hootsuite.nachos.NachoTextView
import com.hootsuite.nachos.chip.ChipInfo
import com.hootsuite.nachos.chip.ChipSpan
import com.hootsuite.nachos.chip.ChipSpanChipCreator
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.navlib.elevateSurface
class MessagesComposeChipCreator(
private val context: Context,
private val nacho: NachoTextView,
private val teacherList: List<Teacher>,
) : ChipSpanChipCreator() {
override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? {
if (data == null || data !is Teacher)
return null
if (data.id !in -24L..0L) {
nacho.allChips.forEach {
if (it.data == data) {
Toast.makeText(
context,
R.string.messages_compose_recipient_exists,
Toast.LENGTH_SHORT
).show()
return null
}
}
val chipSpan = ChipSpan(
context,
data.fullName,
BitmapDrawable(context.resources, data.image),
data
)
chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(data.fullName))
return chipSpan
}
val type = (data.id * -1).toInt()
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context)
val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(context)
val sortByCategory = type in listOf(
Teacher.TYPE_PARENTS_COUNCIL,
Teacher.TYPE_EDUCATOR,
Teacher.TYPE_STUDENT
)
val teachers = if (sortByCategory)
teacherList.sortedBy { it.typeDescription }
else
teacherList
val category = mutableListOf<Teacher>()
val categoryNames = mutableListOf<CharSequence>()
val categoryCheckedItems = mutableListOf<Boolean>()
teachers.forEach { teacher ->
if (!teacher.isType(type))
return@forEach
category += teacher
val name = teacher.fullName
val description = when (type) {
Teacher.TYPE_TEACHER -> null
Teacher.TYPE_PARENTS_COUNCIL -> teacher.typeDescription
Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null
Teacher.TYPE_PEDAGOGUE -> null
Teacher.TYPE_LIBRARIAN -> null
Teacher.TYPE_SCHOOL_ADMIN -> null
Teacher.TYPE_SUPER_ADMIN -> null
Teacher.TYPE_SECRETARIAT -> null
Teacher.TYPE_PRINCIPAL -> null
Teacher.TYPE_EDUCATOR -> teacher.typeDescription
Teacher.TYPE_PARENT -> teacher.typeDescription
Teacher.TYPE_STUDENT -> teacher.typeDescription
Teacher.TYPE_SPECIALIST -> null
else -> teacher.typeDescription
}
categoryNames += listOfNotNull(
name.asSpannable(
ForegroundColorSpan(textColorPrimary)
),
description?.asSpannable(
ForegroundColorSpan(textColorSecondary),
AbsoluteSizeSpan(14.dp)
)
).concat("\n")
// check the teacher if already added as a recipient
categoryCheckedItems += nacho.allChips.firstOrNull { it.data == teacher } != null
}
MaterialAlertDialogBuilder(context)
.setTitle("Dodaj odbiorców - " + Teacher.typeName(context, type))
//.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type)))
.setPositiveButton("OK", null)
.setNeutralButton("Anuluj", null)
.setMultiChoiceItems(
categoryNames.toTypedArray(),
categoryCheckedItems.toBooleanArray()
) { _, which, isChecked ->
val teacher = category[which]
if (isChecked) {
val chipInfoList = mutableListOf<ChipInfo>()
teacher.image =
MessagesUtils.getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
chipInfoList.add(ChipInfo(teacher.fullName, teacher))
nacho.addTextWithChips(chipInfoList)
} else {
nacho.allChips.forEach {
if (it.data == teacher)
nacho.chipTokenizer?.deleteChipAndPadding(it, nacho.text)
}
}
}
.show()
return null
}
override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) {
super.configureChip(chip, chipConfiguration)
chip.setBackgroundColor(elevateSurface(context, 8).toColorStateList())
chip.setTextColor(Themes.getPrimaryTextColor(context))
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-4.
*/
package pl.szczodrzynski.edziennik.ui.modules.messages.compose
import android.content.Context
import com.hootsuite.nachos.NachoTextView
import com.hootsuite.nachos.chip.ChipSpan
import com.hootsuite.nachos.tokenizer.SpanChipTokenizer
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
class MessagesComposeChipTokenizer(
context: Context,
nacho: NachoTextView,
teacherList: List<Teacher>,
) : SpanChipTokenizer<ChipSpan>(
context,
MessagesComposeChipCreator(
context = context,
nacho = nacho,
teacherList = teacherList
),
ChipSpan::class.java
)

View File

@ -5,30 +5,21 @@
package pl.szczodrzynski.edziennik.ui.modules.messages.compose
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.text.*
import android.text.Spanned.*
import android.text.style.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AutoCompleteTextView
import android.widget.Toast
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hootsuite.nachos.ChipConfiguration
import com.hootsuite.nachos.chip.ChipInfo
import com.hootsuite.nachos.chip.ChipSpan
import com.hootsuite.nachos.chip.ChipSpanChipCreator
import com.hootsuite.nachos.tokenizer.SpanChipTokenizer
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
@ -47,13 +38,15 @@ import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.span.BoldSpan
import pl.szczodrzynski.edziennik.utils.span.ItalicSpan
import pl.szczodrzynski.edziennik.utils.span.SubscriptSizeSpan
import pl.szczodrzynski.edziennik.utils.span.SuperscriptSizeSpan
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.elevateSurface
import kotlin.coroutines.CoroutineContext
import kotlin.text.replace
@ -76,11 +69,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
private var teachers = mutableListOf<Teacher>()
private var watchFormatChecked = true
private var watchSelectionChanged = true
private val enableTextStyling
get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
requireContext().theme.applyStyle(Themes.appTheme, true)
// activity, context and profile is valid
b = MessagesComposeFragmentBinding.inflate(inflater)
return b.root
@ -103,40 +101,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
// do your job
}
/*b.fontStyleBold.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_bold)
.sizeDp(24)
.colorAttr(activity, R.attr.colorOnPrimary)
b.fontStyleItalic.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_italic)
.sizeDp(24)
.colorAttr(activity, R.attr.colorOnPrimary)
b.fontStyleUnderline.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_underline)
.sizeDp(24)
.colorAttr(activity, R.attr.colorOnPrimary)
b.fontStyle.addOnButtonCheckedListener { _, checkedId, isChecked ->
val span: Any = when (checkedId) {
R.id.fontStyleBold -> StyleSpan(Typeface.BOLD)
R.id.fontStyleItalic -> StyleSpan(Typeface.ITALIC)
R.id.fontStyleUnderline -> UnderlineSpan()
else -> StyleSpan(Typeface.NORMAL)
if (App.devMode) {
b.textHtml.isVisible = true
b.text.addTextChangedListener {
b.textHtml.text = getHtmlText()
}
if (isChecked) {
val flags = if (b.text.selectionStart == b.text.selectionEnd)
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
else
SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
b.text.text?.setSpan(span, b.text.selectionStart, b.text.selectionEnd, flags)
}
else {
b.text.text?.getSpans(b.text.selectionStart, b.text.selectionEnd, span.javaClass)?.forEach {
if (it is StyleSpan && span is StyleSpan && it.style == span.style)
b.text.text?.removeSpan(it)
else if (it.javaClass == span.javaClass)
b.text.text?.removeSpan(it)
}
}
}*/
}
activity.bottomSheet.prependItem(
BottomSheetPrimaryItem(true)
@ -156,6 +126,50 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
}
}
private fun getHtmlText(): String {
val text = b.text.text ?: return ""
// apparently setting the spans to a different Spannable calls the original EditText's
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
watchSelectionChanged = false
var textHtml = if (enableTextStyling) {
val spanned = SpannableString(text)
// remove zero-length spans, as they seem to affect
// the whole line when converted to HTML
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
spanned.removeSpan(it)
}
HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
.replace("</b><b>", "")
.replace("</i><i>", "")
.replace("</u><u>", "")
.replace("</sub><sub>", "")
.replace("</sup><sup>", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
} else {
text.toString()
}
watchSelectionChanged = true
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
textHtml = textHtml
.replace("<br>", "<p>&nbsp;</p>")
.replace("<b>", "<strong>")
.replace("</b>", "</strong>")
.replace("<i>", "<em>")
.replace("</i>", "</em>")
.replace("<u>", "<span style=\"text-decoration: underline;\">")
.replace("</u>", "</span>")
}
return textHtml
}
private fun getRecipientList() {
if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) {
activity.snackbar("Pobieranie listy odbiorców...")
@ -171,6 +185,80 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
}
}
@Suppress("UNUSED_PARAMETER")
private fun onFormatChecked(
group: MaterialButtonToggleGroup,
checkedId: Int,
isChecked: Boolean
) {
if (!watchFormatChecked)
return
val span = when (checkedId) {
R.id.fontStyleBold -> BoldSpan()
R.id.fontStyleItalic -> ItalicSpan()
R.id.fontStyleUnderline -> UnderlineSpan()
R.id.fontStyleStrike -> StrikethroughSpan()
R.id.fontStyleSubscript -> SubscriptSizeSpan(10, dip = true)
R.id.fontStyleSuperscript -> SuperscriptSizeSpan(10, dip = true)
else -> return
}
// see comments in getHtmlText()
watchSelectionChanged = false
if (isChecked)
BetterHtml.applyFormat(span = span, editText = b.text)
else
BetterHtml.removeFormat(span = span, editText = b.text)
watchSelectionChanged = true
if (App.devMode)
b.textHtml.text = getHtmlText()
}
@Suppress("UNUSED_PARAMETER")
private fun onFormatClear(view: View) {
// shortened version on onFormatChecked(), removing all spans
watchSelectionChanged = false
BetterHtml.removeFormat(span = null, editText = b.text)
watchSelectionChanged = true
if (App.devMode)
b.textHtml.text = getHtmlText()
// force update of text style toggle states
onSelectionChanged(b.text.selectionStart, b.text.selectionEnd)
}
private fun onSelectionChanged(selectionStart: Int, selectionEnd: Int) {
if (!watchSelectionChanged)
return
val spanned = b.text.text ?: return
val spans = spanned.getSpans(selectionStart, selectionEnd, Any::class.java).mapNotNull {
if (it::class.java !in BetterHtml.customSpanClasses)
return@mapNotNull null
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
// remove 0-length spans after navigating out of them
if (spanStart == spanEnd)
spanned.removeSpan(it)
else if (spanned.getSpanFlags(it) hasSet SPAN_EXCLUSIVE_EXCLUSIVE)
spanned.setSpan(it, spanStart, spanEnd, SPAN_EXCLUSIVE_INCLUSIVE)
// names are helpful here
val isNotAfterWord = selectionEnd <= spanEnd
val isSelectionInWord = selectionStart != selectionEnd && selectionStart >= spanStart
val isCursorInWord = selectionStart == selectionEnd && selectionStart > spanStart
val isChecked = (isCursorInWord || isSelectionInWord) && isNotAfterWord
if (isChecked) it::class.java else null
}
watchFormatChecked = false
b.fontStyleBold.isChecked = BoldSpan::class.java in spans
b.fontStyleItalic.isChecked = ItalicSpan::class.java in spans
b.fontStyleUnderline.isChecked = UnderlineSpan::class.java in spans
b.fontStyleStrike.isChecked = StrikethroughSpan::class.java in spans
b.fontStyleSubscript.isChecked = SubscriptSizeSpan::class.java in spans
b.fontStyleSuperscript.isChecked = SuperscriptSizeSpan::class.java in spans
watchFormatChecked = true
}
private fun createView() {
b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
@ -203,107 +291,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
else -> -1
}
b.recipients.chipTokenizer = SpanChipTokenizer<ChipSpan>(activity, object : ChipSpanChipCreator() {
override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? {
if (data == null || data !is Teacher)
return null
if (data.id !in -24L..0L) {
b.recipients.allChips.forEach {
if (it.data == data) {
Toast.makeText(activity, R.string.messages_compose_recipient_exists, Toast.LENGTH_SHORT).show()
return null
}
}
val chipSpan = ChipSpan(context, data.fullName, BitmapDrawable(context.resources, data.image), data)
chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(data.fullName))
return chipSpan
}
val type = (data.id * -1).toInt()
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(activity)
val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
val sortByCategory = type in listOf(
Teacher.TYPE_PARENTS_COUNCIL,
Teacher.TYPE_EDUCATOR,
Teacher.TYPE_STUDENT
)
val teachers = if (sortByCategory)
teachers.sortedBy { it.typeDescription }
else
teachers
val category = mutableListOf<Teacher>()
val categoryNames = mutableListOf<CharSequence>()
val categoryCheckedItems = mutableListOf<Boolean>()
teachers.forEach { teacher ->
if (!teacher.isType(type))
return@forEach
category += teacher
val name = teacher.fullName
val description = when (type) {
Teacher.TYPE_TEACHER -> null
Teacher.TYPE_PARENTS_COUNCIL -> teacher.typeDescription
Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null
Teacher.TYPE_PEDAGOGUE -> null
Teacher.TYPE_LIBRARIAN -> null
Teacher.TYPE_SCHOOL_ADMIN -> null
Teacher.TYPE_SUPER_ADMIN -> null
Teacher.TYPE_SECRETARIAT -> null
Teacher.TYPE_PRINCIPAL -> null
Teacher.TYPE_EDUCATOR -> teacher.typeDescription
Teacher.TYPE_PARENT -> teacher.typeDescription
Teacher.TYPE_STUDENT -> teacher.typeDescription
Teacher.TYPE_SPECIALIST -> null
else -> teacher.typeDescription
}
categoryNames += listOfNotNull(
name.asSpannable(
ForegroundColorSpan(textColorPrimary)
),
description?.asSpannable(
ForegroundColorSpan(textColorSecondary),
AbsoluteSizeSpan(14.dp)
)
).concat("\n")
// check the teacher if already added as a recipient
categoryCheckedItems += b.recipients.allChips.firstOrNull { it.data == teacher } != null
}
MaterialAlertDialogBuilder(activity)
.setTitle("Dodaj odbiorców - "+ Teacher.typeName(activity, type))
//.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type)))
.setPositiveButton("OK", null)
.setNeutralButton("Anuluj", null)
.setMultiChoiceItems(categoryNames.toTypedArray(), categoryCheckedItems.toBooleanArray()) { _, which, isChecked ->
val teacher = category[which]
if (isChecked) {
val chipInfoList = mutableListOf<ChipInfo>()
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
chipInfoList.add(ChipInfo(teacher.fullName, teacher))
b.recipients.addTextWithChips(chipInfoList)
}
else {
b.recipients.allChips.forEach {
if (it.data == teacher)
b.recipients.chipTokenizer?.deleteChipAndPadding(it, b.recipients.text)
}
}
}
.show()
return null
}
override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) {
super.configureChip(chip, chipConfiguration)
chip.setBackgroundColor(elevateSurface(activity, 8).toColorStateList())
chip.setTextColor(getPrimaryTextColor(activity))
}
}, ChipSpan::class.java)
b.recipients.chipTokenizer = MessagesComposeChipTokenizer(activity, b.recipients, teachers)
b.recipients.setIllegalCharacterIdentifier { c ->
c.toString().matches("[\\n;:_ ]".toRegex())
}
@ -330,6 +318,55 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
b.subjectLayout.isEnabled = false
b.textLayout.isEnabled = false
b.fontStyleLayout.isVisible = enableTextStyling
b.fontStyleBold.isEnabled = false
b.fontStyleItalic.isEnabled = false
b.fontStyleUnderline.isEnabled = false
b.fontStyleStrike.isEnabled = false
b.fontStyleSubscript.isEnabled = false
b.fontStyleSuperscript.isEnabled = false
b.fontStyleClear.isEnabled = false
b.text.setOnFocusChangeListener { _, hasFocus ->
b.fontStyleBold.isEnabled = hasFocus
b.fontStyleItalic.isEnabled = hasFocus
b.fontStyleUnderline.isEnabled = hasFocus
b.fontStyleStrike.isEnabled = hasFocus
b.fontStyleSubscript.isEnabled = hasFocus
b.fontStyleSuperscript.isEnabled = hasFocus
b.fontStyleClear.isEnabled = hasFocus
}
b.fontStyleBold.text = CommunityMaterial.Icon2.cmd_format_bold.character.toString()
b.fontStyleItalic.text = CommunityMaterial.Icon2.cmd_format_italic.character.toString()
b.fontStyleUnderline.text = CommunityMaterial.Icon2.cmd_format_underline.character.toString()
b.fontStyleStrike.text = CommunityMaterial.Icon2.cmd_format_strikethrough.character.toString()
b.fontStyleSubscript.text = CommunityMaterial.Icon2.cmd_format_subscript.character.toString()
b.fontStyleSuperscript.text = CommunityMaterial.Icon2.cmd_format_superscript.character.toString()
b.fontStyleBold.attachToastHint(R.string.hint_style_bold)
b.fontStyleItalic.attachToastHint(R.string.hint_style_italic)
b.fontStyleUnderline.attachToastHint(R.string.hint_style_underline)
b.fontStyleStrike.attachToastHint(R.string.hint_style_strike)
b.fontStyleSubscript.attachToastHint(R.string.hint_style_subscript)
b.fontStyleSuperscript.attachToastHint(R.string.hint_style_superscript)
/*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
.toBuilder()
.setBottomLeftCornerSize(0f)
.build()
b.fontStyleSuperscript.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
.toBuilder()
.setBottomRightCornerSize(0f)
.build()
b.fontStyleClear.shapeAppearanceModel = b.fontStyleClear.shapeAppearanceModel
.toBuilder()
.setTopLeftCornerSize(0f)
.setTopRightCornerSize(0f)
.build()*/
b.fontStyle.addOnButtonCheckedListener(this::onFormatChecked)
b.fontStyleClear.setOnClickListener(this::onFormatClear)
b.text.setSelectionChangedListener(this::onSelectionChanged)
activity.navView.bottomBar.apply {
fabEnable = true
fabExtendedText = getString(R.string.messages_compose_send)
@ -382,11 +419,11 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
val dateString = getString(R.string.messages_date_time_format, Date.fromMillis(msg.addedDate).formattedStringShort, Time.fromMillis(msg.addedDate).stringHM)
// add original message info
span.appendText("W dniu ")
span.appendSpan(dateString, StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendSpan(dateString, ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendText(", ")
span.appendSpan(msg.senderName.fixName(), StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendSpan(msg.senderName.fixName(), ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendText(" napisał(a):")
span.setSpan(StyleSpan(Typeface.BOLD), 0, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
span.setSpan(BoldSpan(), 0, span.length, SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendText("\n\n")
if (arguments?.getString("type") == "reply") {
@ -422,6 +459,8 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
b.subject.setText(subject)
b.text.apply {
text = span.appendText(body)
if (!enableTextStyling)
setText(text?.toString())
setSelection(0)
}
b.root.scrollTo(0, 0)
@ -494,30 +533,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength)
return
var textHtml = if (app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) {
HtmlCompat.toHtml(SpannableString(text), HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
}
else {
text.toString()
}
textHtml = textHtml
.replace("</b><b>", "")
.replace("</i><i>", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
textHtml = textHtml
.replace("</p><br>", "</p>")
.replace("<b>", "<strong>")
.replace("</b>", "</strong>")
.replace("<i>", "<em>")
.replace("</i>", "</em>")
.replace("<u>", "<span style=\"text-decoration: underline;\">")
.replace("</u>", "</span>")
}
val textHtml = getHtmlText()
activity.bottomSheet.hideKeyboard()
@ -525,7 +541,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
.setTitle(R.string.messages_compose_confirm_title)
.setMessage(R.string.messages_compose_confirm_text)
.setPositiveButton(R.string.send) { _, _ ->
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml.trim()).enqueue(activity)
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml).enqueue(activity)
}
.setNegativeButton(R.string.cancel, null)
.show()

View File

@ -18,6 +18,7 @@ class TextInputKeyboardEdit : AppCompatEditText {
* Keyboard Listener
*/
internal var listener: KeyboardListener? = null
private var selectionListener: ((Int, Int) -> Unit)? = null
constructor(context: Context) : super(context)
@ -27,14 +28,12 @@ class TextInputKeyboardEdit : AppCompatEditText {
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
super.onFocusChanged(focused, direction, previouslyFocusedRect)
if (listener != null)
listener!!.onStateChanged(this, true)
listener?.onStateChanged(this, true)
}
override fun onKeyPreIme(keyCode: Int, @NonNull event: KeyEvent): Boolean {
if (event.keyCode == KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
if (listener != null)
listener!!.onStateChanged(this, false)
listener?.onStateChanged(this, false)
// Hide cursor
isFocusable = false
@ -50,6 +49,15 @@ class TextInputKeyboardEdit : AppCompatEditText {
this.listener = listener
}
fun setSelectionChangedListener(listener: ((selectionStart: Int, selectionEnd: Int) -> Unit)?) {
this.selectionListener = listener
}
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
super.onSelectionChanged(selStart, selEnd)
selectionListener?.invoke(selStart, selEnd)
}
interface KeyboardListener {
fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean)
}

View File

@ -6,26 +6,41 @@ package pl.szczodrzynski.edziennik.utils.html
import android.content.Context
import android.graphics.Color
import android.graphics.Typeface
import android.text.Editable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.BulletSpan
import android.text.Spanned.*
import android.text.style.*
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.graphics.ColorUtils
import androidx.core.text.HtmlCompat
import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.getWordBounds
import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.utils.span.*
import pl.szczodrzynski.navlib.blendColors
object BetterHtml {
val customSpanClasses = listOf(
BoldSpan::class.java,
ItalicSpan::class.java,
UnderlineSpan::class.java,
StrikethroughSpan::class.java,
SubscriptSizeSpan::class.java,
SuperscriptSizeSpan::class.java,
)
@JvmStatic
fun fromHtml(context: Context, html: String): Spanned {
val hexPattern = "(#[a-fA-F0-9]{6})"
val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})"
.toRegex(RegexOption.IGNORE_CASE)
.toRegex(RegexOption.IGNORE_CASE)
var text = html
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
.replace("background-color: ?$hexPattern;".toRegex(), "")
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
.replace("background-color: ?$hexPattern;".toRegex(), "")
val colorBackground = android.R.attr.colorBackground.resolveAttr(context)
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff
@ -39,7 +54,11 @@ object BetterHtml {
var blendAmount = 1
var numIterations = 0
while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) {
while (numIterations < 100 && ColorUtils.calculateContrast(
colorBackground,
newColor
) < 4.5f
) {
blendAmount += 2
newColor = blendColors(color, blendAmount shl 24 or textColorPrimary)
numIterations++
@ -59,26 +78,109 @@ object BetterHtml {
@Suppress("DEPRECATION")
val htmlSpannable = HtmlCompat.fromHtml(
text,
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV,
null,
LiTagHandler()
text,
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV,
null,
LiTagHandler()
)
val spannableBuilder = SpannableStringBuilder(htmlSpannable)
val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java)
bulletSpans.forEach {
val start = spannableBuilder.getSpanStart(it)
val end = spannableBuilder.getSpanEnd(it)
spannableBuilder.removeSpan(it)
spannableBuilder.setSpan(
ImprovedBulletSpan(bulletRadius = 3.dp, startWidth = 24.dp, gapWidth = 8.dp),
start,
end,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
val spanned = SpannableStringBuilder(htmlSpannable)
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
val spanFlags = spanned.getSpanFlags(it)
val newSpan: Any? = when (it) {
is BulletSpan -> ImprovedBulletSpan(
bulletRadius = 3.dp,
startWidth = 24.dp,
gapWidth = 8.dp
)
is StyleSpan -> when (it.style) {
Typeface.BOLD -> BoldSpan()
Typeface.ITALIC -> ItalicSpan()
else -> null
}
is SubscriptSpan -> SubscriptSizeSpan(size = 10, dip = true)
is SuperscriptSpan -> SuperscriptSizeSpan(size = 10, dip = true)
else -> null
}
if (newSpan != null) {
spanned.removeSpan(it)
spanned.setSpan(newSpan, spanStart, spanEnd, spanFlags)
}
}
return spannableBuilder
return spanned
}
fun applyFormat(span: Any, editText: AppCompatEditText) {
applyFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd)
}
fun removeFormat(span: Any?, editText: AppCompatEditText) {
removeFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd)
}
fun applyFormat(span: Any, spanned: Editable, selectionStart: Int, selectionEnd: Int) {
if (selectionStart == -1 || selectionEnd == -1) return
val cursorOnly = selectionStart == selectionEnd
val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true)
if (cursorOnly && wordBounds != null) {
// use the detected word bounds instead of cursor/selection
val (start, end) = wordBounds
spanned.setSpan(span, start, end, SPAN_EXCLUSIVE_INCLUSIVE)
} else {
val spanFlags = if (cursorOnly)
SPAN_INCLUSIVE_INCLUSIVE
else
SPAN_EXCLUSIVE_INCLUSIVE
spanned.setSpan(span, selectionStart, selectionEnd, spanFlags)
}
}
fun removeFormat(span: Any?, spanned: Editable, selectionStart: Int, selectionEnd: Int) {
if (selectionStart == -1 || selectionEnd == -1) return
val cursorOnly = selectionStart == selectionEnd
val spanClass = span?.javaClass ?: Any::class.java
spanned.getSpans(selectionStart, selectionEnd, spanClass).forEach {
if (span == null && it::class.java !in customSpanClasses)
return@forEach
val spanStart = spanned.getSpanStart(it)
val spanEnd = spanned.getSpanEnd(it)
val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true)
val (newSpanStart, newSpanEnd, newSpanFlags) = when {
!cursorOnly -> {
// cut the selected range out of the span
Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_INCLUSIVE)
}
wordBounds == null -> {
// this allows to change spans mid-word - EXCLUSIVE so the style does
// not apply to characters typed later
// it's set back to INCLUSIVE when the cursor enters the word again
// (onSelectionChanged)
Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_EXCLUSIVE)
}
else /* wordBounds != null */ -> {
// a word is selected, slice the span in two
Triple(wordBounds.first, wordBounds.second, SPAN_EXCLUSIVE_INCLUSIVE)
}
}
// remove the existing span
spanned.removeSpan(it)
// "clone" the span so it can be applied twice, if needed
// (can't use 'span' from the parameters as it's nullable)
val itClone = it::class.java.newInstance()
// reapply the span wherever needed
if (spanStart < newSpanStart)
spanned.setSpan(it, spanStart, newSpanStart, newSpanFlags)
if (spanEnd > newSpanEnd)
spanned.setSpan(itClone, newSpanEnd, spanEnd, newSpanFlags)
}
}
}

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-3.
*/
package pl.szczodrzynski.edziennik.utils.span
import android.graphics.Typeface
import android.text.style.StyleSpan
class BoldSpan : StyleSpan(Typeface.BOLD)

View File

@ -6,7 +6,7 @@
* https://github.com/davidbilik/bullet-span-sample/blob/master/app/src/main/java/cz/davidbilik/bulletsample/ImprovedBulletSpan.kt
*/
package pl.szczodrzynski.edziennik.utils.html
package pl.szczodrzynski.edziennik.utils.span
import android.graphics.Canvas
import android.graphics.Paint
@ -20,10 +20,10 @@ import android.text.style.LeadingMarginSpan
* Copy of [android.text.style.BulletSpan] from android SDK 28 with removed internal code
*/
class ImprovedBulletSpan(
val bulletRadius: Int = STANDARD_BULLET_RADIUS,
val startWidth: Int = STANDARD_GAP_WIDTH,
val gapWidth: Int = STANDARD_GAP_WIDTH,
val color: Int = STANDARD_COLOR
val bulletRadius: Int = STANDARD_BULLET_RADIUS,
val startWidth: Int = STANDARD_GAP_WIDTH,
val gapWidth: Int = STANDARD_GAP_WIDTH,
val color: Int = STANDARD_COLOR
) : LeadingMarginSpan {
companion object {

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-3.
*/
package pl.szczodrzynski.edziennik.utils.span
import android.graphics.Typeface
import android.text.style.StyleSpan
class ItalicSpan : StyleSpan(Typeface.ITALIC)

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-3.
*/
package pl.szczodrzynski.edziennik.utils.span
import android.text.TextPaint
import android.text.style.SubscriptSpan
class SubscriptSizeSpan(
private val size: Int,
private val dip: Boolean,
) : SubscriptSpan() {
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
if (dip) {
textPaint.textSize = size * textPaint.density
} else {
textPaint.textSize = size.toFloat()
}
}
override fun updateMeasureState(textPaint: TextPaint) {
super.updateMeasureState(textPaint)
if (dip) {
textPaint.textSize = size * textPaint.density
} else {
textPaint.textSize = size.toFloat()
}
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-10-3.
*/
package pl.szczodrzynski.edziennik.utils.span
import android.text.TextPaint
import android.text.style.SuperscriptSpan
class SuperscriptSizeSpan(
private val size: Int,
private val dip: Boolean,
) : SuperscriptSpan() {
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
if (dip) {
textPaint.textSize = size * textPaint.density
} else {
textPaint.textSize = size.toFloat()
}
}
override fun updateMeasureState(textPaint: TextPaint) {
super.updateMeasureState(textPaint)
if (dip) {
textPaint.textSize = size * textPaint.density
} else {
textPaint.textSize = size.toFloat()
}
}
}

View File

@ -6,15 +6,21 @@
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="40dp"
android:orientation="vertical"><!-- half of the FAB's size -->
android:orientation="vertical"
android:paddingBottom="40dp"><!-- half of the FAB's size -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/recipientsLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
@ -22,8 +28,8 @@
android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled"
app:endIconMode="custom"
app:endIconDrawable="@drawable/dropdown_arrow">
app:endIconDrawable="@drawable/dropdown_arrow"
app:endIconMode="custom">
<com.hootsuite.nachos.NachoTextView
android:id="@+id/recipients"
@ -34,7 +40,6 @@
android:focusableInTouchMode="true"
android:focusedByDefault="true"
android:hint="@string/messages_compose_to_hint" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
@ -55,10 +60,9 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:hint="@string/messages_compose_subject_hint"
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"
android:singleLine="true"
tools:text="kachoomba"
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"/>
tools:text="kachoomba" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
@ -69,66 +73,131 @@
app:boxBackgroundMode="filled"
app:counterEnabled="true"
tools:counterMaxLength="1983">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:focusable="true"
android:focusableInTouchMode="true"
android:minLines="3"
android:hint="@string/messages_compose_text_hint"
android:ems="10"
android:gravity="start|top"
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n&quot;To jest tak, ale nie. Pamiętaj żeby oczywiście&quot;\n\nCytat ma bardzo duże przesłanie i ogromny wpływ na dzisiejszą kulturę i rozwój współczesnej cywilizacji.\n\nJako zadanie domowe, należy wypisać 5 pierwszych liczb pierwszych. Uzasadnij decyzję, odwołując się do cytatu i 3 innych przykładów z literatury lub filmu.\n\nPozdrawiam,\nJa."
android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences" />
android:hint="@string/messages_compose_text_hint"
android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences"
android:minLines="3"
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n&quot;To jest tak, ale nie. Pamiętaj żeby oczywiście&quot;\n\nCytat ma bardzo duże przesłanie i ogromny wpływ na dzisiejszą kulturę i rozwój współczesnej cywilizacji.\n\nJako zadanie domowe, należy wypisać 5 pierwszych liczb pierwszych. Uzasadnij decyzję, odwołując się do cytatu i 3 innych przykładów z literatury lub filmu.\n\nPozdrawiam,\nJa." />
</com.google.android.material.textfield.TextInputLayout>
<!--<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/fontStyle"
<LinearLayout
android:id="@+id/fontStyleLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp">
android:layout_gravity="center_horizontal"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleBold"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/fontStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
tools:icon="@sample/format-bold"
app:iconPadding="0dp"/>
android:layout_marginHorizontal="8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleBold"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf674" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleItalic"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf691" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleUnderline"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6c5" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleStrike"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleSubscript"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleSuperscript"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp"
android:textSize="20sp"
app:iconPadding="0dp"
tools:text="\uf6b3" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<!-- TODO 2021-10-05: stack the group and button on top -->
<!-- android:layout_marginTop="-13dp" -->
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleItalic"
android:id="@+id/fontStyleClear"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="48dp"
tools:icon="@sample/format-bold"
app:iconPadding="0dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleUnderline"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="48dp"
tools:icon="@sample/format-bold"
app:iconPadding="0dp"/>
</com.google.android.material.button.MaterialButtonToggleGroup>-->
android:layout_marginHorizontal="8dp"
android:text="@string/messages_compose_style_clear" />
</LinearLayout>
<Button
android:id="@+id/breakpoints"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_marginHorizontal="8dp"
android:text="Breakpoints!"
android:visibility="gone"/>
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/textHtml"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="monospace"
android:visibility="gone"
tools:text="&lt;p&gt;Witam,&lt;/p&gt;To jest bardzo długi tekst żeby sprawdzić ok działa"
tools:visibility="visible" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -1464,4 +1464,11 @@
<string name="contributors_subtext_format" translatable="false">\@%s - %s</string>
<string name="contributors_headline">Najłatwiejszy sposób na korzystanie z e-dziennika.</string>
<string name="dialog_lesson_attendance_details">Szczegóły</string>
<string name="hint_style_bold">Pogrubienie</string>
<string name="hint_style_italic">Pochylenie</string>
<string name="hint_style_underline">Podkreślenie</string>
<string name="hint_style_strike">Przekreślenie</string>
<string name="hint_style_subscript">Indeks dolny</string>
<string name="hint_style_superscript">Indeks górny</string>
<string name="messages_compose_style_clear">Wyczyść format</string>
</resources>