forked from github/szkolny
[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:
parent
d59286bb05
commit
692555732d
@ -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
|
||||||
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
)
|
@ -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> </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()
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
@ -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
|
@ -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)
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"To jest tak, ale nie. Pamiętaj żeby oczywiście"\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"To jest tak, ale nie. Pamiętaj żeby oczywiście"\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="<p>Witam,</p>To jest bardzo długi tekst żeby sprawdzić ok działa"
|
||||||
|
tools:visibility="visible" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -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>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user