mirror of
https://github.com/szkolny-eu/szkolny-android.git
synced 2024-11-24 10:54:36 -06:00
[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)
|
||||
.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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.AbsoluteSizeSpan
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.*
|
||||
import android.text.Spanned.*
|
||||
import android.text.style.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AutoCompleteTextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.hootsuite.nachos.ChipConfiguration
|
||||
import com.hootsuite.nachos.chip.ChipInfo
|
||||
import com.hootsuite.nachos.chip.ChipSpan
|
||||
import com.hootsuite.nachos.chip.ChipSpanChipCreator
|
||||
import com.hootsuite.nachos.tokenizer.SpanChipTokenizer
|
||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||
import kotlinx.coroutines.*
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
@ -47,13 +38,15 @@ import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
|
||||
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
|
||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
|
||||
import pl.szczodrzynski.edziennik.utils.Colors
|
||||
import pl.szczodrzynski.edziennik.utils.Themes
|
||||
import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor
|
||||
import pl.szczodrzynski.edziennik.utils.html.BetterHtml
|
||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||
import pl.szczodrzynski.edziennik.utils.models.Time
|
||||
import pl.szczodrzynski.edziennik.utils.span.BoldSpan
|
||||
import pl.szczodrzynski.edziennik.utils.span.ItalicSpan
|
||||
import pl.szczodrzynski.edziennik.utils.span.SubscriptSizeSpan
|
||||
import pl.szczodrzynski.edziennik.utils.span.SuperscriptSizeSpan
|
||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||
import pl.szczodrzynski.navlib.elevateSurface
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.text.replace
|
||||
|
||||
@ -76,11 +69,16 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
|
||||
private var teachers = mutableListOf<Teacher>()
|
||||
|
||||
private var watchFormatChecked = true
|
||||
private var watchSelectionChanged = true
|
||||
private val enableTextStyling
|
||||
get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
activity = (getActivity() as MainActivity?) ?: return null
|
||||
context ?: return null
|
||||
app = activity.application as App
|
||||
context!!.theme.applyStyle(Themes.appTheme, true)
|
||||
requireContext().theme.applyStyle(Themes.appTheme, true)
|
||||
// activity, context and profile is valid
|
||||
b = MessagesComposeFragmentBinding.inflate(inflater)
|
||||
return b.root
|
||||
@ -103,40 +101,12 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
// do your job
|
||||
}
|
||||
|
||||
/*b.fontStyleBold.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_bold)
|
||||
.sizeDp(24)
|
||||
.colorAttr(activity, R.attr.colorOnPrimary)
|
||||
b.fontStyleItalic.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_italic)
|
||||
.sizeDp(24)
|
||||
.colorAttr(activity, R.attr.colorOnPrimary)
|
||||
b.fontStyleUnderline.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_underline)
|
||||
.sizeDp(24)
|
||||
.colorAttr(activity, R.attr.colorOnPrimary)
|
||||
|
||||
b.fontStyle.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
val span: Any = when (checkedId) {
|
||||
R.id.fontStyleBold -> StyleSpan(Typeface.BOLD)
|
||||
R.id.fontStyleItalic -> StyleSpan(Typeface.ITALIC)
|
||||
R.id.fontStyleUnderline -> UnderlineSpan()
|
||||
else -> StyleSpan(Typeface.NORMAL)
|
||||
if (App.devMode) {
|
||||
b.textHtml.isVisible = true
|
||||
b.text.addTextChangedListener {
|
||||
b.textHtml.text = getHtmlText()
|
||||
}
|
||||
|
||||
if (isChecked) {
|
||||
val flags = if (b.text.selectionStart == b.text.selectionEnd)
|
||||
SpannableString.SPAN_INCLUSIVE_INCLUSIVE
|
||||
else
|
||||
SpannableString.SPAN_EXCLUSIVE_INCLUSIVE
|
||||
b.text.text?.setSpan(span, b.text.selectionStart, b.text.selectionEnd, flags)
|
||||
}
|
||||
else {
|
||||
b.text.text?.getSpans(b.text.selectionStart, b.text.selectionEnd, span.javaClass)?.forEach {
|
||||
if (it is StyleSpan && span is StyleSpan && it.style == span.style)
|
||||
b.text.text?.removeSpan(it)
|
||||
else if (it.javaClass == span.javaClass)
|
||||
b.text.text?.removeSpan(it)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
activity.bottomSheet.prependItem(
|
||||
BottomSheetPrimaryItem(true)
|
||||
@ -156,6 +126,50 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHtmlText(): String {
|
||||
val text = b.text.text ?: return ""
|
||||
|
||||
// apparently setting the spans to a different Spannable calls the original EditText's
|
||||
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
|
||||
watchSelectionChanged = false
|
||||
var textHtml = if (enableTextStyling) {
|
||||
val spanned = SpannableString(text)
|
||||
// remove zero-length spans, as they seem to affect
|
||||
// the whole line when converted to HTML
|
||||
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
|
||||
spanned.removeSpan(it)
|
||||
}
|
||||
HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
|
||||
.replace("\n", "")
|
||||
.replace(" dir=\"ltr\"", "")
|
||||
.replace("</b><b>", "")
|
||||
.replace("</i><i>", "")
|
||||
.replace("</u><u>", "")
|
||||
.replace("</sub><sub>", "")
|
||||
.replace("</sup><sup>", "")
|
||||
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
|
||||
} else {
|
||||
text.toString()
|
||||
}
|
||||
watchSelectionChanged = true
|
||||
|
||||
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
|
||||
textHtml = textHtml
|
||||
.replace("<br>", "<p> </p>")
|
||||
.replace("<b>", "<strong>")
|
||||
.replace("</b>", "</strong>")
|
||||
.replace("<i>", "<em>")
|
||||
.replace("</i>", "</em>")
|
||||
.replace("<u>", "<span style=\"text-decoration: underline;\">")
|
||||
.replace("</u>", "</span>")
|
||||
}
|
||||
|
||||
return textHtml
|
||||
}
|
||||
|
||||
private fun getRecipientList() {
|
||||
if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) {
|
||||
activity.snackbar("Pobieranie listy odbiorców...")
|
||||
@ -171,6 +185,80 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun onFormatChecked(
|
||||
group: MaterialButtonToggleGroup,
|
||||
checkedId: Int,
|
||||
isChecked: Boolean
|
||||
) {
|
||||
if (!watchFormatChecked)
|
||||
return
|
||||
val span = when (checkedId) {
|
||||
R.id.fontStyleBold -> BoldSpan()
|
||||
R.id.fontStyleItalic -> ItalicSpan()
|
||||
R.id.fontStyleUnderline -> UnderlineSpan()
|
||||
R.id.fontStyleStrike -> StrikethroughSpan()
|
||||
R.id.fontStyleSubscript -> SubscriptSizeSpan(10, dip = true)
|
||||
R.id.fontStyleSuperscript -> SuperscriptSizeSpan(10, dip = true)
|
||||
else -> return
|
||||
}
|
||||
|
||||
// see comments in getHtmlText()
|
||||
watchSelectionChanged = false
|
||||
if (isChecked)
|
||||
BetterHtml.applyFormat(span = span, editText = b.text)
|
||||
else
|
||||
BetterHtml.removeFormat(span = span, editText = b.text)
|
||||
watchSelectionChanged = true
|
||||
|
||||
if (App.devMode)
|
||||
b.textHtml.text = getHtmlText()
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private fun onFormatClear(view: View) {
|
||||
// shortened version on onFormatChecked(), removing all spans
|
||||
watchSelectionChanged = false
|
||||
BetterHtml.removeFormat(span = null, editText = b.text)
|
||||
watchSelectionChanged = true
|
||||
if (App.devMode)
|
||||
b.textHtml.text = getHtmlText()
|
||||
// force update of text style toggle states
|
||||
onSelectionChanged(b.text.selectionStart, b.text.selectionEnd)
|
||||
}
|
||||
|
||||
private fun onSelectionChanged(selectionStart: Int, selectionEnd: Int) {
|
||||
if (!watchSelectionChanged)
|
||||
return
|
||||
val spanned = b.text.text ?: return
|
||||
val spans = spanned.getSpans(selectionStart, selectionEnd, Any::class.java).mapNotNull {
|
||||
if (it::class.java !in BetterHtml.customSpanClasses)
|
||||
return@mapNotNull null
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
// remove 0-length spans after navigating out of them
|
||||
if (spanStart == spanEnd)
|
||||
spanned.removeSpan(it)
|
||||
else if (spanned.getSpanFlags(it) hasSet SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
spanned.setSpan(it, spanStart, spanEnd, SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
|
||||
// names are helpful here
|
||||
val isNotAfterWord = selectionEnd <= spanEnd
|
||||
val isSelectionInWord = selectionStart != selectionEnd && selectionStart >= spanStart
|
||||
val isCursorInWord = selectionStart == selectionEnd && selectionStart > spanStart
|
||||
val isChecked = (isCursorInWord || isSelectionInWord) && isNotAfterWord
|
||||
if (isChecked) it::class.java else null
|
||||
}
|
||||
watchFormatChecked = false
|
||||
b.fontStyleBold.isChecked = BoldSpan::class.java in spans
|
||||
b.fontStyleItalic.isChecked = ItalicSpan::class.java in spans
|
||||
b.fontStyleUnderline.isChecked = UnderlineSpan::class.java in spans
|
||||
b.fontStyleStrike.isChecked = StrikethroughSpan::class.java in spans
|
||||
b.fontStyleSubscript.isChecked = SubscriptSizeSpan::class.java in spans
|
||||
b.fontStyleSuperscript.isChecked = SuperscriptSizeSpan::class.java in spans
|
||||
watchFormatChecked = true
|
||||
}
|
||||
|
||||
private fun createView() {
|
||||
b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
|
||||
b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f)
|
||||
@ -203,107 +291,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
else -> -1
|
||||
}
|
||||
|
||||
b.recipients.chipTokenizer = SpanChipTokenizer<ChipSpan>(activity, object : ChipSpanChipCreator() {
|
||||
override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? {
|
||||
if (data == null || data !is Teacher)
|
||||
return null
|
||||
if (data.id !in -24L..0L) {
|
||||
b.recipients.allChips.forEach {
|
||||
if (it.data == data) {
|
||||
Toast.makeText(activity, R.string.messages_compose_recipient_exists, Toast.LENGTH_SHORT).show()
|
||||
return null
|
||||
}
|
||||
}
|
||||
val chipSpan = ChipSpan(context, data.fullName, BitmapDrawable(context.resources, data.image), data)
|
||||
chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(data.fullName))
|
||||
return chipSpan
|
||||
}
|
||||
|
||||
val type = (data.id * -1).toInt()
|
||||
|
||||
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(activity)
|
||||
val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
|
||||
|
||||
val sortByCategory = type in listOf(
|
||||
Teacher.TYPE_PARENTS_COUNCIL,
|
||||
Teacher.TYPE_EDUCATOR,
|
||||
Teacher.TYPE_STUDENT
|
||||
)
|
||||
val teachers = if (sortByCategory)
|
||||
teachers.sortedBy { it.typeDescription }
|
||||
else
|
||||
teachers
|
||||
|
||||
val category = mutableListOf<Teacher>()
|
||||
val categoryNames = mutableListOf<CharSequence>()
|
||||
val categoryCheckedItems = mutableListOf<Boolean>()
|
||||
teachers.forEach { teacher ->
|
||||
if (!teacher.isType(type))
|
||||
return@forEach
|
||||
|
||||
category += teacher
|
||||
val name = teacher.fullName
|
||||
val description = when (type) {
|
||||
Teacher.TYPE_TEACHER -> null
|
||||
Teacher.TYPE_PARENTS_COUNCIL -> teacher.typeDescription
|
||||
Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null
|
||||
Teacher.TYPE_PEDAGOGUE -> null
|
||||
Teacher.TYPE_LIBRARIAN -> null
|
||||
Teacher.TYPE_SCHOOL_ADMIN -> null
|
||||
Teacher.TYPE_SUPER_ADMIN -> null
|
||||
Teacher.TYPE_SECRETARIAT -> null
|
||||
Teacher.TYPE_PRINCIPAL -> null
|
||||
Teacher.TYPE_EDUCATOR -> teacher.typeDescription
|
||||
Teacher.TYPE_PARENT -> teacher.typeDescription
|
||||
Teacher.TYPE_STUDENT -> teacher.typeDescription
|
||||
Teacher.TYPE_SPECIALIST -> null
|
||||
else -> teacher.typeDescription
|
||||
}
|
||||
categoryNames += listOfNotNull(
|
||||
name.asSpannable(
|
||||
ForegroundColorSpan(textColorPrimary)
|
||||
),
|
||||
description?.asSpannable(
|
||||
ForegroundColorSpan(textColorSecondary),
|
||||
AbsoluteSizeSpan(14.dp)
|
||||
)
|
||||
).concat("\n")
|
||||
|
||||
// check the teacher if already added as a recipient
|
||||
categoryCheckedItems += b.recipients.allChips.firstOrNull { it.data == teacher } != null
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setTitle("Dodaj odbiorców - "+ Teacher.typeName(activity, type))
|
||||
//.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type)))
|
||||
.setPositiveButton("OK", null)
|
||||
.setNeutralButton("Anuluj", null)
|
||||
.setMultiChoiceItems(categoryNames.toTypedArray(), categoryCheckedItems.toBooleanArray()) { _, which, isChecked ->
|
||||
val teacher = category[which]
|
||||
if (isChecked) {
|
||||
val chipInfoList = mutableListOf<ChipInfo>()
|
||||
teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
|
||||
chipInfoList.add(ChipInfo(teacher.fullName, teacher))
|
||||
b.recipients.addTextWithChips(chipInfoList)
|
||||
}
|
||||
else {
|
||||
b.recipients.allChips.forEach {
|
||||
if (it.data == teacher)
|
||||
b.recipients.chipTokenizer?.deleteChipAndPadding(it, b.recipients.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
return null
|
||||
}
|
||||
|
||||
override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) {
|
||||
super.configureChip(chip, chipConfiguration)
|
||||
chip.setBackgroundColor(elevateSurface(activity, 8).toColorStateList())
|
||||
chip.setTextColor(getPrimaryTextColor(activity))
|
||||
}
|
||||
}, ChipSpan::class.java)
|
||||
|
||||
b.recipients.chipTokenizer = MessagesComposeChipTokenizer(activity, b.recipients, teachers)
|
||||
b.recipients.setIllegalCharacterIdentifier { c ->
|
||||
c.toString().matches("[\\n;:_ ]".toRegex())
|
||||
}
|
||||
@ -330,6 +318,55 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
b.subjectLayout.isEnabled = false
|
||||
b.textLayout.isEnabled = false
|
||||
|
||||
b.fontStyleLayout.isVisible = enableTextStyling
|
||||
b.fontStyleBold.isEnabled = false
|
||||
b.fontStyleItalic.isEnabled = false
|
||||
b.fontStyleUnderline.isEnabled = false
|
||||
b.fontStyleStrike.isEnabled = false
|
||||
b.fontStyleSubscript.isEnabled = false
|
||||
b.fontStyleSuperscript.isEnabled = false
|
||||
b.fontStyleClear.isEnabled = false
|
||||
b.text.setOnFocusChangeListener { _, hasFocus ->
|
||||
b.fontStyleBold.isEnabled = hasFocus
|
||||
b.fontStyleItalic.isEnabled = hasFocus
|
||||
b.fontStyleUnderline.isEnabled = hasFocus
|
||||
b.fontStyleStrike.isEnabled = hasFocus
|
||||
b.fontStyleSubscript.isEnabled = hasFocus
|
||||
b.fontStyleSuperscript.isEnabled = hasFocus
|
||||
b.fontStyleClear.isEnabled = hasFocus
|
||||
}
|
||||
|
||||
b.fontStyleBold.text = CommunityMaterial.Icon2.cmd_format_bold.character.toString()
|
||||
b.fontStyleItalic.text = CommunityMaterial.Icon2.cmd_format_italic.character.toString()
|
||||
b.fontStyleUnderline.text = CommunityMaterial.Icon2.cmd_format_underline.character.toString()
|
||||
b.fontStyleStrike.text = CommunityMaterial.Icon2.cmd_format_strikethrough.character.toString()
|
||||
b.fontStyleSubscript.text = CommunityMaterial.Icon2.cmd_format_subscript.character.toString()
|
||||
b.fontStyleSuperscript.text = CommunityMaterial.Icon2.cmd_format_superscript.character.toString()
|
||||
b.fontStyleBold.attachToastHint(R.string.hint_style_bold)
|
||||
b.fontStyleItalic.attachToastHint(R.string.hint_style_italic)
|
||||
b.fontStyleUnderline.attachToastHint(R.string.hint_style_underline)
|
||||
b.fontStyleStrike.attachToastHint(R.string.hint_style_strike)
|
||||
b.fontStyleSubscript.attachToastHint(R.string.hint_style_subscript)
|
||||
b.fontStyleSuperscript.attachToastHint(R.string.hint_style_superscript)
|
||||
|
||||
/*b.fontStyleBold.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setBottomLeftCornerSize(0f)
|
||||
.build()
|
||||
b.fontStyleSuperscript.shapeAppearanceModel = b.fontStyleBold.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setBottomRightCornerSize(0f)
|
||||
.build()
|
||||
b.fontStyleClear.shapeAppearanceModel = b.fontStyleClear.shapeAppearanceModel
|
||||
.toBuilder()
|
||||
.setTopLeftCornerSize(0f)
|
||||
.setTopRightCornerSize(0f)
|
||||
.build()*/
|
||||
|
||||
b.fontStyle.addOnButtonCheckedListener(this::onFormatChecked)
|
||||
b.fontStyleClear.setOnClickListener(this::onFormatClear)
|
||||
b.text.setSelectionChangedListener(this::onSelectionChanged)
|
||||
|
||||
activity.navView.bottomBar.apply {
|
||||
fabEnable = true
|
||||
fabExtendedText = getString(R.string.messages_compose_send)
|
||||
@ -382,11 +419,11 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
val dateString = getString(R.string.messages_date_time_format, Date.fromMillis(msg.addedDate).formattedStringShort, Time.fromMillis(msg.addedDate).stringHM)
|
||||
// add original message info
|
||||
span.appendText("W dniu ")
|
||||
span.appendSpan(dateString, StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
span.appendSpan(dateString, ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
span.appendText(", ")
|
||||
span.appendSpan(msg.senderName.fixName(), StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
span.appendSpan(msg.senderName.fixName(), ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
span.appendText(" napisał(a):")
|
||||
span.setSpan(StyleSpan(Typeface.BOLD), 0, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
span.setSpan(BoldSpan(), 0, span.length, SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
span.appendText("\n\n")
|
||||
|
||||
if (arguments?.getString("type") == "reply") {
|
||||
@ -422,6 +459,8 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
b.subject.setText(subject)
|
||||
b.text.apply {
|
||||
text = span.appendText(body)
|
||||
if (!enableTextStyling)
|
||||
setText(text?.toString())
|
||||
setSelection(0)
|
||||
}
|
||||
b.root.scrollTo(0, 0)
|
||||
@ -494,30 +533,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength)
|
||||
return
|
||||
|
||||
var textHtml = if (app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) {
|
||||
HtmlCompat.toHtml(SpannableString(text), HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
|
||||
.replace("\n", "")
|
||||
.replace(" dir=\"ltr\"", "")
|
||||
}
|
||||
else {
|
||||
text.toString()
|
||||
}
|
||||
|
||||
textHtml = textHtml
|
||||
.replace("</b><b>", "")
|
||||
.replace("</i><i>", "")
|
||||
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
|
||||
|
||||
if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) {
|
||||
textHtml = textHtml
|
||||
.replace("</p><br>", "</p>")
|
||||
.replace("<b>", "<strong>")
|
||||
.replace("</b>", "</strong>")
|
||||
.replace("<i>", "<em>")
|
||||
.replace("</i>", "</em>")
|
||||
.replace("<u>", "<span style=\"text-decoration: underline;\">")
|
||||
.replace("</u>", "</span>")
|
||||
}
|
||||
val textHtml = getHtmlText()
|
||||
|
||||
activity.bottomSheet.hideKeyboard()
|
||||
|
||||
@ -525,7 +541,7 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||
.setTitle(R.string.messages_compose_confirm_title)
|
||||
.setMessage(R.string.messages_compose_confirm_text)
|
||||
.setPositiveButton(R.string.send) { _, _ ->
|
||||
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml.trim()).enqueue(activity)
|
||||
EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml).enqueue(activity)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
|
@ -18,6 +18,7 @@ class TextInputKeyboardEdit : AppCompatEditText {
|
||||
* Keyboard Listener
|
||||
*/
|
||||
internal var listener: KeyboardListener? = null
|
||||
private var selectionListener: ((Int, Int) -> Unit)? = null
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
@ -27,14 +28,12 @@ class TextInputKeyboardEdit : AppCompatEditText {
|
||||
|
||||
override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) {
|
||||
super.onFocusChanged(focused, direction, previouslyFocusedRect)
|
||||
if (listener != null)
|
||||
listener!!.onStateChanged(this, true)
|
||||
listener?.onStateChanged(this, true)
|
||||
}
|
||||
|
||||
override fun onKeyPreIme(keyCode: Int, @NonNull event: KeyEvent): Boolean {
|
||||
if (event.keyCode == KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
|
||||
if (listener != null)
|
||||
listener!!.onStateChanged(this, false)
|
||||
listener?.onStateChanged(this, false)
|
||||
|
||||
// Hide cursor
|
||||
isFocusable = false
|
||||
@ -50,6 +49,15 @@ class TextInputKeyboardEdit : AppCompatEditText {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
fun setSelectionChangedListener(listener: ((selectionStart: Int, selectionEnd: Int) -> Unit)?) {
|
||||
this.selectionListener = listener
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
||||
super.onSelectionChanged(selStart, selEnd)
|
||||
selectionListener?.invoke(selStart, selEnd)
|
||||
}
|
||||
|
||||
interface KeyboardListener {
|
||||
fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean)
|
||||
}
|
||||
|
@ -6,26 +6,41 @@ package pl.szczodrzynski.edziennik.utils.html
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.text.Editable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.BulletSpan
|
||||
import android.text.Spanned.*
|
||||
import android.text.style.*
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.text.HtmlCompat
|
||||
import pl.szczodrzynski.edziennik.dp
|
||||
import pl.szczodrzynski.edziennik.getWordBounds
|
||||
import pl.szczodrzynski.edziennik.resolveAttr
|
||||
import pl.szczodrzynski.edziennik.utils.span.*
|
||||
import pl.szczodrzynski.navlib.blendColors
|
||||
|
||||
object BetterHtml {
|
||||
|
||||
val customSpanClasses = listOf(
|
||||
BoldSpan::class.java,
|
||||
ItalicSpan::class.java,
|
||||
UnderlineSpan::class.java,
|
||||
StrikethroughSpan::class.java,
|
||||
SubscriptSizeSpan::class.java,
|
||||
SuperscriptSizeSpan::class.java,
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
fun fromHtml(context: Context, html: String): Spanned {
|
||||
val hexPattern = "(#[a-fA-F0-9]{6})"
|
||||
val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})"
|
||||
.toRegex(RegexOption.IGNORE_CASE)
|
||||
.toRegex(RegexOption.IGNORE_CASE)
|
||||
|
||||
var text = html
|
||||
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
|
||||
.replace("background-color: ?$hexPattern;".toRegex(), "")
|
||||
.replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "")
|
||||
.replace("background-color: ?$hexPattern;".toRegex(), "")
|
||||
|
||||
val colorBackground = android.R.attr.colorBackground.resolveAttr(context)
|
||||
val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff
|
||||
@ -39,7 +54,11 @@ object BetterHtml {
|
||||
var blendAmount = 1
|
||||
var numIterations = 0
|
||||
|
||||
while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) {
|
||||
while (numIterations < 100 && ColorUtils.calculateContrast(
|
||||
colorBackground,
|
||||
newColor
|
||||
) < 4.5f
|
||||
) {
|
||||
blendAmount += 2
|
||||
newColor = blendColors(color, blendAmount shl 24 or textColorPrimary)
|
||||
numIterations++
|
||||
@ -59,26 +78,109 @@ object BetterHtml {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val htmlSpannable = HtmlCompat.fromHtml(
|
||||
text,
|
||||
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV,
|
||||
null,
|
||||
LiTagHandler()
|
||||
text,
|
||||
HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV,
|
||||
null,
|
||||
LiTagHandler()
|
||||
)
|
||||
|
||||
val spannableBuilder = SpannableStringBuilder(htmlSpannable)
|
||||
val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java)
|
||||
bulletSpans.forEach {
|
||||
val start = spannableBuilder.getSpanStart(it)
|
||||
val end = spannableBuilder.getSpanEnd(it)
|
||||
spannableBuilder.removeSpan(it)
|
||||
spannableBuilder.setSpan(
|
||||
ImprovedBulletSpan(bulletRadius = 3.dp, startWidth = 24.dp, gapWidth = 8.dp),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
val spanned = SpannableStringBuilder(htmlSpannable)
|
||||
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
val spanFlags = spanned.getSpanFlags(it)
|
||||
|
||||
val newSpan: Any? = when (it) {
|
||||
is BulletSpan -> ImprovedBulletSpan(
|
||||
bulletRadius = 3.dp,
|
||||
startWidth = 24.dp,
|
||||
gapWidth = 8.dp
|
||||
)
|
||||
is StyleSpan -> when (it.style) {
|
||||
Typeface.BOLD -> BoldSpan()
|
||||
Typeface.ITALIC -> ItalicSpan()
|
||||
else -> null
|
||||
}
|
||||
is SubscriptSpan -> SubscriptSizeSpan(size = 10, dip = true)
|
||||
is SuperscriptSpan -> SuperscriptSizeSpan(size = 10, dip = true)
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (newSpan != null) {
|
||||
spanned.removeSpan(it)
|
||||
spanned.setSpan(newSpan, spanStart, spanEnd, spanFlags)
|
||||
}
|
||||
}
|
||||
|
||||
return spannableBuilder
|
||||
return spanned
|
||||
}
|
||||
|
||||
fun applyFormat(span: Any, editText: AppCompatEditText) {
|
||||
applyFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd)
|
||||
}
|
||||
|
||||
fun removeFormat(span: Any?, editText: AppCompatEditText) {
|
||||
removeFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd)
|
||||
}
|
||||
|
||||
fun applyFormat(span: Any, spanned: Editable, selectionStart: Int, selectionEnd: Int) {
|
||||
if (selectionStart == -1 || selectionEnd == -1) return
|
||||
val cursorOnly = selectionStart == selectionEnd
|
||||
|
||||
val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true)
|
||||
if (cursorOnly && wordBounds != null) {
|
||||
// use the detected word bounds instead of cursor/selection
|
||||
val (start, end) = wordBounds
|
||||
spanned.setSpan(span, start, end, SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
} else {
|
||||
val spanFlags = if (cursorOnly)
|
||||
SPAN_INCLUSIVE_INCLUSIVE
|
||||
else
|
||||
SPAN_EXCLUSIVE_INCLUSIVE
|
||||
spanned.setSpan(span, selectionStart, selectionEnd, spanFlags)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeFormat(span: Any?, spanned: Editable, selectionStart: Int, selectionEnd: Int) {
|
||||
if (selectionStart == -1 || selectionEnd == -1) return
|
||||
val cursorOnly = selectionStart == selectionEnd
|
||||
|
||||
val spanClass = span?.javaClass ?: Any::class.java
|
||||
spanned.getSpans(selectionStart, selectionEnd, spanClass).forEach {
|
||||
if (span == null && it::class.java !in customSpanClasses)
|
||||
return@forEach
|
||||
val spanStart = spanned.getSpanStart(it)
|
||||
val spanEnd = spanned.getSpanEnd(it)
|
||||
val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true)
|
||||
|
||||
val (newSpanStart, newSpanEnd, newSpanFlags) = when {
|
||||
!cursorOnly -> {
|
||||
// cut the selected range out of the span
|
||||
Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
}
|
||||
wordBounds == null -> {
|
||||
// this allows to change spans mid-word - EXCLUSIVE so the style does
|
||||
// not apply to characters typed later
|
||||
// it's set back to INCLUSIVE when the cursor enters the word again
|
||||
// (onSelectionChanged)
|
||||
Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
else /* wordBounds != null */ -> {
|
||||
// a word is selected, slice the span in two
|
||||
Triple(wordBounds.first, wordBounds.second, SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
}
|
||||
}
|
||||
|
||||
// remove the existing span
|
||||
spanned.removeSpan(it)
|
||||
// "clone" the span so it can be applied twice, if needed
|
||||
// (can't use 'span' from the parameters as it's nullable)
|
||||
val itClone = it::class.java.newInstance()
|
||||
// reapply the span wherever needed
|
||||
if (spanStart < newSpanStart)
|
||||
spanned.setSpan(it, spanStart, newSpanStart, newSpanFlags)
|
||||
if (spanEnd > newSpanEnd)
|
||||
spanned.setSpan(itClone, newSpanEnd, spanEnd, newSpanFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
||||
package pl.szczodrzynski.edziennik.utils.html
|
||||
package pl.szczodrzynski.edziennik.utils.span
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
@ -20,10 +20,10 @@ import android.text.style.LeadingMarginSpan
|
||||
* Copy of [android.text.style.BulletSpan] from android SDK 28 with removed internal code
|
||||
*/
|
||||
class ImprovedBulletSpan(
|
||||
val bulletRadius: Int = STANDARD_BULLET_RADIUS,
|
||||
val startWidth: Int = STANDARD_GAP_WIDTH,
|
||||
val gapWidth: Int = STANDARD_GAP_WIDTH,
|
||||
val color: Int = STANDARD_COLOR
|
||||
val bulletRadius: Int = STANDARD_BULLET_RADIUS,
|
||||
val startWidth: Int = STANDARD_GAP_WIDTH,
|
||||
val gapWidth: Int = STANDARD_GAP_WIDTH,
|
||||
val color: Int = STANDARD_COLOR
|
||||
) : LeadingMarginSpan {
|
||||
|
||||
companion object {
|
@ -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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial" />
|
||||
</data>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="40dp"
|
||||
android:orientation="vertical"><!-- half of the FAB's size -->
|
||||
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="40dp"><!-- half of the FAB's size -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/recipientsLayout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense"
|
||||
@ -22,8 +28,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
app:boxBackgroundColor="@android:color/transparent"
|
||||
app:boxBackgroundMode="filled"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/dropdown_arrow">
|
||||
app:endIconDrawable="@drawable/dropdown_arrow"
|
||||
app:endIconMode="custom">
|
||||
|
||||
<com.hootsuite.nachos.NachoTextView
|
||||
android:id="@+id/recipients"
|
||||
@ -34,7 +40,6 @@
|
||||
android:focusableInTouchMode="true"
|
||||
android:focusedByDefault="true"
|
||||
android:hint="@string/messages_compose_to_hint" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
@ -55,10 +60,9 @@
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:hint="@string/messages_compose_subject_hint"
|
||||
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"
|
||||
android:singleLine="true"
|
||||
tools:text="kachoomba"
|
||||
android:inputType="textCapSentences|textAutoCorrect|textShortMessage|textAutoComplete|textEmailSubject"/>
|
||||
|
||||
tools:text="kachoomba" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
@ -69,66 +73,131 @@
|
||||
app:boxBackgroundMode="filled"
|
||||
app:counterEnabled="true"
|
||||
tools:counterMaxLength="1983">
|
||||
|
||||
<pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:minLines="3"
|
||||
android:hint="@string/messages_compose_text_hint"
|
||||
android:ems="10"
|
||||
android:gravity="start|top"
|
||||
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n"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:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences" />
|
||||
android:hint="@string/messages_compose_text_hint"
|
||||
android:inputType="textMultiLine|textAutoCorrect|textLongMessage|textAutoComplete|textCapSentences"
|
||||
android:minLines="3"
|
||||
tools:text="Witam,\n\nchciałem przekazać bardzo ważną wiadomość.\nJest to cytat znanej osoby.\n\n"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.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/fontStyle"
|
||||
<LinearLayout
|
||||
android:id="@+id/fontStyleLayout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginTop="4dp">
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleBold"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/fontStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
tools:icon="@sample/format-bold"
|
||||
app:iconPadding="0dp"/>
|
||||
android:layout_marginHorizontal="8dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleBold"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/community_material_font_v5_8_55"
|
||||
android:minWidth="48dp"
|
||||
android:textSize="20sp"
|
||||
app:iconPadding="0dp"
|
||||
tools:text="\uf674" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleItalic"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/community_material_font_v5_8_55"
|
||||
android:minWidth="48dp"
|
||||
android:textSize="20sp"
|
||||
app:iconPadding="0dp"
|
||||
tools:text="\uf691" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleUnderline"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/community_material_font_v5_8_55"
|
||||
android:minWidth="48dp"
|
||||
android:textSize="20sp"
|
||||
app:iconPadding="0dp"
|
||||
tools:text="\uf6c5" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleStrike"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/community_material_font_v5_8_55"
|
||||
android:minWidth="48dp"
|
||||
android:textSize="20sp"
|
||||
app:iconPadding="0dp"
|
||||
tools:text="\uf6b1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleSubscript"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/community_material_font_v5_8_55"
|
||||
android:minWidth="48dp"
|
||||
android:textSize="20sp"
|
||||
app:iconPadding="0dp"
|
||||
tools:text="\uf6b2" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleSuperscript"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/community_material_font_v5_8_55"
|
||||
android:minWidth="48dp"
|
||||
android:textSize="20sp"
|
||||
app:iconPadding="0dp"
|
||||
tools:text="\uf6b3" />
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<!-- TODO 2021-10-05: stack the group and button on top -->
|
||||
<!-- android:layout_marginTop="-13dp" -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleItalic"
|
||||
android:id="@+id/fontStyleClear"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
tools:icon="@sample/format-bold"
|
||||
app:iconPadding="0dp"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fontStyleUnderline"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
tools:icon="@sample/format-bold"
|
||||
app:iconPadding="0dp"/>
|
||||
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>-->
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:text="@string/messages_compose_style_clear" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/breakpoints"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:text="Breakpoints!"
|
||||
android:visibility="gone"/>
|
||||
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textHtml"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:fontFamily="monospace"
|
||||
android:visibility="gone"
|
||||
tools:text="<p>Witam,</p>To jest bardzo długi tekst żeby sprawdzić ok działa"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</layout>
|
||||
|
@ -1464,4 +1464,11 @@
|
||||
<string name="contributors_subtext_format" translatable="false">\@%s - %s</string>
|
||||
<string name="contributors_headline">Najłatwiejszy sposób na korzystanie z e-dziennika.</string>
|
||||
<string name="dialog_lesson_attendance_details">Szczegóły</string>
|
||||
<string name="hint_style_bold">Pogrubienie</string>
|
||||
<string name="hint_style_italic">Pochylenie</string>
|
||||
<string name="hint_style_underline">Podkreślenie</string>
|
||||
<string name="hint_style_strike">Przekreślenie</string>
|
||||
<string name="hint_style_subscript">Indeks dolny</string>
|
||||
<string name="hint_style_superscript">Indeks górny</string>
|
||||
<string name="messages_compose_style_clear">Wyczyść format</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user