From 45e569e08a24ed5e5c3fc8eac5237dd460a205b3 Mon Sep 17 00:00:00 2001 From: 0xboobface <0xboobface@gmail.com> Date: Mon, 24 Sep 2018 20:00:16 +0200 Subject: [PATCH] Add multi-selection to ThumbOverviewTab Clicking while holding shift now selects a ThumbCell and allows the multiple ThumbCells at one. The actions in the context menu are applied to all selected models. "copy url" is disabled, if multiple models are selected, because that doesn't make sense. --- .../java/ctbrec/ui/CtbrecApplication.java | 1 + src/main/java/ctbrec/ui/ThumbCell.css | 4 + src/main/java/ctbrec/ui/ThumbCell.java | 97 ++++-------- src/main/java/ctbrec/ui/ThumbOverviewTab.java | 139 +++++++++++++++++- 4 files changed, 173 insertions(+), 68 deletions(-) create mode 100644 src/main/java/ctbrec/ui/ThumbCell.css 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); + } + } }