From 4d94af26a19212e7beb0e1a8237b22ffa5cdc574 Mon Sep 17 00:00:00 2001
From: Janne Koschinski <janne@kuschku.de>
Date: Sun, 25 Feb 2018 00:56:33 +0100
Subject: [PATCH] Allow selecting buffers

---
 .../ui/chat/buffers/BufferListAdapter.kt      | 100 +++++++++++++++---
 .../chat/buffers/BufferViewConfigFragment.kt  |  49 +++++++--
 .../res/drawable-v21/bg_menuitem_dark.xml     |  11 ++
 .../res/drawable-v21/bg_menuitem_light.xml    |  11 ++
 .../main/res/drawable/bg_menuitem_dark.xml    |  13 +++
 .../main/res/drawable/bg_menuitem_light.xml   |  13 +++
 app/src/main/res/layout/widget_buffer.xml     |   2 +-
 app/src/main/res/layout/widget_network.xml    |   2 +-
 app/src/main/res/values/attrs.xml             |   3 +
 app/src/main/res/values/colors.xml            |   3 +
 app/src/main/res/values/themes_base.xml       |  12 +++
 11 files changed, 199 insertions(+), 20 deletions(-)
 create mode 100644 app/src/main/res/drawable-v21/bg_menuitem_dark.xml
 create mode 100644 app/src/main/res/drawable-v21/bg_menuitem_light.xml
 create mode 100644 app/src/main/res/drawable/bg_menuitem_dark.xml
 create mode 100644 app/src/main/res/drawable/bg_menuitem_light.xml

diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt
index bf4dc0cc0..600c78c7c 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferListAdapter.kt
@@ -30,11 +30,14 @@ class BufferListAdapter(
   liveData: LiveData<List<BufferProps>?>,
   runInBackground: (() -> Unit) -> Any,
   runOnUiThread: (Runnable) -> Any,
-  private val clickListener: ((BufferId) -> Unit)? = null
+  private val clickListener: ((BufferId) -> Unit)? = null,
+  private val longClickListener: ((BufferId) -> Unit)? = null
 ) : RecyclerView.Adapter<BufferListAdapter.BufferViewHolder>() {
   var data = mutableListOf<BufferListItem>()
 
-  var collapsedNetworks = MutableLiveData<Set<NetworkId>>()
+  private val collapsedNetworks = MutableLiveData<Set<NetworkId>>()
+
+  val selectedBuffers = MutableLiveData<Set<BufferId>>()
 
   fun expandListener(networkId: NetworkId) {
     if (collapsedNetworks.value.orEmpty().contains(networkId))
@@ -43,14 +46,29 @@ class BufferListAdapter(
       collapsedNetworks.postValue(collapsedNetworks.value.orEmpty() + networkId)
   }
 
+  fun toggleSelection(buffer: BufferId) {
+    val value = selectedBuffers.value.orEmpty()
+    if (value.contains(buffer)) {
+      selectedBuffers.value = value - buffer
+    } else {
+      selectedBuffers.value = value + buffer
+    }
+  }
+
+  fun unselectAll() {
+    selectedBuffers.value = emptySet()
+  }
+
   init {
     collapsedNetworks.value = emptySet()
+    selectedBuffers.value = emptySet()
 
-    liveData.zip(collapsedNetworks).observe(
-      lifecycleOwner, Observer { it: Pair<List<BufferProps>?, Set<NetworkId>>? ->
+    liveData.zip(collapsedNetworks, selectedBuffers).observe(
+      lifecycleOwner, Observer { it: Triple<List<BufferProps>?, Set<NetworkId>, Set<BufferId>>? ->
       runInBackground {
         val list = it?.first ?: emptyList()
         val collapsedNetworks = it?.second ?: emptySet()
+        val selected = it?.third ?: emptySet()
 
         val old: List<BufferListItem> = data
         val new: List<BufferListItem> = list.sortedBy { props ->
@@ -61,7 +79,8 @@ class BufferListAdapter(
             BufferListItem(
               props,
               BufferState(
-                networkExpanded = !collapsedNetworks.contains(props.network.networkId)
+                networkExpanded = !collapsedNetworks.contains(props.network.networkId),
+                selected = selected.contains(props.info.bufferId)
               )
             )
         }.filter { (props, state) ->
@@ -97,25 +116,29 @@ class BufferListAdapter(
       LayoutInflater.from(parent.context).inflate(
         R.layout.widget_buffer, parent, false
       ),
-      clickListener = clickListener
+      clickListener = clickListener,
+      longClickListener = longClickListener
     )
     BufferInfo.Type.QueryBuffer.toInt()   -> BufferViewHolder.QueryBuffer(
       LayoutInflater.from(parent.context).inflate(
         R.layout.widget_buffer, parent, false
       ),
-      clickListener = clickListener
+      clickListener = clickListener,
+      longClickListener = longClickListener
     )
     BufferInfo.Type.GroupBuffer.toInt()   -> BufferViewHolder.GroupBuffer(
       LayoutInflater.from(parent.context).inflate(
         R.layout.widget_buffer, parent, false
       ),
-      clickListener = clickListener
+      clickListener = clickListener,
+      longClickListener = longClickListener
     )
     BufferInfo.Type.StatusBuffer.toInt()  -> BufferViewHolder.StatusBuffer(
       LayoutInflater.from(parent.context).inflate(
         R.layout.widget_network, parent, false
       ),
       clickListener = clickListener,
+      longClickListener = longClickListener,
       expansionListener = ::expandListener
     )
     else                                  -> throw IllegalArgumentException(
@@ -146,7 +169,8 @@ class BufferListAdapter(
   )
 
   data class BufferState(
-    val networkExpanded: Boolean
+    val networkExpanded: Boolean,
+    val selected: Boolean
   )
 
   abstract class BufferViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
@@ -155,6 +179,7 @@ class BufferListAdapter(
     class StatusBuffer(
       itemView: View,
       private val clickListener: ((BufferId) -> Unit)? = null,
+      private val longClickListener: ((BufferId) -> Unit)? = null,
       private val expansionListener: ((NetworkId) -> Unit)? = null
     ) : BufferViewHolder(itemView) {
       @BindView(R.id.status)
@@ -179,6 +204,16 @@ class BufferListAdapter(
             clickListener?.invoke(buffer)
         }
 
+        itemView.setOnLongClickListener {
+          val buffer = bufferId
+          if (buffer != null) {
+            longClickListener?.invoke(buffer)
+            true
+          } else {
+            false
+          }
+        }
+
         status.setOnClickListener {
           val network = networkId
           if (network != null)
@@ -210,6 +245,8 @@ class BufferListAdapter(
           }
         )
 
+        itemView.isSelected = state.selected
+
         if (state.networkExpanded) {
           status.setImageDrawable(itemView.context.getCompatDrawable(R.drawable.ic_chevron_up))
         } else {
@@ -220,7 +257,8 @@ class BufferListAdapter(
 
     class GroupBuffer(
       itemView: View,
-      private val clickListener: ((BufferId) -> Unit)? = null
+      private val clickListener: ((BufferId) -> Unit)? = null,
+      private val longClickListener: ((BufferId) -> Unit)? = null
     ) : BufferViewHolder(itemView) {
       @BindView(R.id.status)
       lateinit var status: ImageView
@@ -249,6 +287,16 @@ class BufferListAdapter(
             clickListener?.invoke(buffer)
         }
 
+        itemView.setOnLongClickListener {
+          val buffer = bufferId
+          if (buffer != null) {
+            longClickListener?.invoke(buffer)
+            true
+          } else {
+            false
+          }
+        }
+
         online = itemView.context.getCompatDrawable(R.drawable.ic_status).mutate()
         offline = itemView.context.getCompatDrawable(R.drawable.ic_status_offline).mutate()
 
@@ -282,6 +330,8 @@ class BufferListAdapter(
           }
         )
 
+        itemView.isSelected = state.selected
+
         description.visibleIf(props.description.isNotBlank())
 
         status.setImageDrawable(
@@ -295,7 +345,8 @@ class BufferListAdapter(
 
     class ChannelBuffer(
       itemView: View,
-      private val clickListener: ((BufferId) -> Unit)? = null
+      private val clickListener: ((BufferId) -> Unit)? = null,
+      private val longClickListener: ((BufferId) -> Unit)? = null
     ) : BufferViewHolder(itemView) {
       @BindView(R.id.status)
       lateinit var status: ImageView
@@ -324,6 +375,16 @@ class BufferListAdapter(
             clickListener?.invoke(buffer)
         }
 
+        itemView.setOnLongClickListener {
+          val buffer = bufferId
+          if (buffer != null) {
+            longClickListener?.invoke(buffer)
+            true
+          } else {
+            false
+          }
+        }
+
         online = itemView.context.getCompatDrawable(R.drawable.ic_status_channel).mutate()
         offline = itemView.context.getCompatDrawable(R.drawable.ic_status_channel_offline).mutate()
 
@@ -357,6 +418,8 @@ class BufferListAdapter(
           }
         )
 
+        itemView.isSelected = state.selected
+
         description.visibleIf(props.description.isNotBlank())
 
         status.setImageDrawable(
@@ -370,7 +433,8 @@ class BufferListAdapter(
 
     class QueryBuffer(
       itemView: View,
-      private val clickListener: ((BufferId) -> Unit)? = null
+      private val clickListener: ((BufferId) -> Unit)? = null,
+      private val longClickListener: ((BufferId) -> Unit)? = null
     ) : BufferViewHolder(itemView) {
       @BindView(R.id.status)
       lateinit var status: ImageView
@@ -400,6 +464,16 @@ class BufferListAdapter(
             clickListener?.invoke(buffer)
         }
 
+        itemView.setOnLongClickListener {
+          val buffer = bufferId
+          if (buffer != null) {
+            longClickListener?.invoke(buffer)
+            true
+          } else {
+            false
+          }
+        }
+
         online = itemView.context.getCompatDrawable(R.drawable.ic_status).mutate()
         away = itemView.context.getCompatDrawable(R.drawable.ic_status).mutate()
         offline = itemView.context.getCompatDrawable(R.drawable.ic_status_offline).mutate()
@@ -435,6 +509,8 @@ class BufferListAdapter(
           }
         )
 
+        itemView.isSelected = state.selected
+
         description.visibleIf(props.description.isNotBlank())
 
         status.setImageDrawable(
diff --git a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt
index a1f722520..6e3b95730 100644
--- a/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt
+++ b/app/src/main/java/de/kuschku/quasseldroid_ng/ui/chat/buffers/BufferViewConfigFragment.kt
@@ -3,9 +3,7 @@ package de.kuschku.quasseldroid_ng.ui.chat.buffers
 import android.arch.lifecycle.ViewModelProviders
 import android.os.Bundle
 import android.support.v7.widget.*
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
+import android.view.*
 import android.widget.AdapterView
 import butterknife.BindView
 import butterknife.ButterKnife
@@ -44,6 +42,30 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   private var ircFormatDeserializer: IrcFormatDeserializer? = null
   private lateinit var appearanceSettings: AppearanceSettings
 
+  private var isInActionMode = false
+
+  private val actionModeCallback = object : ActionMode.Callback {
+    override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
+      return true
+    }
+
+    override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+      isInActionMode = true
+      return true
+    }
+
+    override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
+      return false
+    }
+
+    override fun onDestroyActionMode(mode: ActionMode?) {
+      isInActionMode = false
+      listAdapter.unselectAll()
+    }
+  }
+
+  private lateinit var listAdapter: BufferListAdapter
+
   override fun onCreate(savedInstanceState: Bundle?) {
     handlerThread.onCreate()
     super.onCreate(savedInstanceState)
@@ -75,7 +97,7 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
       }
     }
 
-    chatList.adapter = BufferListAdapter(
+    listAdapter = BufferListAdapter(
       this,
       viewModel.bufferList.zip(database.filtered().listen(accountId)).map {
         val (data, activityList) = it
@@ -107,8 +129,12 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
       },
       handlerThread::post,
       activity!!::runOnUiThread,
-      clickListener
+      clickListener,
+      longClickListener
     )
+    chatList.adapter = listAdapter
+
+    chatListToolbar.startActionMode(actionModeCallback)
     chatList.layoutManager = LinearLayoutManager(context)
     chatList.itemAnimator = DefaultItemAnimator()
     chatList.setItemViewCacheSize(10)
@@ -121,6 +147,17 @@ class BufferViewConfigFragment : ServiceBoundFragment() {
   }
 
   private val clickListener: ((BufferId) -> Unit)? = {
-    viewModel.setBuffer(it)
+    if (isInActionMode) {
+      longClickListener?.invoke(it)
+    } else {
+      viewModel.setBuffer(it)
+    }
+  }
+
+  private val longClickListener: ((BufferId) -> Unit)? = {
+    if (!isInActionMode) {
+      chatListToolbar.startActionMode(actionModeCallback)
+    }
+    listAdapter.toggleSelection(it)
   }
 }
diff --git a/app/src/main/res/drawable-v21/bg_menuitem_dark.xml b/app/src/main/res/drawable-v21/bg_menuitem_dark.xml
new file mode 100644
index 000000000..6ac08543d
--- /dev/null
+++ b/app/src/main/res/drawable-v21/bg_menuitem_dark.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:state_selected="true">
+    <shape android:shape="rectangle">
+      <solid android:color="@color/ripple_dark" />
+    </shape>
+  </item>
+  <item>
+    <ripple android:color="@color/ripple_dark" />
+  </item>
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable-v21/bg_menuitem_light.xml b/app/src/main/res/drawable-v21/bg_menuitem_light.xml
new file mode 100644
index 000000000..c86946bd1
--- /dev/null
+++ b/app/src/main/res/drawable-v21/bg_menuitem_light.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:state_selected="true">
+    <shape android:shape="rectangle">
+      <solid android:color="@color/ripple_light" />
+    </shape>
+  </item>
+  <item>
+    <ripple android:color="@color/ripple_light" />
+  </item>
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_menuitem_dark.xml b/app/src/main/res/drawable/bg_menuitem_dark.xml
new file mode 100644
index 000000000..cd43fb02f
--- /dev/null
+++ b/app/src/main/res/drawable/bg_menuitem_dark.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:state_selected="true">
+    <shape android:shape="rectangle">
+      <solid android:color="@color/ripple_dark" />
+    </shape>
+  </item>
+  <item android:state_pressed="true">
+    <shape android:shape="rectangle">
+      <solid android:color="@color/ripple_dark" />
+    </shape>
+  </item>
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_menuitem_light.xml b/app/src/main/res/drawable/bg_menuitem_light.xml
new file mode 100644
index 000000000..bdf40bd8a
--- /dev/null
+++ b/app/src/main/res/drawable/bg_menuitem_light.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+  <item android:state_selected="true">
+    <shape android:shape="rectangle">
+      <solid android:color="@color/ripple_light" />
+    </shape>
+  </item>
+  <item android:state_pressed="true">
+    <shape android:shape="rectangle">
+      <solid android:color="@color/ripple_light" />
+    </shape>
+  </item>
+</selector>
\ No newline at end of file
diff --git a/app/src/main/res/layout/widget_buffer.xml b/app/src/main/res/layout/widget_buffer.xml
index d181160f3..b162446df 100644
--- a/app/src/main/res/layout/widget_buffer.xml
+++ b/app/src/main/res/layout/widget_buffer.xml
@@ -3,7 +3,7 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
-  android:foreground="?attr/selectableItemBackgroundBorderless"
+  android:background="?attr/backgroundMenuItem"
   android:minHeight="48dp"
   android:paddingBottom="8dp"
   android:paddingLeft="16dp"
diff --git a/app/src/main/res/layout/widget_network.xml b/app/src/main/res/layout/widget_network.xml
index 89af4b1c5..42128db09 100644
--- a/app/src/main/res/layout/widget_network.xml
+++ b/app/src/main/res/layout/widget_network.xml
@@ -4,7 +4,7 @@
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
-  android:background="?attr/selectableItemBackground"
+  android:background="?attr/backgroundMenuItem"
   android:orientation="vertical">
 
   <View
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index eeb7e7e31..2d2d7219d 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -71,4 +71,7 @@
   <attr name="buttonTheme" format="reference" />
   <attr name="buttonThemeColored" format="reference" />
   <attr name="cardStyle" format="reference" />
+
+  <!-- Menu Items -->
+  <attr name="backgroundMenuItem" format="reference" />
 </resources>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 1b74be2c0..77aae29ab 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -12,4 +12,7 @@
 
   <color name="colorAwayLight">#959595</color>
   <color name="colorAwayDark">#939393</color>
+
+  <color name="ripple_dark">#33ffffff</color>
+  <color name="ripple_light">#1f000000</color>
 </resources>
diff --git a/app/src/main/res/values/themes_base.xml b/app/src/main/res/values/themes_base.xml
index 0506b7d58..6a519ac9a 100644
--- a/app/src/main/res/values/themes_base.xml
+++ b/app/src/main/res/values/themes_base.xml
@@ -4,22 +4,30 @@
     <item name="colorPrimary">@color/colorPrimary</item>
     <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
     <item name="colorAccent">@color/colorAccent</item>
+
+    <item name="backgroundMenuItem">@drawable/bg_menuitem_dark</item>
   </style>
 
   <style name="Theme.AppTheme.Light" parent="Theme.AppCompat.Light.DarkActionBar">
     <item name="colorPrimary">@color/colorPrimary</item>
     <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
     <item name="colorAccent">@color/colorAccent</item>
+
+    <item name="backgroundMenuItem">@drawable/bg_menuitem_light</item>
   </style>
 
   <style name="Theme.AppTheme.NoActionBar" parent="Theme.AppTheme">
     <item name="windowActionBar">false</item>
     <item name="windowNoTitle">true</item>
+
+    <item name="backgroundMenuItem">@drawable/bg_menuitem_dark</item>
   </style>
 
   <style name="Theme.AppTheme.Light.NoActionBar" parent="Theme.AppTheme.Light">
     <item name="windowActionBar">false</item>
     <item name="windowNoTitle">true</item>
+
+    <item name="backgroundMenuItem">@drawable/bg_menuitem_light</item>
   </style>
 
   <style name="Theme.Base.ChatTheme" parent="Theme.AppTheme.NoActionBar" />
@@ -38,6 +46,8 @@
     <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
     <item name="actionBarPopupTheme">@style/Widget.PopupOverlay</item>
 
+    <item name="backgroundMenuItem">@drawable/bg_menuitem_dark</item>
+
     <item name="windowActionModeOverlay">true</item>
 
     <item name="colorTextPrimary">#dedede</item>
@@ -85,6 +95,8 @@
     <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
     <item name="actionBarPopupTheme">@style/Widget.PopupOverlay.Light</item>
 
+    <item name="backgroundMenuItem">@drawable/bg_menuitem_light</item>
+
     <item name="windowActionModeOverlay">true</item>
 
     <item name="colorTextPrimary">#212121</item>
-- 
GitLab