From 37ade3a709dbbb0c07d983d800c50b1c90a7d840 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Wed, 7 Mar 2018 14:44:12 +0100
Subject: [PATCH] Implement formatting editor

---
 .../quasseldroid_ng/ui/chat/ChatActivity.kt   |  45 +++-
 .../quasseldroid_ng/ui/chat/InputEditor.kt    | 207 ++++++++++++++++++
 .../util/helper/SelectionHelper.kt            |  10 +
 .../util/irc/format/IrcFormatDeserializer.kt  |  87 +++-----
 .../util/irc/format/IrcFormatSerializer.kt    |   7 +-
 ...rmat_fill.xml => ic_format_background.xml} |   0
 app/src/main/res/drawable/ic_format_clear.xml |  10 +
 ...mat_paint.xml => ic_format_foreground.xml} |   0
 .../main/res/drawable/ic_format_monospace.xml |  10 +
 .../res/drawable/ic_format_strikethrough.xml  |  10 +
 .../res/layout-sw720dp-land/activity_main.xml |   2 +-
 app/src/main/res/layout/activity_main.xml     |   2 +-
 app/src/main/res/layout/layout_editor.xml     |   5 +-
 app/src/main/res/layout/layout_slider.xml     |   1 +
 app/src/main/res/layout/layout_toolbar.xml    |   2 +-
 app/src/main/res/menu/editor.xml              |  46 ++++
 app/src/main/res/menu/input_panel.xml         |   9 +
 app/src/main/res/values/strings.xml           |   8 +
 .../primitive/serializer/StringSerializer.kt  |   4 +-
 19 files changed, 387 insertions(+), 78 deletions(-)
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt
 create mode 100644 app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SelectionHelper.kt
 rename app/src/main/res/drawable/{ic_format_fill.xml => ic_format_background.xml} (100%)
 create mode 100644 app/src/main/res/drawable/ic_format_clear.xml
 rename app/src/main/res/drawable/{ic_format_paint.xml => ic_format_foreground.xml} (100%)
 create mode 100644 app/src/main/res/drawable/ic_format_monospace.xml
 create mode 100644 app/src/main/res/drawable/ic_format_strikethrough.xml
 create mode 100644 app/src/main/res/menu/editor.xml
 create mode 100644 app/src/main/res/menu/input_panel.xml

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
index df74b7053..af0bf9cb1 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/ChatActivity.kt
@@ -11,11 +11,12 @@ import android.os.Build
 import android.os.Bundle
 import android.os.PersistableBundle
 import android.support.design.widget.Snackbar
+import android.support.v4.graphics.drawable.DrawableCompat
 import android.support.v4.widget.DrawerLayout
 import android.support.v7.app.ActionBarDrawerToggle
+import android.support.v7.widget.ActionMenuView
 import android.support.v7.widget.Toolbar
 import android.text.InputType
-import android.text.Spanned
 import android.view.*
 import android.view.inputmethod.EditorInfo
 import android.widget.EditText
@@ -40,23 +41,29 @@ import de.kuschku.quasseldroid_ng.ui.settings.data.Settings
 import de.kuschku.quasseldroid_ng.ui.viewmodel.QuasselViewModel
 import de.kuschku.quasseldroid_ng.util.AndroidHandlerThread
 import de.kuschku.quasseldroid_ng.util.helper.*
-import de.kuschku.quasseldroid_ng.util.irc.format.IrcFormatSerializer
 import de.kuschku.quasseldroid_ng.util.service.ServiceBoundActivity
 import de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
 
-class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
-  @BindView(R.id.drawerLayout)
+class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
+                     ActionMenuView.OnMenuItemClickListener {
+  @BindView(R.id.drawer_layout)
   lateinit var drawerLayout: DrawerLayout
 
   @BindView(R.id.toolbar)
   lateinit var toolbar: Toolbar
 
-  @BindView(R.id.progressBar)
+  @BindView(R.id.formatting_menu)
+  lateinit var formattingMenu: ActionMenuView
+
+  @BindView(R.id.progress_bar)
   lateinit var progressBar: MaterialContentLoadingProgressBar
 
   @BindView(R.id.editor_panel)
   lateinit var editorPanel: SlidingUpPanelLayout
 
+  @BindView(R.id.history_panel)
+  lateinit var historyPanel: SlidingUpPanelLayout
+
   @BindView(R.id.send)
   lateinit var send: ImageButton
 
@@ -75,7 +82,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
 
   private lateinit var backlogSettings: BacklogSettings
 
-  private lateinit var ircFormatSerializer: IrcFormatSerializer
+  private lateinit var inputEditor: InputEditor
 
   private val panelSlideListener: SlidingUpPanelLayout.PanelSlideListener = object :
     SlidingUpPanelLayout.PanelSlideListener {
@@ -107,7 +114,21 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     viewModel = ViewModelProviders.of(this)[QuasselViewModel::class.java]
     viewModel.setBackend(this.backend)
     backlogSettings = Settings.backlog(this)
-    ircFormatSerializer = IrcFormatSerializer(this)
+
+    inputEditor = InputEditor(chatline)
+    menuInflater.inflate(inputEditor.menu, formattingMenu.menu)
+    menuInflater.inflate(R.menu.input_panel, formattingMenu.menu)
+    formattingMenu.setOnMenuItemClickListener(this)
+
+    formattingMenu.context.theme.styledAttributes(R.attr.colorControlNormal) {
+      val color = getColor(0, 0)
+
+      for (item in (0 until formattingMenu.menu.size()).map { formattingMenu.menu.getItem(it) }) {
+        val drawable = item.icon.mutate()
+        DrawableCompat.setTint(drawable, color)
+        item.icon = drawable
+      }
+    }
 
     database = QuasselDatabase.Creator.init(application)
 
@@ -185,7 +206,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
             for (line in text.lineSequence()) {
               session.aliasManager?.processInput(
                 bufferInfo,
-                if (line is Spanned) ircFormatSerializer.toEscapeCodes(line) else line.toString(),
+                inputEditor.formattedString,
                 output
               )
             }
@@ -320,6 +341,14 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
     else                 -> super.onOptionsItemSelected(item)
   }
 
+  override fun onMenuItemClick(item: MenuItem?) = when (item?.itemId) {
+    R.id.input_history -> {
+      historyPanel.panelState = SlidingUpPanelLayout.PanelState.EXPANDED
+      true
+    }
+    else               -> inputEditor.onMenuItemClick(item)
+  }
+
   override fun onDestroy() {
     handler.onDestroy()
     super.onDestroy()
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt
new file mode 100644
index 000000000..ebf820700
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/InputEditor.kt
@@ -0,0 +1,207 @@
+package de.kuschku.quasseldroid_ng.ui.chat
+
+import android.graphics.Typeface
+import android.support.annotation.MenuRes
+import android.text.Editable
+import android.text.Spanned
+import android.text.style.StrikethroughSpan
+import android.text.style.StyleSpan
+import android.text.style.TypefaceSpan
+import android.text.style.UnderlineSpan
+import android.view.MenuItem
+import android.widget.EditText
+import de.kuschku.quasseldroid_ng.R
+import de.kuschku.quasseldroid_ng.util.helper.selection
+import de.kuschku.quasseldroid_ng.util.irc.format.IrcFormatSerializer
+import de.kuschku.quasseldroid_ng.util.irc.format.spans.*
+
+class InputEditor(private val editText: EditText) {
+  private val serializer = IrcFormatSerializer(editText.context)
+  val formattedString: String
+    get() = serializer.toEscapeCodes(editText.text)
+
+  @MenuRes
+  val menu: Int = R.menu.editor
+
+  fun toggleBold(range: IntRange, createNew: Boolean = true) {
+    if (range.isEmpty())
+      return
+
+    val exists = editText.text.removeSpans<StyleSpan, IrcBoldSpan>(range) { span ->
+      when {
+        span is IrcBoldSpan         -> span
+        span.style == Typeface.BOLD -> IrcBoldSpan()
+        else                        -> null
+      }
+    }
+
+    if (!exists && createNew) {
+      editText.text.setSpan(
+        IrcBoldSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
+  }
+
+  fun toggleItalic(range: IntRange, createNew: Boolean = true) {
+    if (range.isEmpty())
+      return
+
+    val exists = editText.text.removeSpans<StyleSpan, IrcItalicSpan>(range) { span ->
+      when {
+        span is IrcItalicSpan         -> span
+        span.style == Typeface.ITALIC -> IrcItalicSpan()
+        else                          -> null
+      }
+    }
+
+    if (!exists && createNew) {
+      editText.text.setSpan(
+        IrcItalicSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
+  }
+
+  fun toggleUnderline(range: IntRange, createNew: Boolean = true) {
+    if (range.isEmpty())
+      return
+
+    val exists = editText.text.removeSpans<UnderlineSpan, IrcUnderlineSpan>(range) { span ->
+      when {
+        span is IrcUnderlineSpan -> span
+        else                     -> IrcUnderlineSpan()
+      }
+    }
+
+    if (!exists && createNew) {
+      editText.text.setSpan(
+        IrcUnderlineSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
+  }
+
+  fun toggleStrikethrough(range: IntRange, createNew: Boolean = true) {
+    if (range.isEmpty())
+      return
+
+    val exists = editText.text.removeSpans<StrikethroughSpan, IrcStrikethroughSpan>(range) { span ->
+      when {
+        span is IrcStrikethroughSpan -> span
+        else                         -> IrcStrikethroughSpan()
+      }
+    }
+
+    if (!exists && createNew) {
+      editText.text.setSpan(
+        IrcStrikethroughSpan(), range.start, range.endInclusive + 1,
+        Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
+  }
+
+  fun toggleMonospace(range: IntRange, createNew: Boolean = true) {
+    if (range.isEmpty())
+      return
+
+    val exists = editText.text.removeSpans<TypefaceSpan, IrcMonospaceSpan>(range) { span ->
+      when {
+        span is IrcMonospaceSpan   -> span
+        span.family == "monospace" -> IrcMonospaceSpan()
+        else                       -> null
+      }
+    }
+
+    if (!exists && createNew) {
+      editText.text.setSpan(
+        IrcMonospaceSpan(), range.start, range.endInclusive + 1, Spanned.SPAN_INCLUSIVE_EXCLUSIVE
+      )
+    }
+  }
+
+  fun clearFormatting(range: IntRange) {
+    if (range.isEmpty())
+      return
+
+    toggleBold(range, false)
+    toggleItalic(range, false)
+    toggleUnderline(range, false)
+    toggleStrikethrough(range, false)
+    toggleMonospace(range, false)
+  }
+
+  fun onMenuItemClick(item: MenuItem?) = when (item?.itemId) {
+    R.id.format_bold          -> {
+      toggleBold(editText.selection)
+      true
+    }
+    R.id.format_italic        -> {
+      toggleItalic(editText.selection)
+      true
+    }
+    R.id.format_underline     -> {
+      toggleUnderline(editText.selection)
+      true
+    }
+    R.id.format_strikethrough -> {
+      toggleStrikethrough(editText.selection)
+      true
+    }
+    R.id.format_monospace     -> {
+      toggleMonospace(editText.selection)
+      true
+    }
+    R.id.format_clear         -> {
+      clearFormatting(editText.selection)
+      true
+    }
+    else                      -> false
+  }
+
+  private inline fun <reified U, T> Editable.removeSpans(
+    range: IntRange, removeInvalid: Boolean = false, f: (U) -> T?): Boolean where T : Copyable<T> {
+    if (range.isEmpty())
+      return false
+
+    var removedAny = false
+
+    for (raw in getSpans<U>(range.start, range.endInclusive + 1, U::class.java)) {
+      val spanFlags = getSpanFlags(raw)
+      if (spanFlags and Spanned.SPAN_COMPOSING != 0) continue
+
+      val spanEnd = getSpanEnd(raw)
+      val spanStart = getSpanStart(raw)
+
+      val span = f(raw)
+      if (span == null) {
+        if (removeInvalid)
+          removeSpan(raw)
+      } else {
+        removeSpan(raw)
+
+        val endIsIn = spanEnd in range
+        val endIsAfter = spanEnd > range.endInclusive + 1
+
+        val startIsIn = spanStart in range
+        val startIsBefore = spanStart < range.start
+
+        if (endIsIn && startIsIn) {
+          removedAny = true
+        } else if (endIsIn) {
+          setSpan(span, spanStart, range.start, spanFlags)
+          removedAny = true
+        } else if (startIsIn) {
+          setSpan(span, range.endInclusive + 1, spanEnd, spanFlags)
+          removedAny = true
+        } else if (startIsBefore && endIsAfter) {
+          setSpan(span, spanStart, range.start, spanFlags)
+          setSpan(span.copy(), range.endInclusive + 1, spanEnd, spanFlags)
+          removedAny = true
+        } else if (startIsBefore) {
+          setSpan(span, spanStart, range.start, spanFlags)
+          removedAny = true
+        }
+      }
+    }
+
+    return removedAny
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SelectionHelper.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SelectionHelper.kt
new file mode 100644
index 000000000..f69883b38
--- /dev/null
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/helper/SelectionHelper.kt
@@ -0,0 +1,10 @@
+package de.kuschku.quasseldroid_ng.util.helper
+
+import android.text.Selection
+import android.widget.EditText
+
+val CharSequence.selection: IntRange
+  get() = Selection.getSelectionStart(this) until Selection.getSelectionEnd(this)
+
+val EditText.selection: IntRange
+  get() = text.selection
\ No newline at end of file
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
index 75d3c7c00..1033d005b 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatDeserializer.kt
@@ -76,13 +76,13 @@ class IrcFormatDeserializer(private val context: Context) {
     if (str == null) return ""
 
     val plainText = SpannableStringBuilder()
-    var bold: FormatDescription? = null
-    var italic: FormatDescription? = null
-    var underline: FormatDescription? = null
-    var strikethrough: FormatDescription? = null
-    var monospace: FormatDescription? = null
-    var color: FormatDescription? = null
-    var hexColor: FormatDescription? = null
+    var bold: FormatDescription<BoldIrcFormat>? = null
+    var italic: FormatDescription<ItalicIrcFormat>? = null
+    var underline: FormatDescription<UnderlineIrcFormat>? = null
+    var strikethrough: FormatDescription<StrikethroughIrcFormat>? = null
+    var monospace: FormatDescription<MonospaceIrcFormat>? = null
+    var color: FormatDescription<ColorIrcFormat>? = null
+    var hexColor: FormatDescription<HexIrcFormat>? = null
 
     // Iterating over every character
     var normalCount = 0
@@ -100,8 +100,7 @@ class IrcFormatDeserializer(private val context: Context) {
             bold = null
             // Otherwise create a new one
           } else {
-            val format = fromId(character)
-            bold = FormatDescription(plainText.length, format!!)
+            bold = FormatDescription(plainText.length, BoldIrcFormat())
           }
         }
         CODE_ITALIC    -> {
@@ -114,8 +113,7 @@ class IrcFormatDeserializer(private val context: Context) {
             italic = null
             // Otherwise create a new one
           } else {
-            val format = fromId(character)
-            italic = FormatDescription(plainText.length, format!!)
+            italic = FormatDescription(plainText.length, ItalicIrcFormat())
           }
         }
         CODE_UNDERLINE     -> {
@@ -128,8 +126,7 @@ class IrcFormatDeserializer(private val context: Context) {
             underline = null
             // Otherwise create a new one
           } else {
-            val format = fromId(character)
-            underline = FormatDescription(plainText.length, format!!)
+            underline = FormatDescription(plainText.length, UnderlineIrcFormat())
           }
         }
         CODE_STRIKETHROUGH -> {
@@ -142,8 +139,7 @@ class IrcFormatDeserializer(private val context: Context) {
             strikethrough = null
             // Otherwise create a new one
           } else {
-            val format = fromId(character)
-            strikethrough = FormatDescription(plainText.length, format!!)
+            strikethrough = FormatDescription(plainText.length, StrikethroughIrcFormat())
           }
         }
         CODE_MONOSPACE     -> {
@@ -156,8 +152,7 @@ class IrcFormatDeserializer(private val context: Context) {
             monospace = null
             // Otherwise create a new one
           } else {
-            val format = fromId(character)
-            monospace = FormatDescription(plainText.length, format!!)
+            monospace = FormatDescription(plainText.length, MonospaceIrcFormat())
           }
         }
         CODE_COLOR         -> {
@@ -183,7 +178,7 @@ class IrcFormatDeserializer(private val context: Context) {
               if (colorize) color.apply(plainText, plainText.length)
               // Reuse old background, if possible
               if (background.toInt() == -1)
-                background = (color.format as ColorIrcFormat).background
+                background = color.format.background
             }
             // Add new format
             color = FormatDescription(plainText.length, ColorIrcFormat(foreground, background))
@@ -207,7 +202,7 @@ class IrcFormatDeserializer(private val context: Context) {
           if (colorEnd > colorStart) {
             val foreground = readHexNumber(str, colorStart, colorEnd)
             // Add new format
-            hexColor = FormatDescription(plainText.length, ColorHexFormat(foreground))
+            hexColor = FormatDescription(plainText.length, HexIrcFormat(foreground))
 
             // i points in front of the next character
             i = colorEnd - 1
@@ -226,7 +221,7 @@ class IrcFormatDeserializer(private val context: Context) {
           if (color != null) {
             if (colorize) color.apply(plainText, plainText.length)
             color = FormatDescription(
-              plainText.length, (color.format as ColorIrcFormat).copySwapped()
+              plainText.length, color.format.copySwapped()
             )
           }
         }
@@ -274,20 +269,27 @@ class IrcFormatDeserializer(private val context: Context) {
     if (underline != null) {
       if (colorize) underline.apply(plainText, plainText.length)
     }
+    if (strikethrough != null) {
+      if (colorize) strikethrough.apply(plainText, plainText.length)
+    }
+    if (monospace != null) {
+      if (colorize) monospace.apply(plainText, plainText.length)
+    }
     if (color != null) {
       if (colorize) color.apply(plainText, plainText.length)
     }
+    if (hexColor != null) {
+      if (colorize) hexColor.apply(plainText, plainText.length)
+    }
     plainText.append(str.substring(str.length - normalCount, str.length))
     return plainText
   }
 
   private interface IrcFormat {
     fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int)
-
-    fun id(): Byte
   }
 
-  private class FormatDescription(val start: Int, val format: IrcFormat) {
+  private class FormatDescription<U : IrcFormat>(val start: Int, val format: U) {
 
     fun apply(editable: SpannableStringBuilder, end: Int) {
       format.applyTo(editable, start, end)
@@ -298,53 +300,33 @@ class IrcFormatDeserializer(private val context: Context) {
     override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
       editable.setSpan(IrcItalicSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
-
-    override fun id(): Byte {
-      return CODE_ITALIC.toByte()
-    }
   }
 
   private class UnderlineIrcFormat : IrcFormat {
     override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
       editable.setSpan(IrcUnderlineSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
-
-    override fun id(): Byte {
-      return CODE_UNDERLINE.toByte()
-    }
   }
 
   private class StrikethroughIrcFormat : IrcFormat {
     override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
       editable.setSpan(IrcStrikethroughSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
-
-    override fun id(): Byte {
-      return CODE_STRIKETHROUGH.toByte()
-    }
   }
 
   private class MonospaceIrcFormat : IrcFormat {
     override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
       editable.setSpan(IrcMonospaceSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
-
-    override fun id(): Byte {
-      return CODE_MONOSPACE.toByte()
-    }
   }
 
   private class BoldIrcFormat : IrcFormat {
     override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
       editable.setSpan(IrcBoldSpan(), from, to, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
     }
-
-    override fun id(): Byte {
-      return CODE_BOLD.toByte()
-    }
   }
 
-  private inner class ColorHexFormat(val color: Int) : IrcFormat {
+  private inner class HexIrcFormat(val color: Int) : IrcFormat {
 
     override fun applyTo(editable: SpannableStringBuilder, from: Int, to: Int) {
       editable.setSpan(
@@ -352,10 +334,6 @@ class IrcFormatDeserializer(private val context: Context) {
         Spanned.SPAN_INCLUSIVE_EXCLUSIVE
       )
     }
-
-    override fun id(): Byte {
-      return CODE_HEXCOLOR.toByte()
-    }
   }
 
   private inner class ColorIrcFormat(val foreground: Byte, val background: Byte) : IrcFormat {
@@ -378,10 +356,6 @@ class IrcFormatDeserializer(private val context: Context) {
     fun copySwapped(): ColorIrcFormat {
       return ColorIrcFormat(background, foreground)
     }
-
-    override fun id(): Byte {
-      return CODE_COLOR.toByte()
-    }
   }
 
   companion object {
@@ -465,14 +439,5 @@ class IrcFormatDeserializer(private val context: Context) {
       }
       return start + i
     }
-
-    private fun fromId(id: Char) = when (id) {
-      CODE_BOLD          -> BoldIrcFormat()
-      CODE_ITALIC        -> ItalicIrcFormat()
-      CODE_UNDERLINE     -> UnderlineIrcFormat()
-      CODE_STRIKETHROUGH -> StrikethroughIrcFormat()
-      CODE_MONOSPACE     -> MonospaceIrcFormat()
-      else               -> null
-    }
   }
 }
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt
index ced8e7699..db0feed9c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/util/irc/format/IrcFormatSerializer.kt
@@ -105,6 +105,7 @@ class IrcFormatSerializer internal constructor(private val context: Context) {
     }
 
     fun writeReset() {
+      println("reset")
       out.append(CODE_RESET)
     }
 
@@ -139,8 +140,8 @@ class IrcFormatSerializer internal constructor(private val context: Context) {
 
         when (aStyle) {
           is StyleSpan           -> {
-            afterBold = (aStyle.style and Typeface.BOLD != 0)
-            afterItalic = (aStyle.style and Typeface.ITALIC != 0)
+            afterBold = afterBold || aStyle.style and Typeface.BOLD != 0
+            afterItalic = afterItalic || aStyle.style and Typeface.ITALIC != 0
           }
           is UnderlineSpan       -> afterUnderline = true
           is StrikethroughSpan   -> afterStrikethrough = true
@@ -199,7 +200,7 @@ class IrcFormatSerializer internal constructor(private val context: Context) {
       i = next
     }
 
-    if (bold || italic || underline || background != null || foreground != null)
+    if (bold || italic || underline || strikethrough || monospace || background != null || foreground != null)
       writeReset()
   }
 
diff --git a/app/src/main/res/drawable/ic_format_fill.xml b/app/src/main/res/drawable/ic_format_background.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_format_fill.xml
rename to app/src/main/res/drawable/ic_format_background.xml
diff --git a/app/src/main/res/drawable/ic_format_clear.xml b/app/src/main/res/drawable/ic_format_clear.xml
new file mode 100644
index 000000000..c29344e8e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_format_clear.xml
@@ -0,0 +1,10 @@
+<!-- drawable/format_clear.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="24dp"
+  android:height="24dp"
+  android:viewportHeight="24"
+  android:viewportWidth="24">
+  <path
+    android:fillColor="#000"
+    android:pathData="M6,5V5.18L8.82,8H11.22L10.5,9.68L12.6,11.78L14.21,8H20V5H6M3.27,5L2,6.27L8.97,13.24L6.5,19H9.5L11.07,15.34L16.73,21L18,19.73L3.55,5.27L3.27,5Z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_format_paint.xml b/app/src/main/res/drawable/ic_format_foreground.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_format_paint.xml
rename to app/src/main/res/drawable/ic_format_foreground.xml
diff --git a/app/src/main/res/drawable/ic_format_monospace.xml b/app/src/main/res/drawable/ic_format_monospace.xml
new file mode 100644
index 000000000..b2574f467
--- /dev/null
+++ b/app/src/main/res/drawable/ic_format_monospace.xml
@@ -0,0 +1,10 @@
+<!-- drawable/code_tags.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="24dp"
+  android:height="24dp"
+  android:viewportHeight="24"
+  android:viewportWidth="24">
+  <path
+    android:fillColor="#000"
+    android:pathData="M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_format_strikethrough.xml b/app/src/main/res/drawable/ic_format_strikethrough.xml
new file mode 100644
index 000000000..4a9ace4d4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_format_strikethrough.xml
@@ -0,0 +1,10 @@
+<!-- drawable/format_strikethrough_variant.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+  android:width="24dp"
+  android:height="24dp"
+  android:viewportHeight="24"
+  android:viewportWidth="24">
+  <path
+    android:fillColor="#000"
+    android:pathData="M23,12V14H18.61C19.61,16.14 19.56,22 12.38,22C4.05,22.05 4.37,15.5 4.37,15.5L8.34,15.55C8.37,18.92 11.5,18.92 12.12,18.88C12.76,18.83 15.15,18.84 15.34,16.5C15.42,15.41 14.32,14.58 13.12,14H1V12H23M19.41,7.89L15.43,7.86C15.43,7.86 15.6,5.09 12.15,5.08C8.7,5.06 9,7.28 9,7.56C9.04,7.84 9.34,9.22 12,9.88H5.71C5.71,9.88 2.22,3.15 10.74,2C19.45,0.8 19.43,7.91 19.41,7.89Z" />
+</vector>
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw720dp-land/activity_main.xml b/app/src/main/res/layout-sw720dp-land/activity_main.xml
index cb1aeeba4..7af3ba7a1 100644
--- a/app/src/main/res/layout-sw720dp-land/activity_main.xml
+++ b/app/src/main/res/layout-sw720dp-land/activity_main.xml
@@ -1,7 +1,7 @@
 <android.support.v4.widget.DrawerLayout 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"
-  android:id="@+id/drawerLayout"
+  android:id="@+id/drawer_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:fitsSystemWindows="true">
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index b7d6dc16c..6df810bd3 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,7 +1,7 @@
 <android.support.v4.widget.DrawerLayout 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"
-  android:id="@+id/drawerLayout"
+  android:id="@+id/drawer_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:fitsSystemWindows="true">
diff --git a/app/src/main/res/layout/layout_editor.xml b/app/src/main/res/layout/layout_editor.xml
index f5ab7962b..a8bc035af 100644
--- a/app/src/main/res/layout/layout_editor.xml
+++ b/app/src/main/res/layout/layout_editor.xml
@@ -25,7 +25,7 @@
       android:hint="@string/label_placeholder"
       android:imeOptions="flagNoExtractUi"
       android:inputType="textCapSentences|textAutoCorrect|textShortMessage"
-      android:minHeight="?actionBarSize"
+      android:minHeight="?attr/actionBarSize"
       android:paddingBottom="8dp"
       android:paddingLeft="20dp"
       android:paddingRight="20dp"
@@ -53,6 +53,7 @@
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:background="?attr/colorBackgroundCard"
+    android:theme="?attr/formatBarTheme"
     app:layout_constraintBottom_toBottomOf="parent">
 
     <android.support.v7.widget.Toolbar
@@ -62,7 +63,7 @@
 
       <android.support.v7.widget.ActionMenuView
         android:id="@+id/formatting_menu"
-        android:layout_width="wrap_content"
+        android:layout_width="match_parent"
         android:layout_height="?attr/actionBarSize" />
 
     </android.support.v7.widget.Toolbar>
diff --git a/app/src/main/res/layout/layout_slider.xml b/app/src/main/res/layout/layout_slider.xml
index 6d4a3b7a4..d9459514e 100644
--- a/app/src/main/res/layout/layout_slider.xml
+++ b/app/src/main/res/layout/layout_slider.xml
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <com.sothree.slidinguppanel.SlidingUpPanelLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
+  android:id="@+id/history_panel"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   android:gravity="bottom"
diff --git a/app/src/main/res/layout/layout_toolbar.xml b/app/src/main/res/layout/layout_toolbar.xml
index ae3f32270..ff2a77747 100644
--- a/app/src/main/res/layout/layout_toolbar.xml
+++ b/app/src/main/res/layout/layout_toolbar.xml
@@ -27,7 +27,7 @@
     </android.support.v7.widget.Toolbar>
 
     <de.kuschku.quasseldroid_ng.util.ui.MaterialContentLoadingProgressBar
-      android:id="@+id/progressBar"
+      android:id="@+id/progress_bar"
       style="@style/Widget.MaterialProgressBar.ProgressBar.Horizontal.NoPadding"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
diff --git a/app/src/main/res/menu/editor.xml b/app/src/main/res/menu/editor.xml
new file mode 100644
index 000000000..f50026af7
--- /dev/null
+++ b/app/src/main/res/menu/editor.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto">
+  <item
+    android:id="@+id/format_bold"
+    android:icon="@drawable/ic_format_bold"
+    android:title="@string/label_bold"
+    app:showAsAction="always" />
+  <item
+    android:id="@+id/format_italic"
+    android:icon="@drawable/ic_format_italic"
+    android:title="@string/label_italic"
+    app:showAsAction="always" />
+  <item
+    android:id="@+id/format_underline"
+    android:icon="@drawable/ic_format_underline"
+    android:title="@string/label_underline"
+    app:showAsAction="always" />
+  <item
+    android:id="@+id/format_strikethrough"
+    android:icon="@drawable/ic_format_strikethrough"
+    android:title="@string/label_strikethrough"
+    app:showAsAction="always" />
+  <item
+    android:id="@+id/format_monospace"
+    android:icon="@drawable/ic_format_monospace"
+    android:title="@string/label_monospace"
+    app:showAsAction="always" />
+  <!--
+  <item
+    android:id="@+id/format_foreground"
+    android:icon="@drawable/ic_format_foreground"
+    android:title="@string/label_foreground"
+    app:showAsAction="always" />
+  <item
+    android:id="@+id/format_background"
+    android:icon="@drawable/ic_format_background"
+    android:title="@string/label_background"
+    app:showAsAction="always" />
+  -->
+  <item
+    android:id="@+id/format_clear"
+    android:icon="@drawable/ic_format_clear"
+    android:title="@string/label_clear_formatting"
+    app:showAsAction="always" />
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/menu/input_panel.xml b/app/src/main/res/menu/input_panel.xml
new file mode 100644
index 000000000..af252a356
--- /dev/null
+++ b/app/src/main/res/menu/input_panel.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+  xmlns:app="http://schemas.android.com/apk/res-auto">
+  <item
+    android:id="@+id/input_history"
+    android:icon="@drawable/ic_history"
+    android:title="@string/label_input_history"
+    app:showAsAction="always" />
+</menu>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c20443db3..1f0949e22 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -40,4 +40,12 @@
   <string name="notification_channel_highlight_title">Highlight</string>
 
   <string name="buffer_delete_confirmation">Do you want to delete this buffer permanently?</string>
+  <string name="label_bold">Bold</string>
+  <string name="label_italic">Italic</string>
+  <string name="label_strikethrough">Strikethrough</string>
+  <string name="label_underline">Underline</string>
+  <string name="label_monospace">Monospace</string>
+  <string name="label_foreground">Foreground</string>
+  <string name="label_background">Background</string>
+  <string name="label_clear_formatting">Clear Formatting</string>
 </resources>
diff --git a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt
index ae2286048..582a41b8b 100644
--- a/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt
+++ b/lib/src/main/java/de/kuschku/libquassel/protocol/primitive/serializer/StringSerializer.kt
@@ -10,7 +10,7 @@ import java.nio.charset.CharsetDecoder
 import java.nio.charset.CharsetEncoder
 
 abstract class StringSerializer(
-  private val encoder: CharsetEncoder,
+  private var encoder: CharsetEncoder,
   private val decoder: CharsetDecoder,
   private val trailingNullBytes: Int
 ) : Serializer<String?> {
@@ -51,6 +51,7 @@ abstract class StringSerializer(
         val charBuffer = charBuffer(data.length)
         charBuffer.put(data)
         charBuffer.flip()
+        encoder = encoder.charset().newEncoder()
         val byteBuffer = encoder.encode(charBuffer)
         IntSerializer.serialize(buffer, byteBuffer.remaining() + trailingNullBytes, features)
         buffer.put(byteBuffer)
@@ -70,6 +71,7 @@ abstract class StringSerializer(
         val charBuffer = charBuffer(data.length)
         charBuffer.put(data)
         charBuffer.flip()
+        encoder = encoder.charset().newEncoder()
         return encoder.encode(charBuffer)
       }
     } catch (e: Throwable) {
-- 
GitLab