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

323 lines
12 KiB
Java

package ctbrec.ui.tabs;
import static java.nio.charset.StandardCharsets.*;
import static java.nio.file.StandardOpenOption.*;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.eventbus.Subscribe;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.StringUtil;
import ctbrec.event.EventBusHolder;
import ctbrec.io.InstantJsonAdapter;
import ctbrec.io.ModelJsonAdapter;
import ctbrec.recorder.Recorder;
import ctbrec.sites.Site;
import ctbrec.ui.ShutdownListener;
import ctbrec.ui.action.PlayAction;
import ctbrec.ui.controls.CustomMouseBehaviorContextMenu;
import ctbrec.ui.controls.DateTimeCellFactory;
import ctbrec.ui.controls.SearchBox;
import ctbrec.ui.event.PlayerStartedEvent;
import ctbrec.ui.menu.ModelMenuContributor;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
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.HBox;
import javafx.scene.layout.Priority;
import javafx.util.Callback;
public class RecentlyWatchedTab extends Tab implements ShutdownListener {
private static final Logger LOG = LoggerFactory.getLogger(RecentlyWatchedTab.class);
private ObservableList<PlayerStartedEvent> filteredModels = FXCollections.observableArrayList();
private ObservableList<PlayerStartedEvent> observableModels = FXCollections.observableArrayList();
private TableView<PlayerStartedEvent> table = new TableView<>();
private ContextMenu popup;
private ReentrantLock lock = new ReentrantLock();
private Recorder recorder;
private List<Site> sites;
public RecentlyWatchedTab(Recorder recorder, List<Site> sites) {
this.recorder = recorder;
this.sites = sites;
setText("Recently Watched");
createGui();
loadHistory();
subscribeToPlayerEvents();
setOnClosed(evt -> onShutdown());
}
private void createGui() {
Config config = Config.getInstance();
var layout = new BorderPane();
layout.setPadding(new Insets(5, 10, 10, 10));
var filterInput = new SearchBox(false);
filterInput.setPromptText("Filter");
filterInput.textProperty().addListener( (observableValue, oldValue, newValue) -> {
String filter = filterInput.getText();
lock.lock();
try {
filter(filter);
} finally {
lock.unlock();
}
});
filterInput.getStyleClass().remove("search-box-icon");
HBox.setHgrow(filterInput, Priority.ALWAYS);
var topBar = new HBox(5);
topBar.getChildren().addAll(filterInput);
layout.setTop(topBar);
BorderPane.setMargin(topBar, new Insets(0, 0, 5, 0));
table.setItems(observableModels);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
if (!config.getSettings().showGridLinesInTables) {
table.setStyle("-fx-table-cell-border-color: transparent;");
}
table.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> {
popup = createContextMenu();
if (popup != null) {
popup.show(table, event.getScreenX(), event.getScreenY());
}
event.consume();
});
table.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (popup != null) {
popup.hide();
}
});
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
List<PlayerStartedEvent> selectedModels = table.getSelectionModel().getSelectedItems();
if (event.getCode() == KeyCode.DELETE) {
delete(selectedModels);
}
});
var idx = 0;
TableColumn<PlayerStartedEvent, String> name = createTableColumn("Model", 200, idx++);
name.setId("name");
name.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getDisplayName()));
name.setCellFactory(new ClickableCellFactory<>());
table.getColumns().add(name);
TableColumn<PlayerStartedEvent, String> url = createTableColumn("URL", 400, idx);
url.setCellValueFactory(cdf -> new SimpleStringProperty(cdf.getValue().getModel().getUrl()));
url.setCellFactory(new ClickableCellFactory<>());
url.setEditable(false);
url.setId("url");
table.getColumns().add(url);
TableColumn<PlayerStartedEvent, Instant> timestamp = createTableColumn("Timestamp", 150, idx);
timestamp.setId("timestamp");
timestamp.setCellValueFactory(cdf -> new SimpleObjectProperty<>(cdf.getValue().getTimestamp()));
timestamp.setCellFactory(new DateTimeCellFactory<>(config.getDateTimeFormatter()));
timestamp.setEditable(false);
timestamp.setSortType(SortType.DESCENDING);
table.getColumns().add(timestamp);
table.getSortOrder().add(timestamp);
var scrollPane = new ScrollPane();
scrollPane.setFitToHeight(true);
scrollPane.setFitToWidth(true);
scrollPane.setContent(table);
scrollPane.setStyle("-fx-background-color: -fx-background");
layout.setCenter(scrollPane);
setContent(layout);
}
private <T> TableColumn<PlayerStartedEvent, T> createTableColumn(String text, int width, int idx) {
TableColumn<PlayerStartedEvent, T> tc = new TableColumn<>(text);
tc.setPrefWidth(width);
tc.setUserData(idx);
return tc;
}
private void filter(String filter) {
lock.lock();
try {
if (StringUtil.isBlank(filter)) {
observableModels.addAll(filteredModels);
filteredModels.clear();
return;
}
String[] tokens = filter.split(" ");
observableModels.addAll(filteredModels);
filteredModels.clear();
for (var i = 0; i < table.getItems().size(); i++) {
var sb = new StringBuilder();
for (TableColumn<PlayerStartedEvent, ?> tc : table.getColumns()) {
Object cellData = tc.getCellData(i);
if(cellData != null) {
var content = cellData.toString();
sb.append(content).append(' ');
}
}
var searchText = sb.toString();
var tokensMissing = false;
for (String token : tokens) {
if(!searchText.toLowerCase().contains(token.toLowerCase())) {
tokensMissing = true;
break;
}
}
if(tokensMissing) {
PlayerStartedEvent sessionState = table.getItems().get(i);
filteredModels.add(sessionState);
}
}
observableModels.removeAll(filteredModels);
} finally {
lock.unlock();
}
}
private ContextMenu createContextMenu() {
ObservableList<PlayerStartedEvent> selectedRows = table.getSelectionModel().getSelectedItems();
if (selectedRows.isEmpty()) {
return null;
}
List<Model> selectedModels = table.getSelectionModel().getSelectedItems().stream().map(PlayerStartedEvent::getModel).collect(Collectors.toList());
ContextMenu menu = new CustomMouseBehaviorContextMenu();
ModelMenuContributor.newContributor(getTabPane(), Config.getInstance(), recorder) //
.withStartStopCallback(m -> getTabPane().setCursor(Cursor.DEFAULT)) //
.afterwards(table::refresh)
.contributeToMenu(selectedModels, menu);
menu.getItems().add(new SeparatorMenuItem());
var delete = new MenuItem("Delete");
delete.setOnAction(e -> delete(selectedRows));
var clearHistory = new MenuItem("Clear history");
clearHistory.setOnAction(e -> clearHistory());
menu.getItems().addAll(delete, clearHistory);
return menu;
}
private void clearHistory() {
observableModels.clear();
filteredModels.clear();
}
private void delete(List<PlayerStartedEvent> selectedRows) {
observableModels.removeAll(selectedRows);
}
private void subscribeToPlayerEvents() {
EventBusHolder.BUS.register(new Object() {
@Subscribe
public void handleEvent(PlayerStartedEvent evt) {
table.getItems().add(evt);
table.sort();
}
});
}
private class ClickableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
@Override
public TableCell<S, T> call(TableColumn<S, T> param) {
TableCell<S, T> cell = new TableCell<>() {
@Override
protected void updateItem(Object item, boolean empty) {
setText(empty ? "" : Objects.toString(item));
}
};
cell.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2) {
var selectedModel = table.getSelectionModel().getSelectedItem().getModel();
if(selectedModel != null) {
new PlayAction(table, selectedModel).execute();
}
}
});
return cell;
}
}
private void saveHistory() throws IOException {
var moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(Instant.class, new InstantJsonAdapter())
.build();
Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
String json = adapter.indent(" ").toJson(observableModels);
var recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
LOG.debug("Saving recently watched models to {}", recentlyWatched.getAbsolutePath());
Files.createDirectories(recentlyWatched.getParentFile().toPath());
Files.write(recentlyWatched.toPath(), json.getBytes(UTF_8), CREATE, WRITE, TRUNCATE_EXISTING);
}
private void loadHistory() {
var recentlyWatched = new File(Config.getInstance().getConfigDir(), "recently_watched.json");
if(!recentlyWatched.exists()) {
return;
}
LOG.debug("Loading recently watched models from {}", recentlyWatched.getAbsolutePath());
var moshi = new Moshi.Builder()
.add(Model.class, new ModelJsonAdapter(sites))
.add(Instant.class, new InstantJsonAdapter())
.build();
Type type = Types.newParameterizedType(List.class, PlayerStartedEvent.class);
JsonAdapter<List<PlayerStartedEvent>> adapter = moshi.adapter(type);
try {
List<PlayerStartedEvent> fromJson = adapter.fromJson(Files.readString(recentlyWatched.toPath(), UTF_8));
observableModels.addAll(fromJson);
} catch (IOException e) {
LOG.error("Couldn't load recently watched models", e);
}
}
@Override
public void onShutdown() {
try {
saveHistory();
} catch (IOException e) {
LOG.error("Couldn't safe recently watched models", e);
}
}
}