[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) .setEnd(dateYearEnd.inMillisUtc)
.build() .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 package pl.szczodrzynski.edziennik.ui.modules.messages.compose
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle import android.os.Bundle
import android.text.Spannable import android.text.*
import android.text.SpannableString import android.text.Spanned.*
import android.text.SpannableStringBuilder import android.text.style.*
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.Toast
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.hootsuite.nachos.ChipConfiguration
import com.hootsuite.nachos.chip.ChipInfo 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 com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus 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.dialogs.MessagesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage 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
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.Date
import pl.szczodrzynski.edziennik.utils.models.Time 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.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.elevateSurface
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.text.replace import kotlin.text.replace
@ -76,11 +69,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
private var teachers = mutableListOf<Teacher>() 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? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true) requireContext().theme.applyStyle(Themes.appTheme, true)
// activity, context and profile is valid // activity, context and profile is valid
b = MessagesComposeFragmentBinding.inflate(inflater) b = MessagesComposeFragmentBinding.inflate(inflater)
return b.root return b.root
@ -103,40 +101,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
// do your job // do your job
} }
/*b.fontStyleBold.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_bold) if (App.devMode) {
.sizeDp(24) b.textHtml.isVisible = true
.colorAttr(activity, R.attr.colorOnPrimary) b.text.addTextChangedListener {
b.fontStyleItalic.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_italic) b.textHtml.text = getHtmlText()
.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 (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( activity.bottomSheet.prependItem(
BottomSheetPrimaryItem(true) 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() { private fun getRecipientList() {
if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) { if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) {
activity.snackbar("Pobieranie listy odbiorców...") 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() { private fun createView() {
b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
@ -203,107 +291,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
else -> -1 else -> -1
} }
b.recipients.chipTokenizer = SpanChipTokenizer<ChipSpan>(activity, object : ChipSpanChipCreator() { b.recipients.chipTokenizer = MessagesComposeChipTokenizer(activity, b.recipients, teachers)
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.setIllegalCharacterIdentifier { c -> b.recipients.setIllegalCharacterIdentifier { c ->
c.toString().matches("[\\n;:_ ]".toRegex()) c.toString().matches("[\\n;:_ ]".toRegex())
} }
@ -330,6 +318,55 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
b.subjectLayout.isEnabled = false b.subjectLayout.isEnabled = false
b.textLayout.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 { activity.navView.bottomBar.apply {
fabEnable = true fabEnable = true
fabExtendedText = getString(R.string.messages_compose_send) 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) val dateString = getString(R.string.messages_date_time_format, Date.fromMillis(msg.addedDate).formattedStringShort, Time.fromMillis(msg.addedDate).stringHM)
// add original message info // add original message info
span.appendText("W dniu ") span.appendText("W dniu ")
span.appendSpan(dateString, StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) span.appendSpan(dateString, ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE)
span.appendText(", ") 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.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") span.appendText("\n\n")
if (arguments?.getString("type") == "reply") { if (arguments?.getString("type") == "reply") {
@ -422,6 +459,8 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
b.subject.setText(subject) b.subject.setText(subject)
b.text.apply { b.text.apply {
text = span.appendText(body) text = span.appendText(body)
if (!enableTextStyling)
setText(text?.toString())
setSelection(0) setSelection(0)
} }
b.root.scrollTo(0, 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) if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength)
return return
var textHtml = if (app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) { val textHtml = getHtmlText()
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>")
}
activity.bottomSheet.hideKeyboard() activity.bottomSheet.hideKeyboard()
@ -525,7 +541,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
.setTitle(R.string.messages_compose_confirm_title) .setTitle(R.string.messages_compose_confirm_title)
.setMessage(R.string.messages_compose_confirm_text) .setMessage(R.string.messages_compose_confirm_text)
.setPositiveButton(R.string.send) { _, _ -> .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) .setNegativeButton(R.string.cancel, null)
.show() .show()

View File

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

View File

@ -6,17 +6,32 @@ package pl.szczodrzynski.edziennik.utils.html
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.Typeface
import android.text.Editable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned 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.graphics.ColorUtils
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import pl.szczodrzynski.edziennik.dp import pl.szczodrzynski.edziennik.dp
import pl.szczodrzynski.edziennik.getWordBounds
import pl.szczodrzynski.edziennik.resolveAttr import pl.szczodrzynski.edziennik.resolveAttr
import pl.szczodrzynski.edziennik.utils.span.*
import pl.szczodrzynski.navlib.blendColors import pl.szczodrzynski.navlib.blendColors
object BetterHtml { object BetterHtml {
val customSpanClasses = listOf(
BoldSpan::class.java,
ItalicSpan::class.java,
UnderlineSpan::class.java,
StrikethroughSpan::class.java,
SubscriptSizeSpan::class.java,
SuperscriptSizeSpan::class.java,
)
@JvmStatic @JvmStatic
fun fromHtml(context: Context, html: String): Spanned { fun fromHtml(context: Context, html: String): Spanned {
val hexPattern = "(#[a-fA-F0-9]{6})" val hexPattern = "(#[a-fA-F0-9]{6})"
@ -39,7 +54,11 @@ object BetterHtml {
var blendAmount = 1 var blendAmount = 1
var numIterations = 0 var numIterations = 0
while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) { while (numIterations < 100 && ColorUtils.calculateContrast(
colorBackground,
newColor
) < 4.5f
) {
blendAmount += 2 blendAmount += 2
newColor = blendColors(color, blendAmount shl 24 or textColorPrimary) newColor = blendColors(color, blendAmount shl 24 or textColorPrimary)
numIterations++ numIterations++
@ -65,20 +84,103 @@ object BetterHtml {
LiTagHandler() LiTagHandler()
) )
val spannableBuilder = SpannableStringBuilder(htmlSpannable) val spanned = SpannableStringBuilder(htmlSpannable)
val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java) spanned.getSpans(0, spanned.length, Any::class.java).forEach {
bulletSpans.forEach { val spanStart = spanned.getSpanStart(it)
val start = spannableBuilder.getSpanStart(it) val spanEnd = spanned.getSpanEnd(it)
val end = spannableBuilder.getSpanEnd(it) val spanFlags = spanned.getSpanFlags(it)
spannableBuilder.removeSpan(it)
spannableBuilder.setSpan( val newSpan: Any? = when (it) {
ImprovedBulletSpan(bulletRadius = 3.dp, startWidth = 24.dp, gapWidth = 8.dp), is BulletSpan -> ImprovedBulletSpan(
start, bulletRadius = 3.dp,
end, startWidth = 24.dp,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE 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
} }
return spannableBuilder if (newSpan != null) {
spanned.removeSpan(it)
spanned.setSpan(newSpan, spanStart, spanEnd, spanFlags)
}
}
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 * 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.Canvas
import android.graphics.Paint import android.graphics.Paint

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" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial" />
</data>
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="40dp" android:orientation="vertical"
android:orientation="vertical"><!-- half of the FAB's size --> android:paddingBottom="40dp"><!-- half of the FAB's size -->
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/recipientsLayout" android:id="@+id/recipientsLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
@ -22,8 +28,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:boxBackgroundColor="@android:color/transparent" app:boxBackgroundColor="@android:color/transparent"
app:boxBackgroundMode="filled" app:boxBackgroundMode="filled"
app:endIconMode="custom" app:endIconDrawable="@drawable/dropdown_arrow"
app:endIconDrawable="@drawable/dropdown_arrow"> app:endIconMode="custom">
<com.hootsuite.nachos.NachoTextView <com.hootsuite.nachos.NachoTextView
android:id="@+id/recipients" android:id="@+id/recipients"
@ -34,7 +40,6 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:focusedByDefault="true" android:focusedByDefault="true"
android:hint="@string/messages_compose_to_hint" /> android:hint="@string/messages_compose_to_hint" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@ -55,10 +60,9 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:hint="@string/messages_compose_subject_hint" android:hint="@string/messages_compose_subject_hint"
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"
android:singleLine="true" android:singleLine="true"
tools:text="kachoomba" tools:text="kachoomba" />
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"/>
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
@ -69,66 +73,131 @@
app:boxBackgroundMode="filled" app:boxBackgroundMode="filled"
app:counterEnabled="true" app:counterEnabled="true"
tools:counterMaxLength="1983"> tools:counterMaxLength="1983">
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit <pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
android:id="@+id/text" android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ems="10"
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:minLines="3"
android:hint="@string/messages_compose_text_hint"
android:ems="10"
android:gravity="start|top" 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:hint="@string/messages_compose_text_hint"
android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences" /> 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.textfield.TextInputLayout>
<!--<com.google.android.material.button.MaterialButtonToggleGroup <LinearLayout
android:id="@+id/fontStyleLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:orientation="vertical">
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/fontStyle" android:id="@+id/fontStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp" android:layout_marginHorizontal="8dp">
android:layout_marginTop="4dp">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleBold" android:id="@+id/fontStyleBold"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp" android:minWidth="48dp"
tools:icon="@sample/format-bold" android:textSize="20sp"
app:iconPadding="0dp"/> app:iconPadding="0dp"
tools:text="\uf674" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleItalic" android:id="@+id/fontStyleItalic"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp" android:minWidth="48dp"
tools:icon="@sample/format-bold" android:textSize="20sp"
app:iconPadding="0dp"/> app:iconPadding="0dp"
tools:text="\uf691" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/fontStyleUnderline" android:id="@+id/fontStyleUnderline"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v5_8_55"
android:minWidth="48dp" android:minWidth="48dp"
tools:icon="@sample/format-bold" android:textSize="20sp"
app:iconPadding="0dp"/> app:iconPadding="0dp"
tools:text="\uf6c5" />
</com.google.android.material.button.MaterialButtonToggleGroup>--> <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/fontStyleClear"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:text="@string/messages_compose_style_clear" />
</LinearLayout>
<Button <Button
android:id="@+id/breakpoints" android:id="@+id/breakpoints"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_marginHorizontal="8dp" android:layout_marginHorizontal="8dp"
android:text="Breakpoints!" 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> </LinearLayout>
</ScrollView> </ScrollView>
</layout> </layout>

View File

@ -1464,4 +1464,11 @@
<string name="contributors_subtext_format" translatable="false">\@%s - %s</string> <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="contributors_headline">Najłatwiejszy sposób na korzystanie z e-dziennika.</string>
<string name="dialog_lesson_attendance_details">Szczegóły</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> </resources>