Skip to content

Commit

Permalink
First ccut of the search feature in ikonli-browser
Browse files Browse the repository at this point in the history
  • Loading branch information
aalmiray committed Dec 7, 2020
1 parent a907280 commit 6b61c60
Show file tree
Hide file tree
Showing 7 changed files with 433 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/ikonli-browser/ikonli-browser.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ repositories {
dependencies {
implementation('org.kordamp.bootstrapfx:bootstrapfx-core:0.4.0') { transitive = false }
implementation('org.kordamp.desktoppanefx:desktoppanefx-core:0.15.0') { transitive = false }
implementation "io.reactivex.rxjava3:rxjava:$rxjavaVersion"
implementation "com.miglayout:miglayout-javafx:$miglayoutVersion"

implementation project(':ikonli-core')
implementation project(':ikonli-javafx')
Expand Down
4 changes: 4 additions & 0 deletions apps/ikonli-browser/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
requires org.kordamp.ikonli.whhg;
requires org.kordamp.ikonli.win10;
requires org.kordamp.ikonli.zondicons;
requires miglayout.javafx;
requires io.reactivex.rxjava3;
requires org.reactivestreams;

uses org.kordamp.ikonli.IkonHandler;
uses org.kordamp.ikonli.IkonProvider;
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ private Menu createActionsMenu() {
browse.setOnAction(e -> IkonPickerDialog.show(desktopPane));
MenuItem search = new MenuItem("Search");
search.setGraphic(FontIcon.of(BoxiconsRegular.SEARCH));
search.setOnAction(e -> SearchInternalWindow.show(desktopPane));
actionsMenu.getItems().addAll(browse, search);

return actionsMenu;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright 2015-2020 Andres Almiray
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.kordamp.ikonli.browser;

import eu.hansolo.tilesfx.tools.FlowGridPane;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.paint.Color;
import org.kordamp.desktoppanefx.scene.layout.DesktopPane;
import org.kordamp.desktoppanefx.scene.layout.InternalWindow;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.IkonProvider;
import org.kordamp.ikonli.boxicons.BoxiconsRegular;
import org.kordamp.ikonli.browser.internal.JavaFXThreadProxyObservableList;
import org.kordamp.ikonli.javafx.FontIcon;
import org.tbee.javafx.scene.layout.MigPane;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;

import static java.util.EnumSet.allOf;
import static java.util.Objects.requireNonNull;
import static javafx.collections.FXCollections.observableArrayList;

/**
* @author Andres Almiray
*/
public class SearchInternalWindow extends InternalWindow {
private static final String SEARCH_WINDOW_ID = "__SEARCH__";

private final Model model = new Model();
private final Controller controller = new Controller(model);

public SearchInternalWindow(Node icon, MigPane migPane) {
super(SEARCH_WINDOW_ID, icon, "Search", migPane);

Label label = new Label("Search:");
TextField searchItem = new TextField();
model.termProperty().bind(searchItem.textProperty());
Button searchButton = new Button();
searchButton.setGraphic(FontIcon.of(BoxiconsRegular.SEARCH_ALT_2, Color.WHITE));
searchButton.getStyleClass().addAll("btn", "btm-sm", "btn-primary");
migPane.add(label);
migPane.add(searchItem, "grow");
migPane.add(searchButton);

Button cancelButton = new Button();
cancelButton.setGraphic(FontIcon.of(BoxiconsRegular.X, Color.WHITE));
cancelButton.getStyleClass().addAll("btn", "btm-sm", "btn-danger");
ProgressBar progress = new ProgressBar();
migPane.add(progress, "span 2, grow");
migPane.add(cancelButton);

label = new Label("Selection:");
TextField selection = new TextField();
selection.setEditable(false);
selection.textProperty().bind(model.selectionProperty());
Button copy = new Button();
copy.setGraphic(FontIcon.of(BoxiconsRegular.COPY, Color.WHITE));
copy.getStyleClass().addAll("btn", "btm-sm", "btn-primary");
migPane.add(label);
migPane.add(selection, "grow");
migPane.add(copy);

copy.disableProperty().bind(selection.textProperty().isEmpty());
copy.setOnAction(e -> {
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString(model.getSelection());
clipboard.setContent(content);
});

final FlowGridPane grid = new FlowGridPane(10, 20);
grid.setHgap(5);
grid.setVgap(5);
grid.setAlignment(Pos.CENTER);
grid.setCenterShape(true);
grid.setPadding(new Insets(5));
grid.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
migPane.add(new ScrollPane(grid), "span 3, grow");

searchButton.setOnAction(e -> searchIcons());

BooleanProperty enabled = new SimpleBooleanProperty(this, "enabled", true);
BooleanProperty running = new SimpleBooleanProperty(this, "running", false);

searchItem.textProperty().addListener((observable, oldValue, newValue) -> {
model.setState(null == newValue || newValue.isEmpty() ? State.DISABLED : State.READY);
});
running.addListener((observable, oldValue, newValue) -> {
if (null != newValue && newValue) grid.getChildren().clear();
});

model.stateProperty().addListener((observable, oldValue, newValue) ->
Platform.runLater(() -> {
switch (newValue) {
case DISABLED:
enabled.setValue(false);
running.setValue(false);
break;
case READY:
enabled.setValue(true);
running.setValue(false);
break;
case RUNNING:
enabled.setValue(false);
running.setValue(true);
break;
}
}));

searchButton.disableProperty().bind(Bindings.not(enabled));
cancelButton.visibleProperty().bind(running);
cancelButton.managedProperty().bind(running);
progress.visibleProperty().bind(running);
progress.managedProperty().bind(running);

model.getIkons().addListener(new ListChangeListener<>() {
private final FontIcon[] previousIcon = new FontIcon[1];

@Override
public void onChanged(Change<? extends Ikon> c) {
while (c.next()) {
if (c.wasAdded()) {
addIcons(new ArrayList<>(c.getAddedSubList()), grid);
}
}
}

private void addIcons(List<? extends Ikon> list, FlowGridPane grid) {
if (Platform.isFxApplicationThread()) {
doAddIcons(list, grid);
} else {
Platform.runLater(() -> doAddIcons(list, grid));
}
}

private void doAddIcons(List<? extends Ikon> list, FlowGridPane grid) {
list.forEach(ikon -> grid.getChildren().add(createIcon(ikon)));
}

private FontIcon createIcon(Ikon ikon) {
FontIcon icon = FontIcon.of(ikon);
icon.getStyleClass().setAll("font-icon");
icon.setOnMouseClicked(me -> {
if (previousIcon[0] != null) {
previousIcon[0].getStyleClass().remove("active-icon");
}
FontIcon nextIcon = (FontIcon) me.getSource();
model.setSelection(nextIcon.getIconCode().getDescription());
nextIcon.getStyleClass().add("active-icon");
previousIcon[0] = nextIcon;
});
return icon;
}
});

setOnHidden(e -> controller.cancel());
}

private void searchIcons() {
controller.cancel();
controller.search();
}

public static void show(DesktopPane desktopPane) {
desktopPane.findInternalWindow(SEARCH_WINDOW_ID)
.or(() -> Optional.of(createInternalWindow()))
.ifPresent(desktopPane::addInternalWindow);
}

private static SearchInternalWindow createInternalWindow() {
SearchInternalWindow internalWindow = new SearchInternalWindow(
FontIcon.of(BoxiconsRegular.GRID_SMALL, Color.WHITE),
new MigPane("fill, wrap 3",
"[left, p!][80%][center]",
"[p][p][p][80%]"));
internalWindow.setPrefSize(800, 600);
internalWindow.getStylesheets().addAll(
IkonBrowser.class.getResource("common.css").toExternalForm(),
IkonBrowser.class.getResource("ikon-window.css").toExternalForm());
return internalWindow;
}

private static <E> ObservableList<E> createJavaFXThreadProxyList(ObservableList<E> source) {
requireNonNull(source, "Argument 'source' must not be null");
return new JavaFXThreadProxyObservableList<>(source);
}

public enum State {
DISABLED,
READY,
RUNNING
}

private static class Model {
private final ObservableList<Ikon> ikons = observableArrayList();
private StringProperty term;
private StringProperty selection;
private ObjectProperty<State> state;
private Disposable disposable;

public Model() {
stateProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == State.RUNNING) {
ikons.clear();
}
});
}

public ObservableList<Ikon> getIkons() {
return ikons;
}

public Disposable getDisposable() {
return disposable;
}

public void setDisposable(Disposable disposable) {
this.disposable = disposable;
}

public StringProperty termProperty() {
if (term == null) {
term = new SimpleStringProperty(this, "term");
}
return term;
}

public String getTerm() {
return termProperty().get();
}

public void setTerm(String term) {
termProperty().set(term);
}

public StringProperty selectionProperty() {
if (selection == null) {
selection = new SimpleStringProperty(this, "selection");
}
return selection;
}

public String getSelection() {
return selectionProperty().get();
}

public void setSelection(String selection) {
this.selectionProperty().set(selection);
}

public State getState() {
return stateProperty().get();
}

public void setState(State state) {
stateProperty().set(state);
}

public ObjectProperty<State> stateProperty() {
if (state == null) {
state = new SimpleObjectProperty<>(this, "state", State.READY);
}
return state;
}
}

private static class Controller {
private final Model model;
private final List<Ikon> ikons = new ArrayList<>();

public Controller(Model model) {
this.model = model;
}

public void search() {
if (ikons.isEmpty()) {
loadIcons();
}

String term = model.getTerm().toLowerCase();
Flowable<Ikon> flowable = Flowable.fromIterable(ikons)
.filter(ikon -> ikon.getDescription().contains(term));

model.setDisposable(flowable
.doOnSubscribe(disposable -> {
model.getIkons().clear();
model.setState(State.RUNNING);
})
.doOnTerminate(() -> model.setState(State.READY))
.doOnError(Throwable::printStackTrace)
.subscribeOn(Schedulers.io())
.subscribe(model.getIkons()::add));
}

public void cancel() {
model.setSelection("");
if (model.getDisposable() != null) {
model.getDisposable().dispose();
model.setState(State.READY);
}
}

private void loadIcons() {
if (null != IkonProvider.class.getModule().getLayer()) {
for (IkonProvider provider : ServiceLoader.load(IkonProvider.class.getModule().getLayer(), IkonProvider.class)) {
add(allOf(provider.getIkon()), ikons);
}
} else {
for (IkonProvider provider : ServiceLoader.load(IkonProvider.class)) {
add(allOf(provider.getIkon()), ikons);
}
}
}

private void add(EnumSet<? extends Ikon> set, List<Ikon> ikons) {
ikons.addAll(set);
}
}
}
Loading

0 comments on commit 6b61c60

Please sign in to comment.