Skip to content

Commit

Permalink
Add InputFilterList for Improved Searchability
Browse files Browse the repository at this point in the history
Improves usability and navigation for scenarios with a large number of choices.

- Implemented a new JComponent, InputFilterList, to enable search functionality.
- Applied InputFilterList in the following cases:
    - When Built-in Job type has more than 10 choices.
    - For Git Parameter type.

Fixes #656
  • Loading branch information
LakeLab committed Dec 23, 2024
1 parent d8fdb77 commit 76ebff3
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
import org.codinjutsu.tools.jenkins.model.JobParameter;
import org.codinjutsu.tools.jenkins.model.JobParameterType;
import org.codinjutsu.tools.jenkins.model.ProjectJob;
import org.codinjutsu.tools.jenkins.view.inputfilter.InputFilterList;
import org.codinjutsu.tools.jenkins.view.parameter.JobParameterComponent;
import org.codinjutsu.tools.jenkins.view.parameter.PasswordComponent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.text.JTextComponent;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

Expand All @@ -32,6 +34,7 @@ public final class JobParameterRenderers {

public static final Icon ERROR_ICON = AllIcons.General.BalloonError;
public static final String MISSING_NAME_LABEL = "<Missing Name>";
public static final int NUMBER_OF_REQUIRING_INPUT_FILTER_LIST = 10;

@NotNull
public static JobParameterComponent<VirtualFile> createFileUpload(JobParameter jobParameter, String defaultValue) {
Expand Down Expand Up @@ -84,6 +87,18 @@ public static JobParameterComponent<String> createCheckBox(JobParameter jobParam
return new JobParameterComponent<>(jobParameter, checkBox, asString(JCheckBox::isSelected));
}

@NotNull
public static JobParameterComponent<String> createInputFilterList(@NotNull JobParameter jobParameter, String defaultValue) {
final String[] choices = jobParameter.getChoices().toArray(new String[0]);
final InputFilterList list = new InputFilterList(defaultValue, List.of(choices),
(text, textToFilter) -> {
if (textToFilter.isEmpty()) return true;
return text.toLowerCase().contains(textToFilter.toLowerCase());
});
return new JobParameterComponent<>(jobParameter, list, InputFilterList::getSelectedItem,
() -> defaultValue != null && list.getSelectedItem() == null);
}

@NotNull
public static JobParameterComponent<String> createComboBox(@NotNull JobParameter jobParameter, String defaultValue) {
final String[] choices = jobParameter.getChoices().toArray(new String[0]);
Expand Down Expand Up @@ -143,6 +158,34 @@ public static JobParameterComponent<String> createComboBoxIfChoicesExists(@NotNu
return renderer.apply(jobParameter, defaultValue);
}

@NotNull
public static JobParameterComponent<String> createComboBoxOrInputFilterListIfChoicesExists(@NotNull JobParameter jobParameter,
String defaultValue) {
final BiFunction<JobParameter, String, JobParameterComponent<String>> renderer;
List<String> choices = jobParameter.getChoices();
if (choices.isEmpty()) {
renderer = JobParameterRenderers::createTextField;
} else if (choices.size() <= NUMBER_OF_REQUIRING_INPUT_FILTER_LIST) {
renderer = JobParameterRenderers::createComboBox;
} else {
renderer = JobParameterRenderers::createInputFilterList;
}
return renderer.apply(jobParameter, defaultValue);
}

@NotNull
public static JobParameterComponent<String> createInputFilterListIfChoicesExists(@NotNull JobParameter jobParameter,
String defaultValue) {
final BiFunction<JobParameter, String, JobParameterComponent<String>> renderer;
List<String> choices = jobParameter.getChoices();
if (choices.isEmpty()) {
renderer = JobParameterRenderers::createTextField;
} else {
renderer = JobParameterRenderers::createInputFilterList;
}
return renderer.apply(jobParameter, defaultValue);
}

@NotNull
public static Function<JobParameter, JobParameterComponent<String>> createGitParameterChoices(
@NotNull ProjectJob projectJob) {
Expand All @@ -162,9 +205,9 @@ public static JobParameterComponent<String> createGitParameterChoices(@NotNull P
.defaultValue(jobParameter.getDefaultValue())
.choices(requestManager.getGitParameterChoices(projectJob.getJob(), jobParameter))
.build();
return createComboBoxIfChoicesExists(gitParameter, defaultValue);
return createInputFilterListIfChoicesExists(gitParameter, defaultValue);
} else {
return createComboBoxIfChoicesExists(jobParameter, defaultValue);
return createInputFilterListIfChoicesExists(jobParameter, defaultValue);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package org.codinjutsu.tools.jenkins.view.inputfilter;

import com.intellij.ui.JBColor;

import javax.swing.*;
import java.awt.*;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;

class HintTextField extends JTextField implements FocusListener {

private final String hint;
private final JBColor hintColor = JBColor.gray;

private Color originalColor;
private boolean showingHint;

public HintTextField(final String hint) {
super(hint);
this.hint = hint;
this.showingHint = true;
this.originalColor = this.getForeground();
super.setForeground(hintColor);
super.addFocusListener(this);
}

@Override
public void setForeground(Color fg) {
originalColor = fg;
super.setForeground(fg);
}

@Override
public void setText(String t) {
super.setText(t);
if (!t.isEmpty()) {
super.setForeground(originalColor);
showingHint = false;
}
}

@Override
public void focusGained(FocusEvent e) {
if (this.getText().isEmpty()) {
super.setForeground(originalColor);
super.setText("");
showingHint = false;
}
}

@Override
public void focusLost(FocusEvent e) {
if (this.getText().isEmpty()) {
super.setForeground(hintColor);
super.setText(hint);
showingHint = true;
}
}

@Override
public String getText() {
return showingHint ? "" : super.getText();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package org.codinjutsu.tools.jenkins.view.inputfilter;

import com.intellij.ui.DocumentAdapter;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.panels.HorizontalLayout;
import com.intellij.util.ui.StatusText;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import java.awt.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.BiPredicate;

import static javax.swing.SwingConstants.TOP;

public class InputFilterList extends JComponent {

private final JTextField filterTextField = new HintTextField("Search");
private final BiPredicate<String, String> userFilter;
private final DefaultListModel<String> model = new DefaultListModel<>();
private final JBList<String> list = new JBList<>(model);
private final JBScrollPane scrollPane = new JBScrollPane(this.list);

private final List<String> itemList;
private final String defaultValue;

private String filteringText = "";

public InputFilterList(String defaultValue, List<String> itemList, BiPredicate<String, String> userFilter) {
this.itemList = itemList;
this.userFilter = userFilter;
this.defaultValue = defaultValue;

setLayout(new HorizontalLayout(0, TOP));

model.addAll(itemList);
list.setSelectedValue(defaultValue, true);

initDefaultEmptyTextIfNeed();
initUIPreferredSize();
initDocumentListener();
initKeyListener();
}

public String getSelectedItem() {
String currentSelectedValue = list.getSelectedValue();
return currentSelectedValue != null ? currentSelectedValue : defaultValue;
}


private void initDefaultEmptyTextIfNeed() {
if (defaultValue != null) {
StatusText emptyText = list.getEmptyText();
emptyText.setShowAboveCenter(false);
emptyText.setText("No result");
emptyText.appendLine(String.format("(Default value : %s)", defaultValue));
}
}

private void initUIPreferredSize() {
scrollPane.setPreferredSize(new Dimension(250, 100));
add(scrollPane);

filterTextField.setPreferredSize(new Dimension(80, filterTextField.getPreferredSize().height));
add(filterTextField);
}

private void initKeyListener() {
filterTextField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_KP_UP) {
moveSelectedIndex(true);
e.consume();
} else if (e.getKeyCode() == KeyEvent.VK_DOWN || e.getKeyCode() == KeyEvent.VK_KP_DOWN) {
moveSelectedIndex(false);
e.consume();
}
}
});
}

private void moveSelectedIndex(boolean isUp) {
int currentIndex = list.getSelectedIndex();
if (currentIndex == -1) return;
if (isUp && currentIndex == 0) return;
if (!isUp && currentIndex == model.size() - 1) return;
list.setSelectedIndex(isUp ? currentIndex - 1 : currentIndex + 1);
list.ensureIndexIsVisible(isUp ? currentIndex - 1 : currentIndex + 1);
}

private void initDocumentListener() {
filterTextField.getDocument().addDocumentListener(new DocumentAdapter() {

@Override
protected void textChanged(@NotNull DocumentEvent e) {
SwingUtilities.invokeLater(() -> {
applyFilter();
filteringText = filterTextField.getText();
});
}
});
}

private void applyFilter() {
if (Objects.equals(filterTextField.getText(), filteringText)) {
return;
}
model.removeAllElements();

final ArrayList<String> filteredList = new ArrayList<>();
boolean hasItem = false;
for (String item : itemList) {
if (userFilter.test(item, filterTextField.getText())) {
hasItem = true;
filteredList.add(item);
}
}

model.addAll(filteredList);
if (hasItem) {
list.setSelectedIndex(0);
list.ensureIndexIsVisible(0);
} else {
list.clearSelection();
}
filterTextField
.setForeground(!hasItem ? JBColor.RED : UIManager.getColor("Label.foreground"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@
import java.util.Optional;
import java.util.function.BiFunction;

import static org.codinjutsu.tools.jenkins.view.extension.JobParameterRenderers.NUMBER_OF_REQUIRING_INPUT_FILTER_LIST;

public class BuiltInJobParameterRenderer implements JobParameterRenderer {

private final Map<JobParameterType, BiFunction<JobParameter, String, JobParameterComponent<?>>> converter = new HashMap<>();

public BuiltInJobParameterRenderer() {
converter.put(BuildInJobParameter.ChoiceParameterDefinition, JobParameterRenderers::createComboBox);
converter.put(BuildInJobParameter.ChoiceParameterDefinition, JobParameterRenderers::createComboBoxOrInputFilterListIfChoicesExists);
converter.put(BuildInJobParameter.BooleanParameterDefinition, JobParameterRenderers::createCheckBox);
converter.put(BuildInJobParameter.StringParameterDefinition, JobParameterRenderers::createTextField);
converter.put(BuildInJobParameter.PasswordParameterDefinition, JobParameterRenderers::createPasswordField);
Expand All @@ -47,7 +49,13 @@ public boolean isForJobParameter(@NotNull JobParameter jobParameter) {
public Optional<JLabel> createLabel(@NotNull JobParameter jobParameter) {
final JobParameterType jobParameterType = jobParameter.getJobParameterType();
final Optional<JLabel> label = JobParameterRenderer.super.createLabel(jobParameter);
if (BuildInJobParameter.TextParameterDefinition.equals(jobParameterType)) {
boolean isTypeForInputTextFilter =
BuildInJobParameter.TextParameterDefinition.equals(jobParameterType)
|| BuildInJobParameter.ChoiceParameterDefinition.equals(jobParameterType);

boolean isRequiringCountForInputTextFilter =
jobParameter.getChoices().size() > NUMBER_OF_REQUIRING_INPUT_FILTER_LIST;
if (isTypeForInputTextFilter && isRequiringCountForInputTextFilter) {
label.ifPresent(textAreaLabel -> textAreaLabel.setVerticalAlignment(SwingConstants.TOP));
}
return label;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.annotation.Nonnull;
import javax.swing.*;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

public class GitParameterRenderer implements JobParameterRenderer {
Expand Down Expand Up @@ -51,12 +54,20 @@ public JobParameterComponent<String> render(@NotNull JobParameter jobParameter,
return JobParameterRenderers.createErrorLabel(jobParameter);
}
if (projectJob == null) {
return JobParameterRenderers.createComboBoxIfChoicesExists(jobParameter, jobParameter.getDefaultValue());
return JobParameterRenderers.createInputFilterListIfChoicesExists(jobParameter, jobParameter.getDefaultValue());
} else {
return JobParameterRenderers.createGitParameterChoices(projectJob).apply(jobParameter);
}
}

@Nonnull
@Override
public Optional<JLabel> createLabel(@NotNull JobParameter jobParameter) {
final Optional<JLabel> label = JobParameterRenderer.super.createLabel(jobParameter);
label.ifPresent(textAreaLabel -> textAreaLabel.setVerticalAlignment(SwingConstants.TOP));
return label;
}

@Override
public boolean isForJobParameter(@NotNull JobParameter jobParameter) {
return validTypes.contains(jobParameter.getJobParameterType());
Expand Down

0 comments on commit 76ebff3

Please sign in to comment.