ctbrec-5.3.2-experimental/client/src/main/java/ctbrec/ui/tabs/ThumbOverviewTab.java

898 lines
34 KiB
Java

package ctbrec.ui.tabs;
import static ctbrec.ui.controls.Dialogs.*;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.event.EventBusHolder;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.sites.mfc.MyFreeCamsClient;
import ctbrec.sites.mfc.MyFreeCamsModel;
import ctbrec.ui.AutosizeAlert;
import ctbrec.ui.DesktopIntegration;
import ctbrec.ui.SiteUiFactory;
import ctbrec.ui.TipDialog;
import ctbrec.ui.TokenLabel;
import ctbrec.ui.action.OpenRecordingsDir;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.controls.SearchPopover;
import ctbrec.ui.controls.SearchPopoverTreeList;
import javafx.animation.FadeTransition;
import javafx.animation.Interpolator;
import javafx.animation.ParallelTransition;
import javafx.animation.ScaleTransition;
import javafx.animation.Transition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.Worker.State;
import javafx.concurrent.WorkerStateEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.transform.Transform;
import javafx.util.Duration;
public class ThumbOverviewTab extends Tab implements TabSelectionListener {
private static final Logger LOG = LoggerFactory.getLogger(ThumbOverviewTab.class);
protected static BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
static ExecutorService threadPool = new ThreadPoolExecutor(2, 2, 10, TimeUnit.MINUTES, queue, createThreadFactory());
protected FlowPane grid = new FlowPane();
protected PaginatedScheduledService updateService;
protected HBox pagination;
protected List<ThumbCell> selectedThumbCells = Collections.synchronizedList(new ArrayList<>());
List<ThumbCell> filteredThumbCells = Collections.synchronizedList(new ArrayList<>());
Recorder recorder;
private String filter;
ReentrantLock gridLock = new ReentrantLock();
ScrollPane scrollPane = new ScrollPane();
boolean loginRequired;
TextField pageInput = new TextField(Integer.toString(1));
Button pageFirst = new Button("1");
Button pagePrev = new Button("");
Button pageNext = new Button("");
private volatile boolean updatesSuspended = false;
ContextMenu popup;
Site site;
StackPane root = new StackPane();
Task<List<Model>> searchTask;
SearchPopover popover;
SearchPopoverTreeList popoverTreelist = new SearchPopoverTreeList();
private ComboBox<Integer> thumbWidth;
public ThumbOverviewTab(String title, PaginatedScheduledService updateService, Site site) {
super(title);
this.updateService = updateService;
this.site = site;
setClosable(false);
createGui();
initializeUpdateService();
}
protected void createGui() {
grid.setPadding(new Insets(5));
grid.setHgap(5);
grid.setVgap(5);
SearchBox filterInput = new SearchBox(false);
filterInput.setPromptText("Filter models on this page");
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
filter = filterInput.getText();
gridLock.lock();
try {
filter();
moveActiveRecordingsToFront();
} finally {
gridLock.unlock();
}
});
Tooltip filterTooltip = new Tooltip("Filter the models by their name, stream description or #hashtags.\n\n"
+ "If the display of stream resolution is enabled, you can even filter for public rooms or by resolution.\n\n"
+ "Try \"1080\" or \">720\" or \"public\"");
filterInput.setTooltip(filterTooltip);
filterInput.getStyleClass().remove("search-box-icon");
SearchBox searchInput = new SearchBox();
searchInput.setPromptText("Search Model");
searchInput.prefWidth(200);
searchInput.textProperty().addListener(search());
searchInput.addEventHandler(KeyEvent.KEY_PRESSED, evt -> {
if(evt.getCode() == KeyCode.ESCAPE) {
popover.hide();
}
});
popover = new SearchPopover();
popover.maxWidthProperty().bind(popover.minWidthProperty());
popover.prefWidthProperty().bind(popover.minWidthProperty());
popover.setMinWidth(400);
popover.maxHeightProperty().bind(popover.minHeightProperty());
popover.prefHeightProperty().bind(popover.minHeightProperty());
popover.setMinHeight(450);
popover.pushPage(popoverTreelist);
StackPane.setAlignment(popover, Pos.TOP_RIGHT);
StackPane.setMargin(popover, new Insets(35, 50, 0, 0));
HBox topBar = new HBox(5);
HBox.setHgrow(filterInput, Priority.ALWAYS);
topBar.getChildren().add(filterInput);
if (site.supportsTips() && site.credentialsAvailable()) {
Button buyTokens = new Button("Buy Tokens");
buyTokens.setOnAction(e -> DesktopIntegration.open(site.getBuyTokensLink()));
TokenLabel tokenBalance = new TokenLabel(site);
tokenBalance.setAlignment(Pos.CENTER_RIGHT);
tokenBalance.prefHeightProperty().bind(buyTokens.heightProperty());
topBar.getChildren().addAll(tokenBalance, buyTokens);
tokenBalance.loadBalance();
}
if(site.supportsSearch()) {
topBar.getChildren().add(searchInput);
}
BorderPane.setMargin(topBar, new Insets(0, 5, 0, 5));
scrollPane.setContent(grid);
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
BorderPane.setMargin(scrollPane, new Insets(5));
pagination = new HBox(5);
pagination.getChildren().add(pageFirst);
pagination.getChildren().add(pagePrev);
pagination.getChildren().add(pageNext);
pagination.getChildren().add(pageInput);
BorderPane.setMargin(pagination, new Insets(5));
pageInput.setPrefWidth(50);
pageInput.setOnAction(e -> handlePageNumberInput());
pageFirst.setTooltip(new Tooltip("First Page"));
pageFirst.setOnAction(e -> {
pageInput.setText(Integer.toString(1));
updateService.setPage(1);
restartUpdateService();
});
pagePrev.setTooltip(new Tooltip("Previous Page"));
pagePrev.setOnAction(e -> {
int page = updateService.getPage();
page = Math.max(1, --page);
pageInput.setText(Integer.toString(page));
updateService.setPage(page);
restartUpdateService();
});
pageNext.setTooltip(new Tooltip("Next Page"));
pageNext.setOnAction(e -> {
int page = updateService.getPage();
page++;
pageInput.setText(Integer.toString(page));
updateService.setPage(page);
restartUpdateService();
});
HBox thumbSizeSelector = new HBox(5);
Label l = new Label("Thumb Size");
l.setPadding(new Insets(5,0,0,0));
thumbSizeSelector.getChildren().add(l);
List<Integer> thumbWidths = new ArrayList<>();
thumbWidths.add(180);
thumbWidths.add(200);
thumbWidths.add(220);
thumbWidths.add(270);
thumbWidths.add(360);
thumbWidth = new ComboBox<>(FXCollections.observableList(thumbWidths));
thumbWidth.getSelectionModel().select(Integer.valueOf(Config.getInstance().getSettings().thumbWidth));
thumbWidth.setOnAction(e -> {
int width = thumbWidth.getSelectionModel().getSelectedItem();
Config.getInstance().getSettings().thumbWidth = width;
updateThumbSize();
});
thumbSizeSelector.getChildren().add(thumbWidth);
BorderPane.setMargin(thumbSizeSelector, new Insets(5));
BorderPane bottomPane = new BorderPane();
bottomPane.setLeft(pagination);
bottomPane.setRight(thumbSizeSelector);
BorderPane borderPane = new BorderPane();
borderPane.setPadding(new Insets(5));
borderPane.setTop(topBar);
borderPane.setCenter(scrollPane);
borderPane.setBottom(bottomPane);
root.getChildren().add(borderPane);
root.getChildren().add(popover);
setContent(root);
}
private ChangeListener<? super String> search() {
return (observableValue, oldValue, newValue) -> {
if(searchTask != null) {
searchTask.cancel(true);
}
if(newValue.length() < 2) {
return;
}
searchTask = new ThumbOverviewTabSearchTask(site, popover, popoverTreelist, newValue);
new Thread(searchTask).start();
};
}
private void updateThumbSize() {
int width = Config.getInstance().getSettings().thumbWidth;
thumbWidth.getSelectionModel().select(Integer.valueOf(width));
for (Node node : grid.getChildren()) {
if(node instanceof ThumbCell) {
ThumbCell cell = (ThumbCell) node;
cell.setThumbWidth(width);
}
}
for (ThumbCell cell : filteredThumbCells) {
cell.setThumbWidth(width);
}
}
private void handlePageNumberInput() {
try {
int page = Integer.parseInt(pageInput.getText());
page = Math.max(1, page);
updateService.setPage(page);
restartUpdateService();
} catch(NumberFormatException e) {
// noop
} finally {
pageInput.setText(Integer.toString(updateService.getPage()));
}
}
private void restartUpdateService() {
gridLock.lock();
try {
grid.getChildren().clear();
filteredThumbCells.clear();
deselected();
selected();
} finally {
gridLock.unlock();
}
}
void initializeUpdateService() {
int refreshRate = Config.getInstance().getSettings().overviewUpdateIntervalInSecs;
updateService.setPeriod(new Duration(TimeUnit.SECONDS.toMillis(refreshRate)));
updateService.setOnSucceeded(event -> onSuccess());
updateService.setOnFailed(this::onFail);
}
protected void onSuccess() {
if(updatesSuspended) {
return;
}
List<Model> models = filterIgnoredModels(updateService.getValue());
updateGrid(models);
}
private List<Model> filterIgnoredModels(List<Model> models) {
List<Model> ignored = Config.getInstance().getSettings().modelsIgnored;
return models.stream()
.filter(m -> !ignored.contains(m))
.collect(Collectors.toList());
}
protected void updateGrid(List<? extends Model> models) {
gridLock.lock();
try {
ObservableList<Node> nodes = grid.getChildren();
// first remove models, which are not in the updated list
removeModelsMissingInUpdate(nodes, models);
// now update existing cells and create new ones models, which are new in the update
createOrUpdateModelCells(nodes, models);
// reapply the filter
filteredThumbCells.clear();
filter();
// move models, which are tracked by the recorder to the front
moveActiveRecordingsToFront();
} finally {
gridLock.unlock();
}
}
private void createOrUpdateModelCells(ObservableList<Node> nodes, List<? extends Model> models) {
List<ThumbCell> positionChangedOrNew = new ArrayList<>();
int index = 0;
for (Model model : models) {
boolean found = false;
for (Node node : nodes) { // NOSONAR
if (!(node instanceof ThumbCell)) continue;
ThumbCell cell = (ThumbCell) node;
if(cell.getModel().equals(model)) {
found = true;
cell.setModel(model);
if(index != cell.getIndex()) {
cell.setIndex(index);
positionChangedOrNew.add(cell);
}
break;
}
}
if(!found) {
ThumbCell newCell = createThumbCell(model, recorder);
newCell.setIndex(index);
positionChangedOrNew.add(newCell);
}
index++;
}
rearrangeCells(nodes, positionChangedOrNew);
}
private void rearrangeCells(ObservableList<Node> nodes, List<ThumbCell> positionChangedOrNew) {
for (ThumbCell thumbCell : positionChangedOrNew) {
nodes.remove(thumbCell);
if(thumbCell.getIndex() < nodes.size()) {
nodes.add(thumbCell.getIndex(), thumbCell);
} else {
nodes.add(thumbCell);
}
}
}
private void removeModelsMissingInUpdate(ObservableList<Node> nodes, List<? extends Model> models) {
for (Iterator<Node> iterator = nodes.iterator(); iterator.hasNext();) {
Node node = iterator.next();
if (!(node instanceof ThumbCell)) continue;
ThumbCell cell = (ThumbCell) node;
if (!models.contains(cell.getModel())) {
iterator.remove();
}
}
}
ThumbCell createThumbCell(Model model, Recorder recorder) {
ThumbCell newCell = new ThumbCell(this, model, recorder);
newCell.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
suspendUpdates(true);
popup = createContextMenu(newCell);
popup.show(newCell, event.getScreenX(), event.getScreenY());
popup.setOnHidden(e -> suspendUpdates(false));
event.consume();
});
newCell.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if(popup != null) {
popup.hide();
popup = null;
return;
}
});
newCell.selectionProperty().addListener((obs, oldValue, newValue) -> {
if(newValue.booleanValue()) {
selectedThumbCells.add(newCell);
} else {
selectedThumbCells.remove(newCell);
}
});
newCell.setOnMouseClicked(mouseClickListener);
return newCell;
}
private ContextMenu createContextMenu(ThumbCell cell) {
MenuItem openInPlayer = new MenuItem("Open in Player");
openInPlayer.setOnAction(e -> startPlayer(getSelectedThumbCells(cell)));
MenuItem start = new MenuItem("Start Recording");
start.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), true));
MenuItem stop = new MenuItem("Stop Recording");
stop.setOnAction(e -> startStopAction(getSelectedThumbCells(cell), false));
MenuItem startStop = recorder.isTracked(cell.getModel()) ? stop : start;
MenuItem pause = new MenuItem("Pause Recording");
pause.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), true));
MenuItem resume = new MenuItem("Resume Recording");
resume.setOnAction(e -> pauseResumeAction(getSelectedThumbCells(cell), false));
MenuItem pauseResume = recorder.isSuspended(cell.getModel()) ? resume : pause;
MenuItem follow = new MenuItem("Follow");
follow.setOnAction(e -> follow(getSelectedThumbCells(cell), true));
MenuItem unfollow = new MenuItem("Unfollow");
unfollow.setOnAction(e -> follow(getSelectedThumbCells(cell), false));
MenuItem ignore = new MenuItem("Ignore");
ignore.setOnAction(e -> ignore(getSelectedThumbCells(cell)));
MenuItem refresh = new MenuItem("Refresh");
refresh.setOnAction(e -> refresh());
MenuItem openRecDir = new MenuItem("Open recording directory");
openRecDir.setOnAction(e -> new OpenRecordingsDir(cell, cell.getModel()).execute());
MenuItem copyUrl = createCopyUrlMenuItem(cell);
MenuItem sendTip = createTipMenuItem(cell);
configureItemsForSelection(cell, openInPlayer, copyUrl, sendTip);
ContextMenu contextMenu = new ContextMenu();
contextMenu.setAutoHide(true);
contextMenu.setHideOnEscape(true);
contextMenu.setAutoFix(true);
contextMenu.getItems().addAll(openInPlayer, startStop);
if(recorder.isTracked(cell.getModel())) {
contextMenu.getItems().add(pauseResume);
}
if(site.supportsFollow()) {
MenuItem followOrUnFollow = (this instanceof FollowedTab) ? unfollow : follow;
followOrUnFollow.setDisable(!site.credentialsAvailable());
contextMenu.getItems().add(followOrUnFollow);
}
if(site.supportsTips()) {
contextMenu.getItems().add(sendTip);
}
contextMenu.getItems().addAll(copyUrl, ignore, refresh, openRecDir);
if(cell.getModel() instanceof MyFreeCamsModel && Objects.equals(System.getenv("CTBREC_DEV"), "1")) {
MenuItem debug = new MenuItem("debug");
debug.setOnAction(e -> MyFreeCamsClient.getInstance().getSessionState(cell.getModel()));
contextMenu.getItems().add(debug);
}
return contextMenu;
}
private void refresh() {
if (updateService.isRunning()) {
updateService.cancel();
updateService.reset();
updateService.restart();
}
}
/* check, if other cells are selected, too. in that case, we have to disable menu items, which make sense only for
* single selections. but only do that, if the popup has been triggered on a selected cell. otherwise remove the
* selection and show the normal menu
*/
private void configureItemsForSelection(ThumbCell cell, MenuItem openInPlayer, MenuItem copyUrl, MenuItem sendTip) {
if (selectedThumbCells.size() > 1 || selectedThumbCells.size() == 1 && selectedThumbCells.get(0) != cell) {
if(cell.isSelected()) {
if(Config.getInstance().getSettings().singlePlayer) {
openInPlayer.setDisable(true);
}
copyUrl.setDisable(true);
sendTip.setDisable(true);
} else {
removeSelection();
}
}
}
private MenuItem createCopyUrlMenuItem(ThumbCell cell) {
MenuItem copyUrl = new MenuItem("Copy URL");
copyUrl.setOnAction(e -> {
final Clipboard clipboard = Clipboard.getSystemClipboard();
final ClipboardContent content = new ClipboardContent();
content.putString(cell.getModel().getUrl());
clipboard.setContent(content);
});
return copyUrl;
}
private MenuItem createTipMenuItem(ThumbCell cell) {
MenuItem sendTip = new MenuItem("Send Tip");
sendTip.setOnAction(e -> {
TipDialog tipDialog = new TipDialog(getTabPane().getScene(), site, cell.getModel());
tipDialog.showAndWait();
String tipText = tipDialog.getResult();
if(tipText != null) {
DecimalFormat df = new DecimalFormat("0.##");
try {
Number tokens = df.parse(tipText);
SiteUiFactory.getUi(site).login();
cell.getModel().receiveTip(tokens.doubleValue());
Map<String, Object> event = new HashMap<>();
event.put("event", "tokens.sent");
event.put("amount", tokens.doubleValue());
EventBusHolder.BUS.post(event);
} catch (IOException ex) {
LOG.error("An error occured while sending tip", ex);
showError("Couldn't send tip", "An error occured while sending tip:", ex);
} catch (Exception ex) {
showError("Couldn't send tip", "You entered an invalid amount of tokens", ex);
}
}
});
sendTip.setDisable(!site.credentialsAvailable());
return sendTip;
}
private List<ThumbCell> getSelectedThumbCells(ThumbCell cell) {
if(selectedThumbCells.isEmpty()) {
return Collections.singletonList(cell);
} else {
return selectedThumbCells;
}
}
protected void follow(List<ThumbCell> selection, boolean follow) {
for (ThumbCell thumbCell : selection) {
thumbCell.follow(follow).thenAccept(success -> {
if(follow && success.booleanValue()) {
showAddToFollowedAnimation(thumbCell);
}
});
}
if(!follow) {
selectedThumbCells.clear();
}
}
protected void ignore(List<ThumbCell> selection) {
for (ThumbCell thumbCell : selection) {
Model model = thumbCell.getModel();
Config.getInstance().getSettings().modelsIgnored.add(model);
grid.getChildren().remove(thumbCell);
}
selectedThumbCells.clear();
}
private void showAddToFollowedAnimation(ThumbCell thumbCell) {
Platform.runLater(() -> {
Transform tx = thumbCell.getLocalToParentTransform();
ImageView iv = new ImageView();
iv.setFitWidth(thumbCell.getWidth());
root.getChildren().add(iv);
StackPane.setAlignment(iv, Pos.TOP_LEFT);
iv.setImage(thumbCell.getImage());
double scrollPaneTopLeft = scrollPane.getVvalue() * (grid.getHeight() - scrollPane.getViewportBounds().getHeight());
double offsetInViewPort = tx.getTy() - scrollPaneTopLeft;
int duration = 500;
TranslateTransition translate = new TranslateTransition(Duration.millis(duration), iv);
translate.setFromX(0);
translate.setFromY(0);
translate.setByX(-tx.getTx() - 200);
TabProvider tabProvider = SiteUiFactory.getUi(site).getTabProvider();
Tab followedTab = tabProvider.getFollowedTab();
translate.setByY(-offsetInViewPort + getFollowedTabYPosition(followedTab));
StackPane.setMargin(iv, new Insets(offsetInViewPort, 0, 0, tx.getTx()));
translate.setInterpolator(Interpolator.EASE_BOTH);
FadeTransition fade = new FadeTransition(Duration.millis(duration), iv);
fade.setFromValue(1);
fade.setToValue(.3);
ScaleTransition scale = new ScaleTransition(Duration.millis(duration), iv);
scale.setToX(0.1);
scale.setToY(0.1);
ParallelTransition pt = new ParallelTransition(translate, scale);
pt.play();
pt.setOnFinished(evt -> root.getChildren().remove(iv));
String normalStyle = followedTab.getStyle();
Color normal = Color.web("#f4f4f4");
Color highlight = Color.web("#2b8513");
Transition blink = new Transition() {
{
setCycleDuration(Duration.millis(500));
}
@Override
protected void interpolate(double frac) {
double rh = highlight.getRed();
double rn = normal.getRed();
double diff = rh - rn;
double r = (rn + diff * frac) * 255;
double gh = highlight.getGreen();
double gn = normal.getGreen();
diff = gh - gn;
double g = (gn + diff * frac) * 255;
double bh = highlight.getBlue();
double bn = normal.getBlue();
diff = bh - bn;
double b = (bn + diff * frac) * 255;
String style = "-fx-background-color: rgb(" + r + "," + g + "," + b + ")";
followedTab.setStyle(style);
}
};
blink.setCycleCount(6);
blink.setAutoReverse(true);
blink.setOnFinished(evt -> followedTab.setStyle(normalStyle));
blink.play();
});
}
private double getFollowedTabYPosition(Tab followedTab) {
TabPane tabPane = getTabPane();
int idx = Math.max(0, tabPane.getTabs().indexOf(followedTab));
for (Node node : tabPane.getChildrenUnmodifiable()) {
Parent p = (Parent) node;
for (Node child : p.getChildrenUnmodifiable()) {
if(child.getStyleClass().contains("headers-region")) {
Parent tabContainer = (Parent) child;
Node tab = tabContainer.getChildrenUnmodifiable().get(tabContainer.getChildrenUnmodifiable().size() - idx - 1);
return tab.getLayoutX() - 85;
}
}
}
return 0;
}
private void startStopAction(List<ThumbCell> selection, boolean start) {
for (ThumbCell thumbCell : selection) {
thumbCell.startStopAction(start);
}
}
private void pauseResumeAction(List<ThumbCell> selection, boolean pause) {
for (ThumbCell thumbCell : selection) {
thumbCell.pauseResumeAction(pause);
}
}
private void startPlayer(List<ThumbCell> selection) {
for (ThumbCell thumbCell : selection) {
thumbCell.startPlayer();
}
}
private EventHandler<MouseEvent> mouseClickListener = e -> {
ThumbCell cell = (ThumbCell) e.getSource();
if (e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) {
cell.setSelected(false);
cell.startPlayer();
} else if (e.getButton() == MouseButton.PRIMARY && e.isControlDown()) {
if(popup == null) {
cell.setSelected(!cell.isSelected());
}
} else if (e.getButton() == MouseButton.PRIMARY) {
removeSelection();
}
};
protected void onFail(WorkerStateEvent event) {
if(updatesSuspended) {
return;
}
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, getTabPane().getScene());
alert.setTitle("Error");
alert.setHeaderText("Couldn't fetch model list");
if(event.getSource().getException() != null) {
if(event.getSource().getException() instanceof SocketTimeoutException) {
LOG.debug("Fetching model list timed out");
return;
} else {
alert.setContentText(event.getSource().getException().getLocalizedMessage());
}
LOG.error("Couldn't update model list", event.getSource().getException());
} else {
alert.setContentText(event.getEventType().toString());
}
alert.showAndWait();
}
void filter() {
Collections.sort(filteredThumbCells, (o1, o2) -> {
ThumbCell c1 = o1;
ThumbCell c2 = o2;
if(c1.getIndex() < c2.getIndex()) return -1;
if(c1.getIndex() > c2.getIndex()) return 1;
return c1.getModel().getName().compareTo(c2.getModel().getName());
});
if (filter == null || filter.isEmpty()) {
for (ThumbCell thumbCell : filteredThumbCells) {
insert(thumbCell);
}
filteredThumbCells.clear();
return;
}
// remove the ones from grid, which don't match
for (Iterator<Node> iterator = grid.getChildren().iterator(); iterator.hasNext();) {
Node node = iterator.next();
ThumbCell cell = (ThumbCell) node;
Model m = cell.getModel();
if(!matches(m, filter)) {
iterator.remove();
filteredThumbCells.add(cell);
cell.setSelected(false);
}
}
// add the ones, which might have been filtered before, but now match
for (Iterator<ThumbCell> iterator = filteredThumbCells.iterator(); iterator.hasNext();) {
ThumbCell thumbCell = iterator.next();
Model m = thumbCell.getModel();
if(matches(m, filter)) {
iterator.remove();
insert(thumbCell);
}
}
}
private void moveActiveRecordingsToFront() {
List<ThumbCell> thumbsToMove = new ArrayList<>();
ObservableList<Node> thumbs = grid.getChildren();
for (int i = thumbs.size()-1; i >= 0; i--) {
ThumbCell thumb = (ThumbCell) thumbs.get(i);
if(recorder.isTracked(thumb.getModel())) {
thumbs.remove(i);
thumbsToMove.add(0, thumb);
}
}
thumbs.addAll(0, thumbsToMove);
}
private void insert(ThumbCell thumbCell) {
if(grid.getChildren().contains(thumbCell)) {
return;
}
if(thumbCell.getIndex() < grid.getChildren().size()-1) {
grid.getChildren().add(thumbCell.getIndex(), thumbCell);
} else {
grid.getChildren().add(thumbCell);
}
}
private boolean matches(Model m, String filter) {
try {
String[] tokens = filter.split(" ");
boolean tokensMissing = false;
for (String token : tokens) {
if (!modelPropertiesMatchToken(token, m)) {
tokensMissing = true;
}
}
return !tokensMissing;
} catch (NumberFormatException | ExecutionException | IOException e) {
LOG.error("Error while filtering model list", e);
return false;
}
}
private boolean modelPropertiesMatchToken(String token, Model m) throws IOException, ExecutionException {
int[] resolution = Optional.ofNullable(ThumbCell.resolutionCache.getIfPresent(m)).orElse(new int[2]);
String searchText = createSearchText(m);
boolean tokensMissing = false;
if (token.matches(">\\d+")) {
int res = Integer.parseInt(token.substring(1));
if (resolution[1] < res) {
tokensMissing = true;
}
} else if (token.matches("<\\d+")) {
int res = Integer.parseInt(token.substring(1));
if (resolution[1] > res) {
tokensMissing = true;
}
} else if (token.equals("public")) {
if (m.getOnlineState(true) != ctbrec.Model.State.ONLINE) {
tokensMissing = true;
}
} else {
boolean negated = false;
if(token.startsWith("!")) {
negated = true;
token = token.substring(1);
}
boolean tokenFound = searchText.toLowerCase().contains(token.toLowerCase());
tokensMissing = !tokenFound && !negated || tokenFound && negated;
}
return !tokensMissing;
}
private String createSearchText(Model m) throws ExecutionException {
StringBuilder searchTextBuilder = new StringBuilder(m.getName());
searchTextBuilder.append(' ');
searchTextBuilder.append(m.getDisplayName());
searchTextBuilder.append(' ');
for (String tag : m.getTags()) {
searchTextBuilder.append(tag).append(' ');
}
int[] resolution = Optional.ofNullable(ThumbCell.resolutionCache.getIfPresent(m)).orElse(new int[2]);
searchTextBuilder.append(resolution[1]);
searchTextBuilder.append(' ');
searchTextBuilder.append(Optional.ofNullable(m.getDescription()).orElse(""));
return searchTextBuilder.toString().trim();
}
public void setRecorder(Recorder recorder) {
this.recorder = recorder;
popoverTreelist.setRecorder(recorder);
}
@Override
public void selected() {
queue.clear();
if(updateService != null) {
State s = updateService.getState();
if (s != State.SCHEDULED && s != State.RUNNING) {
updateService.reset();
updateService.restart();
}
}
updateThumbSize();
}
@Override
public void deselected() {
if (updateService != null) {
updateService.cancel();
}
queue.clear();
}
void suspendUpdates(boolean suspend) {
this.updatesSuspended = suspend;
}
private void removeSelection() {
while (!selectedThumbCells.isEmpty()) {
selectedThumbCells.get(0).setSelected(false);
}
}
private static int threadCounter = 0;
private static ThreadFactory createThreadFactory() {
return r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setPriority(Thread.MIN_PRIORITY);
t.setName("ResolutionDetector-" + threadCounter++);
return t;
};
}
}