/**
* erlyberly, erlang trace debugger
* Copyright (C) 2016 Andy Till
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package ui;
/*
* Copyright 2014 Jens Deters.
*
* 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.
*/
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.WritableImage;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Pane;
import javafx.scene.transform.Transform;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
/**
* A simple Utility to make all {@link Tab}s of a {@link TabPane} detachable.
* <br>
* <h1>Usage</h1>
* To get a {@link TabPane} in charge of control:
* <br>
* <b>Hint: Only already added {@link Tab}s are going to be in charge of control!</b>
* <pre>
* {@code
* TabPaneDude.create().makeTabsDetachable(myTapPane);
* }
* </pre> Tabs can then be detached simply by dragging a tab title to the desired window position.
*
/**
*
* @author Jens Deters (www.jensd.de)
* @version 1.0.0
* @since 14-10-2014
*/
public class TabPaneDetacher {
private TabPane tabPane;
private Tab currentTab;
private final List<Tab> originalTabs;
private final Map<Integer, Tab> tapTransferMap;
private String[] stylesheets;
private final BooleanProperty alwaysOnTop;
private TabPaneDetacher() {
originalTabs = new ArrayList<>();
stylesheets = new String[]{};
tapTransferMap = new HashMap<>();
alwaysOnTop = new SimpleBooleanProperty();
}
/**
* Creates a new instance of the TabPaneDetacher
*
* @return The new instance of the TabPaneDetacher.
*/
public static TabPaneDetacher create() {
return new TabPaneDetacher();
}
public BooleanProperty alwaysOnTopProperty() {
return alwaysOnTop;
}
public Boolean isAlwaysOnTop() {
return alwaysOnTop.get();
}
/**
*
* Sets whether detached Tabs should be always on top.
*
* @param alwaysOnTop The state to be set.
* @return The current TabPaneDetacher instance.
*/
public TabPaneDetacher alwaysOnTop(boolean alwaysOnTop){
alwaysOnTopProperty().set(alwaysOnTop);
return this;
}
/**
* Sets the stylesheets that should be assigend to the new created {@link Stage}.
*
* @param stylesheets The stylesheets to be set.
* @return The current TabPaneDetacher instance.
*/
public TabPaneDetacher stylesheets(String... stylesheets) {
this.stylesheets = stylesheets;
return this;
}
/**
* Make all added {@link Tab}s of the given {@link TabPane} detachable.
*
* @param tabPane The {@link TabPane} to take over.
* @return The current TabPaneDetacher instance.
*/
public TabPaneDetacher makeTabsDetachable(TabPane tabPane) {
this.tabPane = tabPane;
originalTabs.addAll(tabPane.getTabs());
for (int i = 0; i < tabPane.getTabs().size(); i++) {
tapTransferMap.put(i, tabPane.getTabs().get(i));
}
tabPane.getTabs().stream().forEach(t -> {
t.setClosable(false);
});
tabPane.getTabs().addListener((Observable o) -> {
for (Tab tabX : tabPane.getTabs()) {
if(!(tabX.getContent() instanceof Pane)) {
throw new RuntimeException("Tab added where the content node was not a subclass of Pane, this means it cannot be dragged by TabPaneDetacher.");
}
}
});
tabPane.setOnDragDetected(
(MouseEvent event) -> {
if (event.getSource() instanceof TabPane) {
Pane rootPane = (Pane) tabPane.getScene().getRoot();
rootPane.setOnDragOver((DragEvent event1) -> {
event1.acceptTransferModes(TransferMode.ANY);
event1.consume();
});
currentTab = tabPane.getSelectionModel().getSelectedItem();
SnapshotParameters snapshotParams = new SnapshotParameters();
snapshotParams.setTransform(Transform.scale(0.4, 0.4));
WritableImage snapshot = currentTab.getContent().snapshot(snapshotParams, null);
Dragboard db = tabPane.startDragAndDrop(TransferMode.MOVE);
ClipboardContent clipboardContent = new ClipboardContent();
clipboardContent.put(DataFormat.PLAIN_TEXT, "detach");
db.setDragView(snapshot, 40, 40);
db.setContent(clipboardContent);
}
event.consume();
}
);
tabPane.setOnDragDone(
(DragEvent event) -> {
openTabInStage(currentTab);
tabPane.setCursor(Cursor.DEFAULT);
event.consume();
}
);
return this;
}
/**
* Opens the content of the given {@link Tab} in a separate Stage. While the content is removed from the {@link Tab} it is
* added to the root of a new {@link Stage}. The Window title is set to the name of the {@link Tab};
*
* @param tab The {@link Tab} to get the content from.
*/
public void openTabInStage(final Tab tab) {
if(tab == null){
return;
}
int originalTab = originalTabs.indexOf(tab);
tapTransferMap.remove(originalTab);
Pane content = (Pane) tab.getContent();
if (content == null) {
throw new IllegalArgumentException("Can not detach Tab '" + tab.getText() + "': content is empty (null).");
}
tab.setContent(null);
final Scene scene = new Scene(content, content.getPrefWidth(), content.getPrefHeight());
scene.getStylesheets().addAll(stylesheets);
Stage stage = new Stage();
stage.setScene(scene);
stage.setTitle(tab.getText());
stage.setAlwaysOnTop(isAlwaysOnTop());
stage.setOnCloseRequest((WindowEvent t) -> {
stage.close();
tab.setContent(content);
int originalTabIndex = originalTabs.indexOf(tab);
tapTransferMap.put(originalTabIndex, tab);
int index = 0;
SortedSet<Integer> keys = new TreeSet<>(tapTransferMap.keySet());
for (Integer key : keys) {
Tab value = tapTransferMap.get(key);
if(!tabPane.getTabs().contains(value)){
tabPane.getTabs().add(index, value);
}
index++;
}
tabPane.getSelectionModel().select(tab);
});
stage.setOnShown((WindowEvent t) -> {
tab.getTabPane().getTabs().remove(tab);
});
CloseWindowOnEscape.apply(scene, stage);
stage.show();
}
}