diff --git a/src/main/java/ctbrec/ui/CtbrecApplication.java b/src/main/java/ctbrec/ui/CtbrecApplication.java index 00028fd3..932f3721 100644 --- a/src/main/java/ctbrec/ui/CtbrecApplication.java +++ b/src/main/java/ctbrec/ui/CtbrecApplication.java @@ -97,6 +97,7 @@ public class CtbrecApplication extends Application { int windowWidth = Config.getInstance().getSettings().windowWidth; int windowHeight = Config.getInstance().getSettings().windowHeight; primaryStage.setScene(new Scene(tabPane, windowWidth, windowHeight)); + primaryStage.getScene().getStylesheets().add("/ctbrec/ui/ThumbCell.css"); primaryStage.getScene().widthProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowWidth = newVal.intValue()); primaryStage.getScene().heightProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue()); primaryStage.setMaximized(Config.getInstance().getSettings().windowMaximized); diff --git a/src/main/java/ctbrec/ui/ThumbCell.css b/src/main/java/ctbrec/ui/ThumbCell.css new file mode 100644 index 00000000..2ed299ba --- /dev/null +++ b/src/main/java/ctbrec/ui/ThumbCell.css @@ -0,0 +1,4 @@ +.selection-background { + /*-fx-fill: #0096C9;*/ + -fx-fill: -fx-accent; +} \ No newline at end of file diff --git a/src/main/java/ctbrec/ui/ThumbCell.java b/src/main/java/ctbrec/ui/ThumbCell.java index 2f9287ae..2ba2d8e8 100644 --- a/src/main/java/ctbrec/ui/ThumbCell.java +++ b/src/main/java/ctbrec/ui/ThumbCell.java @@ -24,24 +24,18 @@ import javafx.animation.Interpolator; import javafx.animation.ParallelTransition; import javafx.animation.Transition; import javafx.application.Platform; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ObservableList; -import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Alert; import javafx.scene.control.ContextMenu; -import javafx.scene.control.MenuItem; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.input.Clipboard; -import javafx.scene.input.ClipboardContent; -import javafx.scene.input.ContextMenuEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; @@ -69,6 +63,7 @@ public class ThumbCell extends StackPane { private Rectangle resolutionBackground; private Rectangle nameBackground; private Rectangle topicBackground; + private Rectangle selectionOverlay; private Text name; private Text topic; private Text resolutionTag; @@ -80,14 +75,13 @@ public class ThumbCell extends StackPane { private Color colorNormal = Color.BLACK; private Color colorHighlight = Color.WHITE; private Color colorRecording = new Color(0.8, 0.28, 0.28, 1); + private SimpleBooleanProperty selectionProperty = new SimpleBooleanProperty(false); private HttpClient client; - private ThumbOverviewTab parent; private ObservableList thumbCellList; public ThumbCell(ThumbOverviewTab parent, Model model, Recorder recorder, HttpClient client) { - this.parent = parent; this.thumbCellList = parent.grid.getChildren(); this.model = model; this.recorder = recorder; @@ -158,6 +152,14 @@ public class ThumbCell extends StackPane { recordingAnimation.setCycleCount(FadeTransition.INDEFINITE); recordingAnimation.setAutoReverse(true); + selectionOverlay = new Rectangle(); + //selectionOverlay.setFill(new Color(0, 150f/255, 201f/255, .75)); + //selectionOverlay.setStyle("-fx-background-color: -fx-accent"); + //selectionOverlay.getStyleClass().add("table-view"); + selectionOverlay.setOpacity(0); + StackPane.setAlignment(selectionOverlay, Pos.TOP_LEFT); + getChildren().add(selectionOverlay); + setOnMouseEntered((e) -> { new ParallelTransition(changeColor(nameBackground, colorNormal, colorHighlight), changeColor(name, colorHighlight, colorNormal)).playFromStart(); new ParallelTransition(changeOpacity(topicBackground, 0.7), changeOpacity(topic, 0.7)).playFromStart(); @@ -166,20 +168,6 @@ public class ThumbCell extends StackPane { new ParallelTransition(changeColor(nameBackground, colorHighlight, colorNormal), changeColor(name, colorNormal, colorHighlight)).playFromStart(); new ParallelTransition(changeOpacity(topicBackground, 0), changeOpacity(topic, 0)).playFromStart(); }); - setOnMouseClicked(doubleClickListener); - addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, event -> { - parent.suspendUpdates(true); - popup = createContextMenu(); - popup.show(ThumbCell.this, event.getScreenX(), event.getScreenY()); - popup.setOnHidden((e) -> parent.suspendUpdates(false)); - event.consume(); - }); - addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { - if(popup != null) { - popup.hide(); - } - }); - setThumbWidth(width); setRecording(recording); @@ -188,6 +176,20 @@ public class ThumbCell extends StackPane { } } + public void setSelected(boolean selected) { + selectionProperty.set(selected); + selectionOverlay.getStyleClass().add("selection-background"); + selectionOverlay.setOpacity(selected ? .75 : 0); + } + + public boolean isSelected() { + return selectionProperty.get(); + } + + public ObservableValue selectionProperty() { + return selectionProperty; + } + private void determineResolution() { if(ThumbOverviewTab.resolutionProcessing.contains(model)) { LOG.debug("Already fetching resolution for model {}. Queue size {}", model.getName(), ThumbOverviewTab.resolutionProcessing.size()); @@ -268,38 +270,6 @@ public class ThumbCell extends StackPane { } } - private ContextMenu createContextMenu() { - MenuItem openInPlayer = new MenuItem("Open in Player"); - openInPlayer.setOnAction((e) -> startPlayer()); - - MenuItem start = new MenuItem("Start Recording"); - start.setOnAction((e) -> startStopAction(true)); - MenuItem stop = new MenuItem("Stop Recording"); - stop.setOnAction((e) -> startStopAction(false)); - MenuItem startStop = recorder.isRecording(model) ? stop : start; - - MenuItem follow = new MenuItem("Follow"); - follow.setOnAction((e) -> follow(true)); - MenuItem unfollow = new MenuItem("Unfollow"); - unfollow.setOnAction((e) -> follow(false)); - - MenuItem copyUrl = new MenuItem("Copy URL"); - copyUrl.setOnAction((e) -> { - final Clipboard clipboard = Clipboard.getSystemClipboard(); - final ClipboardContent content = new ClipboardContent(); - content.putString(model.getUrl()); - clipboard.setContent(content); - }); - - ContextMenu contextMenu = new ContextMenu(); - contextMenu.setAutoHide(true); - contextMenu.setHideOnEscape(true); - contextMenu.setAutoFix(true); - MenuItem followOrUnFollow = parent instanceof FollowedTab ? unfollow : follow; - contextMenu.getItems().addAll(openInPlayer, startStop , followOrUnFollow, copyUrl); - return contextMenu; - } - private Transition changeColor(Shape shape, Color from, Color to) { FillTransition transition = new FillTransition(ANIMATION_DURATION, from, to); transition.setShape(shape); @@ -313,16 +283,7 @@ public class ThumbCell extends StackPane { return transition; } - private EventHandler doubleClickListener = new EventHandler() { - @Override - public void handle(MouseEvent e) { - if(e.getButton() == MouseButton.PRIMARY && e.getClickCount() == 2) { - startPlayer(); - } - } - }; - - private void startPlayer() { + void startPlayer() { // TODO if manual choice of stream quality is enabled, do the same thing as starting a download here?!? // or maybe not, because the player should automatically switch between resolutions depending on the // network bandwidth @@ -359,7 +320,7 @@ public class ThumbCell extends StackPane { recordingIndicator.setVisible(recording); } - private void startStopAction(boolean start) { + void startStopAction(boolean start) { setCursor(Cursor.WAIT); boolean selectSource = Config.getInstance().getSettings().chooseStreamQuality; @@ -408,7 +369,7 @@ public class ThumbCell extends StackPane { }.start(); } - private void follow(boolean follow) { + void follow(boolean follow) { setCursor(Cursor.WAIT); new Thread() { @Override @@ -553,5 +514,7 @@ public class ThumbCell extends StackPane { int margin = 4; topic.maxWidth(w-margin*2); topic.setWrappingWidth(w-margin*2); + selectionOverlay.setWidth(w); + selectionOverlay.setHeight(h); } } diff --git a/src/main/java/ctbrec/ui/ThumbOverviewTab.java b/src/main/java/ctbrec/ui/ThumbOverviewTab.java index db8d752e..3eff90dd 100644 --- a/src/main/java/ctbrec/ui/ThumbOverviewTab.java +++ b/src/main/java/ctbrec/ui/ThumbOverviewTab.java @@ -34,16 +34,24 @@ import javafx.concurrent.ScheduledService; import javafx.concurrent.Task; import javafx.concurrent.Worker.State; import javafx.concurrent.WorkerStateEvent; +import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Node; 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.TextField; import javafx.scene.control.Tooltip; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.ContextMenuEvent; +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; @@ -61,6 +69,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { ScheduledService> updateService; Recorder recorder; List filteredThumbCells = Collections.synchronizedList(new ArrayList<>()); + List selectedThumbCells = Collections.synchronizedList(new ArrayList<>()); String filter; FlowPane grid = new FlowPane(); ReentrantLock gridLock = new ReentrantLock(); @@ -73,6 +82,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { Button pagePrev = new Button("◀"); Button pageNext = new Button("▶"); private volatile boolean updatesSuspended = false; + ContextMenu popup; private ComboBox thumbWidth; @@ -236,7 +246,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } } if(!found) { - ThumbCell newCell = new ThumbCell(this, model, recorder, client); + ThumbCell newCell = createThumbCell(this, model, recorder, client); newCell.setIndex(index); positionChangedOrNew.add(newCell); } @@ -260,6 +270,126 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { } + private ThumbCell createThumbCell(ThumbOverviewTab thumbOverviewTab, Model model, Recorder recorder2, HttpClient client2) { + ThumbCell newCell = new ThumbCell(this, model, recorder, client); + 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) { + 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(cell)); + + MenuItem start = new MenuItem("Start Recording"); + start.setOnAction((e) -> startStopAction(cell, true)); + MenuItem stop = new MenuItem("Stop Recording"); + stop.setOnAction((e) -> startStopAction(cell, false)); + MenuItem startStop = recorder.isRecording(cell.getModel()) ? stop : start; + + MenuItem follow = new MenuItem("Follow"); + follow.setOnAction((e) -> follow(cell, true)); + MenuItem unfollow = new MenuItem("Unfollow"); + unfollow.setOnAction((e) -> follow(cell, false)); + + 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); + }); + + // check, if other cells are selected, too. in that case, we have to disable menu item, 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 + 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); + } else { + removeSelection(); + } + } + + ContextMenu contextMenu = new ContextMenu(); + contextMenu.setAutoHide(true); + contextMenu.setHideOnEscape(true); + contextMenu.setAutoFix(true); + MenuItem followOrUnFollow = this instanceof FollowedTab ? unfollow : follow; + contextMenu.getItems().addAll(openInPlayer, startStop , followOrUnFollow, copyUrl); + return contextMenu; + } + + private void follow(ThumbCell cell, boolean follow) { + if(selectedThumbCells.isEmpty()) { + cell.follow(follow); + } else { + for (ThumbCell thumbCell : selectedThumbCells) { + thumbCell.follow(follow); + } + } + } + + private void startStopAction(ThumbCell cell, boolean start) { + if(selectedThumbCells.isEmpty()) { + cell.startStopAction(start); + } else { + for (ThumbCell thumbCell : selectedThumbCells) { + thumbCell.startStopAction(start); + } + } + } + + private void startPlayer(ThumbCell cell) { + if(selectedThumbCells.isEmpty()) { + cell.startPlayer(); + } else { + for (ThumbCell thumbCell : selectedThumbCells) { + thumbCell.startPlayer(); + } + } + } + + private EventHandler mouseClickListener = new EventHandler() { + @Override + public void handle(MouseEvent 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.isShiftDown()) { + if(popup == null) { + cell.setSelected(!cell.isSelected()); + } + } else if (e.getButton() == MouseButton.PRIMARY) { + removeSelection(); + } + } + }; + protected void onFail(WorkerStateEvent event) { if(updatesSuspended) { return; @@ -309,6 +439,7 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { if(!matches(m, filter)) { iterator.remove(); filteredThumbCells.add(cell); + cell.setSelected(false); } } @@ -439,4 +570,10 @@ public class ThumbOverviewTab extends Tab implements TabSelectionListener { void suspendUpdates(boolean suspend) { this.updatesSuspended = suspend; } + + private void removeSelection() { + while(selectedThumbCells.size() > 0) { + selectedThumbCells.get(0).setSelected(false); + } + } }