Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regex filters panel #29

Merged
merged 5 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 64 additions & 2 deletions gui/proxy-tool/src/main/kotlin/net/rsprox/gui/FiltersSidePanel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import com.formdev.flatlaf.extras.components.FlatSeparator
import com.formdev.flatlaf.extras.components.FlatTabbedPane
import com.formdev.flatlaf.extras.components.FlatTriStateCheckBox
import net.miginfocom.swing.MigLayout
import net.rsprox.gui.components.RegexFilterPanel
import net.rsprox.gui.dialogs.Dialogs
import net.rsprox.proxy.ProxyService
import net.rsprox.shared.StreamDirection
import net.rsprox.shared.filters.PropertyFilter
import net.rsprox.shared.filters.ProtCategory
import net.rsprox.shared.filters.RegexFilter
import java.awt.BorderLayout
import java.awt.event.ActionListener
import java.awt.event.FocusEvent
Expand Down Expand Up @@ -49,6 +51,11 @@ public class FiltersSidePanel(
private val checkboxes = hashMapOf<PropertyFilter, JCheckBox>()
private val incomingPanel = FiltersPanel(StreamDirection.SERVER_TO_CLIENT)
private val outgoingPanel = FiltersPanel(StreamDirection.CLIENT_TO_SERVER)
private val regexPanel = JPanel().apply {
layout = BorderLayout()
border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
}

private val searchBox = JTextField(SEARCH)

init {
Expand All @@ -59,6 +66,7 @@ public class FiltersSidePanel(
proxyService.filterSetStore.setActive(presetsBox.selectedIndex)
updateButtonState()
updateFilterState()
buildRegexPanel()
}

createButton.addActionListener {
Expand Down Expand Up @@ -182,6 +190,7 @@ public class FiltersSidePanel(
val tabbedGroup = FlatTabbedPane()
tabbedGroup.addTab("Incoming", incomingPanel.wrapWithBorderlessScrollPane())
tabbedGroup.addTab("Outgoing", outgoingPanel.wrapWithBorderlessScrollPane())
tabbedGroup.addTab("Regex", regexPanel.wrapWithBorderlessScrollPane())

tabbedGroup.addMouseListener(
object : MouseAdapter() {
Expand Down Expand Up @@ -210,6 +219,57 @@ public class FiltersSidePanel(
updateFilterState()
}

private fun buildRegexPanel() {
regexPanel.removeAll()

val regexFiltersContainer = JPanel()
regexFiltersContainer.layout = MigLayout("ins 5", "[grow, fill]", "[]")
regexFiltersContainer.border = BorderFactory.createEmptyBorder(0, 0, 0, 0)
regexPanel.add(regexFiltersContainer, BorderLayout.CENTER)

val actionsPanel = JPanel(BorderLayout())
actionsPanel.border = BorderFactory.createEmptyBorder(5, 5, 5, 5)

// Add the title label.
val regexLabel = FlatLabel().apply {
text = "Regex Filters"
labelType = FlatLabel.LabelType.large
isEnabled = presetsBox.selectedIndex != 0
}
actionsPanel.add(regexLabel, BorderLayout.WEST)

// Add the add new filter button.
actionsPanel.add(FlatButton().apply {
icon = AppIcons.Add
toolTipText = "Add new regex filter"
addActionListener {
val filterStore = proxyService.filterSetStore.getActive()
val regexFilter = RegexFilter("prot_name", Regex(""), true)
filterStore.addRegexFilter(regexFilter)
regexFiltersContainer.addRegexFilterPanel(regexFilter)

regexPanel.revalidate()
regexPanel.repaint()
}
isEnabled = presetsBox.selectedIndex != 0
}, BorderLayout.EAST)
regexPanel.add(actionsPanel, BorderLayout.NORTH)

// Add the stored regex filters.
val active = proxyService.filterSetStore.getActive()
active.getRegexFilters().forEach { regexFilter ->
regexFiltersContainer.addRegexFilterPanel(regexFilter)
}

revalidate()
}

private fun JPanel.addRegexFilterPanel(filter: RegexFilter) {
val filterStore = proxyService.filterSetStore.getActive()
val regexFilterPanel = RegexFilterPanel(filterStore, filter)
add(regexFilterPanel, "wrap", 0)
}

private fun updateFilterState() {
val active = proxyService.filterSetStore.getActive()
val selectedIndex = presetsBox.selectedIndex
Expand Down Expand Up @@ -458,10 +518,12 @@ public class FiltersSidePanel(
private companion object {
private const val SEARCH: String = "Search filters..."

private fun JComponent.wrapWithBorderlessScrollPane() =
private fun JComponent.wrapWithBorderlessScrollPane(
verticalPolicy: Int = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS
) =
JScrollPane(this).apply {
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
verticalScrollBarPolicy = ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS
verticalScrollBarPolicy = verticalPolicy
verticalScrollBar.unitIncrement = 16

border = null
Expand Down
2 changes: 2 additions & 0 deletions gui/proxy-tool/src/main/kotlin/net/rsprox/gui/ProxyToolGui.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public fun main() {
UIManager.put("Tree.leafIcon", emptyIcon)
UIManager.put("Tree.openIcon", emptyIcon)
UIManager.put("Tree.closedIcon", emptyIcon)
UIManager.put("Component.focusWidth", 0)
UIManager.put("Component.innerOutlineWidth", 0)

val app = App()
app.init()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package net.rsprox.gui.components

import com.formdev.flatlaf.FlatClientProperties
import com.formdev.flatlaf.extras.components.FlatButton
import com.formdev.flatlaf.extras.components.FlatButton.ButtonType
import com.formdev.flatlaf.extras.components.FlatCheckBox
import com.formdev.flatlaf.extras.components.FlatLabel
import com.formdev.flatlaf.extras.components.FlatTextField
import net.miginfocom.swing.MigLayout
import net.rsprox.gui.AppIcons
import net.rsprox.shared.filters.PropertyFilterSet
import net.rsprox.shared.filters.RegexFilter
import java.awt.event.FocusAdapter
import java.awt.event.FocusEvent
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.util.regex.PatternSyntaxException
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JSeparator
import javax.swing.JTextField
import javax.swing.event.DocumentEvent
import javax.swing.event.DocumentListener
import javax.swing.undo.UndoManager

public class RegexFilterPanel(
private val filterSet: PropertyFilterSet,
private var currentFilter: RegexFilter
) : JPanel() {

private val protNameLabel = FlatLabel()
private val regexTextField = FlatTextField()
private val perLineCheckbox = FlatCheckBox()

init {
layout = MigLayout("gap 5", "[grow, fill][]", "[][][]")
putClientProperty(FlatClientProperties.STYLE, "background: darken(${'$'}Panel.background,2%)")

// Add a label for the filter name.
protNameLabel.text = currentFilter.protName
protNameLabel.toolTipText = "The name of the filter."
installNameEditor(protNameLabel)
add(protNameLabel)

// Add a delete button to remove the filter.
add(FlatButton().apply {
toolTipText = "Delete"
icon = AppIcons.Delete
buttonType = ButtonType.borderless
addActionListener {
// Remove the filter from the filter set.
filterSet.removeRegexFilter(currentFilter)

// Remove the panel from the parent.
val regexFilterPanel = this@RegexFilterPanel
val parent = regexFilterPanel.parent
parent.remove(regexFilterPanel)

// Revalidate and repaint the parent.
parent.revalidate()
parent.repaint()
}
}, "wrap")

add(JSeparator(), "span, wrap")

// Add a text field for the regular expression.
regexTextField.toolTipText = "The regular expression to match."
regexTextField.placeholderText = "Regular expression"
regexTextField.addActionListener { regexTextField.transferFocus() }
regexTextField.document.addDocumentListener(object : DocumentListener {
override fun insertUpdate(e: DocumentEvent) {
regexUpdated()
}

override fun removeUpdate(e: DocumentEvent) {
regexUpdated()
}

override fun changedUpdate(e: DocumentEvent) {
regexUpdated()
}
})

// Add undo/redo support to the text field.
val undoManager = UndoManager()
regexTextField.document.addUndoableEditListener(undoManager)
regexTextField.addKeyListener(UndoRedoKeyListener(undoManager))

// Set the columns to 1 to prevent the text field from growing and resizing the parent.
regexTextField.columns = 1

add(regexTextField)

// Add a checkbox for matching per line.
perLineCheckbox.toolTipText = "Whether to match per line of output."
perLineCheckbox.addActionListener { saveFilter() }
add(perLineCheckbox, "wrap")

// Set the initial values based on the filter.
protNameLabel.text = currentFilter.protName
regexTextField.text = currentFilter.regex.pattern
perLineCheckbox.isSelected = currentFilter.perLine
}

private fun installNameEditor(label: FlatLabel) {
label.addMouseListener(object : MouseAdapter() {

override fun mouseClicked(e: MouseEvent) {
if (e.clickCount == 2) {
val editor = FlatTextField()
editor.text = label.text
editor.placeholderText = label.text
editor.border = null
// do not let it grow over the parent
editor.selectAll()

remove(label)
add(editor, 0)

revalidate()
repaint()

editor.addActionListener {
if (editor.text.isNotBlank()) {
label.text = editor.text
}
swapEditorWithLabel(editor, label)
}
editor.addFocusListener(object : FocusAdapter() {
override fun focusLost(e: FocusEvent) {
swapEditorWithLabel(editor, label)
}
})
editor.requestFocusInWindow()
}
}
})
}

private fun saveFilter() {
val oldRegexFilter = currentFilter
val newRegexFilter = RegexFilter(
protName = protNameLabel.text,
regex = if (validateRegex(regexTextField.text)) Regex(regexTextField.text) else oldRegexFilter.regex,
perLine = perLineCheckbox.isSelected
)
currentFilter = newRegexFilter
filterSet.replaceRegexFilter(oldRegexFilter, newRegexFilter)
}

private fun swapEditorWithLabel(editor: JTextField, label: JLabel) {
// Sync the label text with the editor text.
label.text = editor.text
saveFilter()

// Remove the editor and add the label back.
remove(editor)
add(label, 0)

// Revalidate and repaint the panel.
revalidate()
repaint()
}

private fun regexUpdated() {
if (validateRegex(regexTextField.text)) {
regexTextField.outline = null
saveFilter()
} else {
regexTextField.outline = "error"
}
}

private companion object {
private fun validateRegex(regex: String): Boolean {
return try {
Regex(regex)
true
} catch (_: PatternSyntaxException) {
false
}
}
}

private class UndoRedoKeyListener(private val undoManager: UndoManager) : KeyAdapter() {
override fun keyPressed(e: KeyEvent) {
if (e.isControlDown) {
when {
e.keyCode == KeyEvent.VK_Z -> {
if (undoManager.canUndo()) {
undoManager.undo()
}
}
e.keyCode == KeyEvent.VK_Y -> {
if (undoManager.canRedo()) {
undoManager.redo()
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ public class DefaultPropertyFilterSet(
save()
}

override fun replaceRegexFilter(oldRegexFilter: RegexFilter, newRegexFilter: RegexFilter) {
val index = regexFilters.indexOf(oldRegexFilter)
if (index == -1) return
regexFilters[index] = newRegexFilter
save()
}

override fun clearRegexFilters() {
regexFilters.clear()
save()
Expand Down Expand Up @@ -144,6 +151,7 @@ public class DefaultPropertyFilterSet(
builder.append("version=").append(propertyFilterSet.version).appendLine()
builder.append("creationtime=").append(propertyFilterSet.creationTime).appendLine()
builder.append("name=").append(propertyFilterSet.name).appendLine()
builder.append("active=").append(propertyFilterSet.active).appendLine()
for ((k, v) in propertyFilterSet.filters) {
builder
.append(k)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public class UnmodifiablePropertyFilterSet : PropertyFilterSet {
override fun removeRegexFilter(regexFilter: RegexFilter) {
}

override fun replaceRegexFilter(oldRegexFilter: RegexFilter, newRegexFilter: RegexFilter) {
}

override fun clearRegexFilters() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ public interface PropertyFilterSet {

public fun removeRegexFilter(regexFilter: RegexFilter)

public fun replaceRegexFilter(oldRegexFilter: RegexFilter, newRegexFilter: RegexFilter)

public fun clearRegexFilters()
}