c292db2e1fe558d13a49db1546327d53f334c1eb
[idea/community.git] / platform / collaboration-tools / src / com / intellij / collaboration / ui / codereview / list / search / ReviewListSearchPanelFactory.kt
1 // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2 package com.intellij.collaboration.ui.codereview.list.search
3
4 import com.intellij.collaboration.messages.CollaborationToolsBundle
5 import com.intellij.collaboration.ui.codereview.list.search.ChooserPopupUtil.showAndAwaitListSubmission
6 import com.intellij.icons.AllIcons
7 import com.intellij.ide.DataManager
8 import com.intellij.openapi.actionSystem.*
9 import com.intellij.openapi.project.DumbAwareAction
10 import com.intellij.openapi.ui.popup.JBPopupFactory
11 import com.intellij.ui.*
12 import com.intellij.ui.components.GradientViewport
13 import com.intellij.ui.components.JBScrollPane
14 import com.intellij.ui.components.JBThinOverlappingScrollBar
15 import com.intellij.ui.components.panels.HorizontalLayout
16 import com.intellij.util.ui.InlineIconButton
17 import com.intellij.util.ui.JBUI
18 import kotlinx.coroutines.CoroutineScope
19 import kotlinx.coroutines.flow.update
20 import kotlinx.coroutines.launch
21 import org.jetbrains.annotations.Nls
22 import java.awt.Adjustable
23 import java.awt.BorderLayout
24 import java.awt.event.ActionListener
25 import javax.swing.JComponent
26 import javax.swing.JPanel
27 import javax.swing.ScrollPaneConstants
28
29 abstract class ReviewListSearchPanelFactory<S : ReviewListSearchValue, Q : ReviewListQuickFilter<S>, VM : ReviewListSearchPanelViewModel<S, Q>>(
30   protected val vm: VM
31 ) {
32
33   fun create(viewScope: CoroutineScope): JComponent {
34     val searchField = ReviewListSearchTextFieldFactory(vm.queryState).create(viewScope, chooseFromHistory = { point ->
35       val value = JBPopupFactory.getInstance()
36         .createPopupChooserBuilder(vm.getSearchHistory().reversed())
37         .setRenderer(SimpleListCellRenderer.create { label, value, _ ->
38           label.text = getShortText(value)
39         })
40         .createPopup()
41         .showAndAwaitListSubmission<S>(point)
42       if (value != null) {
43         vm.searchState.update { value }
44       }
45     })
46
47     val filters = createFilters(viewScope)
48
49     val filtersPanel = JPanel(HorizontalLayout(4)).apply {
50       isOpaque = false
51       filters.forEach { add(it, HorizontalLayout.LEFT) }
52     }.let {
53       ScrollPaneFactory.createScrollPane(it, true).apply {
54         viewport = GradientViewport(it, JBUI.insets(0, 10), false)
55
56         verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER
57         horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS
58         horizontalScrollBar = JBThinOverlappingScrollBar(Adjustable.HORIZONTAL)
59
60         ClientProperty.put(this, JBScrollPane.FORCE_HORIZONTAL_SCROLL, true)
61       }
62     }
63
64     val quickFilterButton = QuickFilterButtonFactory().create(viewScope, vm.quickFilters)
65
66     val filterPanel = JPanel(BorderLayout()).apply {
67       border = JBUI.Borders.emptyTop(10)
68       isOpaque = false
69       add(quickFilterButton, BorderLayout.WEST)
70       add(filtersPanel, BorderLayout.CENTER)
71     }
72
73     val searchPanel = JPanel(BorderLayout()).apply {
74       border = JBUI.Borders.compound(IdeBorderFactory.createBorder(SideBorder.BOTTOM), JBUI.Borders.empty(8, 10, 0, 10))
75       add(searchField, BorderLayout.CENTER)
76       add(filterPanel, BorderLayout.SOUTH)
77     }
78
79     return searchPanel
80   }
81
82   protected abstract fun getShortText(searchValue: S): @Nls String
83
84   protected abstract fun createFilters(viewScope: CoroutineScope): List<JComponent>
85
86   protected abstract fun Q.getQuickFilterTitle(): @Nls String
87
88   private inner class QuickFilterButtonFactory {
89
90     fun create(viewScope: CoroutineScope, quickFilters: List<Q>): JComponent {
91       val button = InlineIconButton(Companion.FILTER_ICON.originalIcon).apply {
92         border = JBUI.Borders.empty(3)
93       }.also {
94         it.actionListener = ActionListener { _ ->
95           showQuickFiltersPopup(it, quickFilters)
96         }
97       }
98
99       viewScope.launch {
100         vm.searchState.collect {
101           button.icon = if (it.filterCount == 0) Companion.FILTER_ICON.originalIcon else Companion.FILTER_ICON.getLiveIndicatorIcon(true)
102         }
103       }
104
105       return button
106     }
107
108     private fun showQuickFiltersPopup(parentComponent: JComponent, quickFilters: List<Q>) {
109       val quickFiltersActions =
110         quickFilters.map { QuickFilterAction(it.getQuickFilterTitle(), it.filter) } +
111         Separator() +
112         ClearFiltersAction()
113
114
115       JBPopupFactory.getInstance()
116         .createActionGroupPopup(CollaborationToolsBundle.message("review.list.filter.quick.title"), DefaultActionGroup(quickFiltersActions),
117                                 DataManager.getInstance().getDataContext(parentComponent),
118                                 JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
119                                 false)
120         .showUnderneathOf(parentComponent)
121     }
122
123     private inner class QuickFilterAction(name: @Nls String, private val search: S)
124       : DumbAwareAction(name), Toggleable {
125       override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
126       override fun update(e: AnActionEvent) = Toggleable.setSelected(e.presentation, vm.searchState.value == search)
127       override fun actionPerformed(e: AnActionEvent) = vm.searchState.update { search }
128     }
129
130     private inner class ClearFiltersAction
131       : DumbAwareAction(CollaborationToolsBundle.message("review.list.filter.quick.clear", vm.searchState.value.filterCount)) {
132
133       override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
134
135       override fun update(e: AnActionEvent) {
136         e.presentation.isEnabledAndVisible = vm.searchState.value.filterCount > 0
137       }
138
139       override fun actionPerformed(e: AnActionEvent) = vm.searchState.update { vm.emptySearch }
140     }
141   }
142
143   companion object {
144     private val FILTER_ICON: BadgeIconSupplier = BadgeIconSupplier(AllIcons.General.Filter)
145   }
146 }