tabs
This commit is contained in:
@@ -39,6 +39,9 @@ public record AppConfig(
|
|||||||
"new_pane",
|
"new_pane",
|
||||||
"next_floating",
|
"next_floating",
|
||||||
"close_pane",
|
"close_pane",
|
||||||
|
"new_tab",
|
||||||
|
"previous_tab",
|
||||||
|
"next_tab",
|
||||||
"open_font_selector",
|
"open_font_selector",
|
||||||
"open_scrollback"
|
"open_scrollback"
|
||||||
);
|
);
|
||||||
@@ -86,17 +89,20 @@ public record AppConfig(
|
|||||||
true,
|
true,
|
||||||
defaultScrollbackEditorCommand(),
|
defaultScrollbackEditorCommand(),
|
||||||
Map.of(),
|
Map.of(),
|
||||||
Map.of(
|
Map.ofEntries(
|
||||||
"navigate_left", KeyBinding.parse("ALT+H"),
|
Map.entry("navigate_left", KeyBinding.parse("ALT+H")),
|
||||||
"navigate_down", KeyBinding.parse("ALT+J"),
|
Map.entry("navigate_down", KeyBinding.parse("ALT+J")),
|
||||||
"navigate_up", KeyBinding.parse("ALT+K"),
|
Map.entry("navigate_up", KeyBinding.parse("ALT+K")),
|
||||||
"navigate_right", KeyBinding.parse("ALT+L"),
|
Map.entry("navigate_right", KeyBinding.parse("ALT+L")),
|
||||||
"toggle_floating", KeyBinding.parse("ALT+F"),
|
Map.entry("toggle_floating", KeyBinding.parse("ALT+F")),
|
||||||
"new_pane", KeyBinding.parse("ALT+N"),
|
Map.entry("new_pane", KeyBinding.parse("ALT+N")),
|
||||||
"next_floating", KeyBinding.parse("ALT+F12"),
|
Map.entry("next_floating", KeyBinding.parse("ALT+F12")),
|
||||||
"close_pane", KeyBinding.parse("ALT+X"),
|
Map.entry("close_pane", KeyBinding.parse("ALT+X")),
|
||||||
"open_font_selector", KeyBinding.parse("ALT+T"),
|
Map.entry("new_tab", KeyBinding.parse("ALT+A")),
|
||||||
"open_scrollback", KeyBinding.parse("ALT+S")
|
Map.entry("previous_tab", KeyBinding.parse("ALT+SHIFT+H")),
|
||||||
|
Map.entry("next_tab", KeyBinding.parse("ALT+SHIFT+L")),
|
||||||
|
Map.entry("open_font_selector", KeyBinding.parse("ALT+T")),
|
||||||
|
Map.entry("open_scrollback", KeyBinding.parse("ALT+S"))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.gregor.jprototerm;
|
|||||||
|
|
||||||
import javafx.animation.AnimationTimer;
|
import javafx.animation.AnimationTimer;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
@@ -80,6 +81,20 @@ public final class Main extends Application {
|
|||||||
} else if (config.keybindings().get("close_pane").matches(event)) {
|
} else if (config.keybindings().get("close_pane").matches(event)) {
|
||||||
workspace.closeActivePane();
|
workspace.closeActivePane();
|
||||||
event.consume();
|
event.consume();
|
||||||
|
if (workspace.isEmpty()) {
|
||||||
|
// Closing the last pane quits the app.
|
||||||
|
workspace.close();
|
||||||
|
Platform.exit();
|
||||||
|
}
|
||||||
|
} else if (config.keybindings().get("new_tab").matches(event)) {
|
||||||
|
workspace.newTab();
|
||||||
|
event.consume();
|
||||||
|
} else if (config.keybindings().get("previous_tab").matches(event)) {
|
||||||
|
workspace.previousTab();
|
||||||
|
event.consume();
|
||||||
|
} else if (config.keybindings().get("next_tab").matches(event)) {
|
||||||
|
workspace.nextTab();
|
||||||
|
event.consume();
|
||||||
} else if (config.keybindings().get("open_font_selector").matches(event)) {
|
} else if (config.keybindings().get("open_font_selector").matches(event)) {
|
||||||
openFontSelector();
|
openFontSelector();
|
||||||
event.consume();
|
event.consume();
|
||||||
|
|||||||
321
src/main/java/com/gregor/jprototerm/Tab.java
Normal file
321
src/main/java/com/gregor/jprototerm/Tab.java
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
package com.gregor.jprototerm;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One tab: an isolated stack of panes (tiled + floating) with its own active pane and
|
||||||
|
* stashed-floating state. {@link TerminalWorkspace} owns the list of tabs and renders only
|
||||||
|
* the current one. Mutating methods return whether they actually changed anything so the
|
||||||
|
* workspace can bump its render version conditionally.
|
||||||
|
*/
|
||||||
|
final class Tab implements AutoCloseable {
|
||||||
|
private final AppConfig config;
|
||||||
|
private final List<TerminalPane> panes = new ArrayList<>();
|
||||||
|
private int activeIndex;
|
||||||
|
private int hiddenFloatingFocusIndex = -1;
|
||||||
|
|
||||||
|
Tab(AppConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
panes.add(openPane(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalPane activePane() {
|
||||||
|
return panes.get(activeIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isEmpty() {
|
||||||
|
return panes.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TerminalPane> panes() {
|
||||||
|
if (panes.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<TerminalPane> visible = panes.stream().filter(TerminalPane::visible).toList();
|
||||||
|
TerminalPane active = activePane();
|
||||||
|
if (!active.visible() || !active.floating()) {
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TerminalPane> ordered = new ArrayList<>(visible.size());
|
||||||
|
visible.stream()
|
||||||
|
.filter(pane -> pane != active)
|
||||||
|
.forEach(ordered::add);
|
||||||
|
ordered.add(active);
|
||||||
|
return List.copyOf(ordered);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isActive(TerminalPane pane) {
|
||||||
|
return !panes.isEmpty() && activePane() == pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean focus(TerminalPane pane) {
|
||||||
|
int index = panes.indexOf(pane);
|
||||||
|
if (index >= 0 && pane.visible() && activeIndex != index) {
|
||||||
|
activeIndex = index;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void layout(double width, double height) {
|
||||||
|
List<TerminalPane> tiled = panes.stream()
|
||||||
|
.filter(TerminalPane::visible)
|
||||||
|
.filter(pane -> !pane.floating())
|
||||||
|
.toList();
|
||||||
|
int tileCount = Math.max(1, tiled.size());
|
||||||
|
double tileWidth = width / tileCount;
|
||||||
|
for (int i = 0; i < tiled.size(); i++) {
|
||||||
|
tiled.get(i).bounds(i * tileWidth, 0, tileWidth, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TerminalPane> floating = panes.stream()
|
||||||
|
.filter(TerminalPane::visible)
|
||||||
|
.filter(TerminalPane::floating)
|
||||||
|
.toList();
|
||||||
|
for (int i = 0; i < floating.size(); i++) {
|
||||||
|
TerminalPane pane = floating.get(i);
|
||||||
|
if (pane.visible() && pane.floating()) {
|
||||||
|
double floatingWidth = Math.max(420, width * 0.58);
|
||||||
|
double floatingHeight = Math.max(260, height * 0.58);
|
||||||
|
double offset = i * 28.0;
|
||||||
|
pane.bounds(
|
||||||
|
Math.min(width - floatingWidth - 12.0, ((width - floatingWidth) / 2.0) + offset),
|
||||||
|
Math.min(height - floatingHeight - 12.0, ((height - floatingHeight) / 2.0) + offset),
|
||||||
|
floatingWidth,
|
||||||
|
floatingHeight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean navigate(Direction direction) {
|
||||||
|
TerminalPane current = activePane();
|
||||||
|
if (current.floating() && navigateFloatingStack(direction)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
TerminalPane target = panes.stream()
|
||||||
|
.filter(TerminalPane::visible)
|
||||||
|
.filter(pane -> pane != current)
|
||||||
|
.filter(pane -> directionFilter(direction, current, pane))
|
||||||
|
.min(Comparator.comparingDouble(pane -> distance(current, pane)))
|
||||||
|
.orElse(null);
|
||||||
|
if (target != null) {
|
||||||
|
activeIndex = panes.indexOf(target);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleFloating() {
|
||||||
|
List<TerminalPane> floating = panes.stream()
|
||||||
|
.filter(TerminalPane::floating)
|
||||||
|
.toList();
|
||||||
|
if (floating.isEmpty()) {
|
||||||
|
createFloatingPane();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean anyVisible = floating.stream().anyMatch(TerminalPane::visible);
|
||||||
|
if (anyVisible) {
|
||||||
|
TerminalPane active = activePane();
|
||||||
|
hiddenFloatingFocusIndex = active.floating() ? activeIndex : firstVisibleFloatingIndex();
|
||||||
|
floating.forEach(pane -> pane.setVisible(false));
|
||||||
|
activeIndex = firstVisibleNonFloatingIndex();
|
||||||
|
} else {
|
||||||
|
floating.forEach(pane -> pane.setVisible(true));
|
||||||
|
activeIndex = visibleIndexOrFallback(hiddenFloatingFocusIndex, panes.indexOf(floating.get(floating.size() - 1)));
|
||||||
|
hiddenFloatingFocusIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "New pane": adds a floating pane while floating panes are shown, otherwise adds a
|
||||||
|
* tiled pane (the tiled row is redistributed equally by the layout).
|
||||||
|
*/
|
||||||
|
void createPane() {
|
||||||
|
if (anyFloatingVisible()) {
|
||||||
|
createFloatingPane();
|
||||||
|
} else {
|
||||||
|
TerminalPane pane = openPane(false);
|
||||||
|
panes.add(pane);
|
||||||
|
activeIndex = panes.size() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void nextFloatingPane() {
|
||||||
|
TerminalPane next = nextFloatingAfter(activeIndex);
|
||||||
|
next.setVisible(true);
|
||||||
|
activeIndex = panes.indexOf(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeActivePane() {
|
||||||
|
TerminalPane active = activePane();
|
||||||
|
int removed = activeIndex;
|
||||||
|
int previous = previousVisibleIndex(removed);
|
||||||
|
panes.remove(removed);
|
||||||
|
active.close();
|
||||||
|
if (panes.isEmpty()) {
|
||||||
|
activeIndex = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeIndex = adjustIndexAfterRemoval(previous, removed);
|
||||||
|
hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed);
|
||||||
|
// If only hidden panes remained (e.g. closed the last tiled pane while floating
|
||||||
|
// panes were stashed), reveal the one we're focusing so the screen isn't blank.
|
||||||
|
if (!panes.get(activeIndex).visible()) {
|
||||||
|
panes.get(activeIndex).setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createFloatingPane() {
|
||||||
|
TerminalPane pane = openPane(true);
|
||||||
|
panes.add(pane);
|
||||||
|
activeIndex = panes.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean anyFloatingVisible() {
|
||||||
|
return panes.stream().anyMatch(pane -> pane.floating() && pane.visible());
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerminalPane nextFloatingAfter(int index) {
|
||||||
|
for (int i = index + 1; i < panes.size(); i++) {
|
||||||
|
TerminalPane pane = panes.get(i);
|
||||||
|
if (pane.floating()) {
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i <= index && i < panes.size(); i++) {
|
||||||
|
TerminalPane pane = panes.get(i);
|
||||||
|
if (pane.floating()) {
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return createAndReturnFloatingPane();
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerminalPane createAndReturnFloatingPane() {
|
||||||
|
TerminalPane pane = openPane(true);
|
||||||
|
panes.add(pane);
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean navigateFloatingStack(Direction direction) {
|
||||||
|
List<TerminalPane> floating = panes.stream()
|
||||||
|
.filter(TerminalPane::visible)
|
||||||
|
.filter(TerminalPane::floating)
|
||||||
|
.toList();
|
||||||
|
if (floating.size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int current = floating.indexOf(activePane());
|
||||||
|
if (current < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int next = switch (direction) {
|
||||||
|
case LEFT, UP -> current - 1;
|
||||||
|
case DOWN, RIGHT -> current + 1;
|
||||||
|
};
|
||||||
|
if (next < 0 || next >= floating.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeIndex = panes.indexOf(floating.get(next));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int firstVisibleFloatingIndex() {
|
||||||
|
for (int i = 0; i < panes.size(); i++) {
|
||||||
|
TerminalPane pane = panes.get(i);
|
||||||
|
if (pane.visible() && pane.floating()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int firstVisibleNonFloatingIndex() {
|
||||||
|
for (int i = 0; i < panes.size(); i++) {
|
||||||
|
TerminalPane pane = panes.get(i);
|
||||||
|
if (pane.visible() && !pane.floating()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int previousVisibleIndex(int index) {
|
||||||
|
for (int i = index - 1; i >= 0; i--) {
|
||||||
|
if (panes.get(i).visible()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = index + 1; i < panes.size(); i++) {
|
||||||
|
if (panes.get(i).visible()) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstVisibleNonFloatingIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int visibleIndexOrFallback(int index, int fallback) {
|
||||||
|
if (index >= 0 && index < panes.size() && panes.get(index).visible()) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int adjustIndexAfterRemoval(int index, int removedIndex) {
|
||||||
|
if (index < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return index > removedIndex ? index - 1 : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int adjustHiddenFocusAfterRemoval(int index, int removedIndex) {
|
||||||
|
if (index < 0 || index == removedIndex) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return index > removedIndex ? index - 1 : index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TerminalPane openPane(boolean floating) {
|
||||||
|
TerminalPane pane = TerminalPane.create(config.columns(), config.rows(), config.maxScrollback());
|
||||||
|
pane.setFloating(floating);
|
||||||
|
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, config.columns(), config.rows()));
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean directionFilter(Direction direction, TerminalPane current, TerminalPane candidate) {
|
||||||
|
double currentCenterX = current.x() + current.width() / 2.0;
|
||||||
|
double currentCenterY = current.y() + current.height() / 2.0;
|
||||||
|
double candidateCenterX = candidate.x() + candidate.width() / 2.0;
|
||||||
|
double candidateCenterY = candidate.y() + candidate.height() / 2.0;
|
||||||
|
|
||||||
|
return switch (direction) {
|
||||||
|
case LEFT -> candidateCenterX < currentCenterX;
|
||||||
|
case DOWN -> candidateCenterY > currentCenterY;
|
||||||
|
case UP -> candidateCenterY < currentCenterY;
|
||||||
|
case RIGHT -> candidateCenterX > currentCenterX;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double distance(TerminalPane current, TerminalPane candidate) {
|
||||||
|
double dx = (current.x() + current.width() / 2.0) - (candidate.x() + candidate.width() / 2.0);
|
||||||
|
double dy = (current.y() + current.height() / 2.0) - (candidate.y() + candidate.height() / 2.0);
|
||||||
|
return Math.sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
for (TerminalPane pane : panes) {
|
||||||
|
pane.close();
|
||||||
|
}
|
||||||
|
panes.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,312 +1,131 @@
|
|||||||
package com.gregor.jprototerm;
|
package com.gregor.jprototerm;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the tabs and renders only the current one. Pane operations delegate to the current
|
||||||
|
* tab; tab operations switch which tab is current. A single render version bumps on any
|
||||||
|
* change (intra-tab or tab switch) so the renderer recomposites when needed.
|
||||||
|
*/
|
||||||
public final class TerminalWorkspace implements AutoCloseable {
|
public final class TerminalWorkspace implements AutoCloseable {
|
||||||
private final AppConfig config;
|
private final AppConfig config;
|
||||||
private final List<TerminalPane> panes = new ArrayList<>();
|
private final List<Tab> tabs = new ArrayList<>();
|
||||||
private int activeIndex;
|
private int currentTab;
|
||||||
private int hiddenFloatingFocusIndex = -1;
|
|
||||||
private long version;
|
private long version;
|
||||||
|
|
||||||
public TerminalWorkspace(AppConfig config) {
|
public TerminalWorkspace(AppConfig config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
panes.add(openPane(false));
|
tabs.add(new Tab(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TerminalPane activePane() {
|
private Tab current() {
|
||||||
return panes.get(activeIndex);
|
return tabs.get(currentTab);
|
||||||
}
|
|
||||||
|
|
||||||
public List<TerminalPane> panes() {
|
|
||||||
List<TerminalPane> visible = panes.stream().filter(TerminalPane::visible).toList();
|
|
||||||
TerminalPane active = activePane();
|
|
||||||
if (!active.visible() || !active.floating()) {
|
|
||||||
return visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<TerminalPane> ordered = new ArrayList<>(visible.size());
|
|
||||||
visible.stream()
|
|
||||||
.filter(pane -> pane != active)
|
|
||||||
.forEach(ordered::add);
|
|
||||||
ordered.add(active);
|
|
||||||
return List.copyOf(ordered);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isActive(TerminalPane pane) {
|
|
||||||
return activePane() == pane;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long version() {
|
public long version() {
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void focus(TerminalPane pane) {
|
public boolean isEmpty() {
|
||||||
int index = panes.indexOf(pane);
|
return tabs.isEmpty();
|
||||||
if (index >= 0 && pane.visible() && activeIndex != index) {
|
|
||||||
activeIndex = index;
|
|
||||||
version++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TerminalPane activePane() {
|
||||||
|
return current().activePane();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<TerminalPane> panes() {
|
||||||
|
return tabs.isEmpty() ? List.of() : current().panes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isActive(TerminalPane pane) {
|
||||||
|
return !tabs.isEmpty() && current().isActive(pane);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void layout(double width, double height) {
|
public void layout(double width, double height) {
|
||||||
List<TerminalPane> tiled = panes.stream()
|
if (!tabs.isEmpty()) {
|
||||||
.filter(TerminalPane::visible)
|
current().layout(width, height);
|
||||||
.filter(pane -> !pane.floating())
|
}
|
||||||
.toList();
|
|
||||||
int tileCount = Math.max(1, tiled.size());
|
|
||||||
double tileWidth = width / tileCount;
|
|
||||||
for (int i = 0; i < tiled.size(); i++) {
|
|
||||||
tiled.get(i).bounds(i * tileWidth, 0, tileWidth, height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<TerminalPane> floating = panes.stream()
|
public void focus(TerminalPane pane) {
|
||||||
.filter(TerminalPane::visible)
|
if (!tabs.isEmpty() && current().focus(pane)) {
|
||||||
.filter(TerminalPane::floating)
|
version++;
|
||||||
.toList();
|
|
||||||
for (int i = 0; i < floating.size(); i++) {
|
|
||||||
TerminalPane pane = floating.get(i);
|
|
||||||
if (pane.visible() && pane.floating()) {
|
|
||||||
double floatingWidth = Math.max(420, width * 0.58);
|
|
||||||
double floatingHeight = Math.max(260, height * 0.58);
|
|
||||||
double offset = i * 28.0;
|
|
||||||
pane.bounds(
|
|
||||||
Math.min(width - floatingWidth - 12.0, ((width - floatingWidth) / 2.0) + offset),
|
|
||||||
Math.min(height - floatingHeight - 12.0, ((height - floatingHeight) / 2.0) + offset),
|
|
||||||
floatingWidth,
|
|
||||||
floatingHeight
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void navigate(Direction direction) {
|
public void navigate(Direction direction) {
|
||||||
TerminalPane current = activePane();
|
if (!tabs.isEmpty() && current().navigate(direction)) {
|
||||||
if (current.floating() && navigateFloatingStack(direction)) {
|
|
||||||
version++;
|
version++;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
panes.stream()
|
|
||||||
.filter(TerminalPane::visible)
|
|
||||||
.filter(pane -> pane != current)
|
|
||||||
.filter(pane -> directionFilter(direction, current, pane))
|
|
||||||
.min(Comparator.comparingDouble(pane -> distance(current, pane)))
|
|
||||||
.ifPresent(pane -> {
|
|
||||||
activeIndex = panes.indexOf(pane);
|
|
||||||
version++;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleFloating() {
|
public void toggleFloating() {
|
||||||
List<TerminalPane> floating = panes.stream()
|
if (tabs.isEmpty()) {
|
||||||
.filter(TerminalPane::floating)
|
|
||||||
.toList();
|
|
||||||
if (floating.isEmpty()) {
|
|
||||||
createFloatingPane();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
current().toggleFloating();
|
||||||
boolean anyVisible = floating.stream().anyMatch(TerminalPane::visible);
|
|
||||||
if (anyVisible) {
|
|
||||||
TerminalPane active = activePane();
|
|
||||||
hiddenFloatingFocusIndex = active.floating() ? activeIndex : firstVisibleFloatingIndex();
|
|
||||||
floating.forEach(pane -> pane.setVisible(false));
|
|
||||||
activeIndex = firstVisibleNonFloatingIndex();
|
|
||||||
version++;
|
|
||||||
} else {
|
|
||||||
floating.forEach(pane -> pane.setVisible(true));
|
|
||||||
activeIndex = visibleIndexOrFallback(hiddenFloatingFocusIndex, panes.indexOf(floating.get(floating.size() - 1)));
|
|
||||||
hiddenFloatingFocusIndex = -1;
|
|
||||||
version++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void createFloatingPane() {
|
|
||||||
TerminalPane pane = openPane(true);
|
|
||||||
panes.add(pane);
|
|
||||||
activeIndex = panes.size() - 1;
|
|
||||||
version++;
|
version++;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* "New pane": adds a floating pane while floating panes are shown, otherwise adds a
|
|
||||||
* tiled pane (the tiled row is redistributed equally by the layout).
|
|
||||||
*/
|
|
||||||
public void createPane() {
|
public void createPane() {
|
||||||
if (anyFloatingVisible()) {
|
if (tabs.isEmpty()) {
|
||||||
createFloatingPane();
|
return;
|
||||||
} else {
|
}
|
||||||
TerminalPane pane = openPane(false);
|
current().createPane();
|
||||||
panes.add(pane);
|
|
||||||
activeIndex = panes.size() - 1;
|
|
||||||
version++;
|
version++;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private boolean anyFloatingVisible() {
|
|
||||||
return panes.stream().anyMatch(pane -> pane.floating() && pane.visible());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void nextFloatingPane() {
|
public void nextFloatingPane() {
|
||||||
TerminalPane next = nextFloatingAfter(activeIndex);
|
if (tabs.isEmpty()) {
|
||||||
next.setVisible(true);
|
return;
|
||||||
activeIndex = panes.indexOf(next);
|
}
|
||||||
|
current().nextFloatingPane();
|
||||||
version++;
|
version++;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeActivePane() {
|
public void closeActivePane() {
|
||||||
TerminalPane active = activePane();
|
if (tabs.isEmpty()) {
|
||||||
if (!active.floating() || panes.stream().filter(pane -> !pane.floating()).count() == 0) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
current().closeActivePane();
|
||||||
int removed = activeIndex;
|
if (current().isEmpty()) {
|
||||||
int previous = previousVisibleIndex(removed);
|
// Closing a tab's last pane closes the tab. When no tabs remain the workspace
|
||||||
panes.remove(removed);
|
// is empty and Main quits.
|
||||||
active.close();
|
tabs.remove(currentTab);
|
||||||
activeIndex = adjustIndexAfterRemoval(previous, removed);
|
if (currentTab >= tabs.size()) {
|
||||||
hiddenFloatingFocusIndex = adjustHiddenFocusAfterRemoval(hiddenFloatingFocusIndex, removed);
|
currentTab = Math.max(0, tabs.size() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
version++;
|
version++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private TerminalPane nextFloatingAfter(int index) {
|
public void newTab() {
|
||||||
for (int i = index + 1; i < panes.size(); i++) {
|
tabs.add(new Tab(config));
|
||||||
TerminalPane pane = panes.get(i);
|
currentTab = tabs.size() - 1;
|
||||||
if (pane.floating()) {
|
version++;
|
||||||
return pane;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = 0; i <= index && i < panes.size(); i++) {
|
|
||||||
TerminalPane pane = panes.get(i);
|
|
||||||
if (pane.floating()) {
|
|
||||||
return pane;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return createAndReturnFloatingPane();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private TerminalPane createAndReturnFloatingPane() {
|
public void nextTab() {
|
||||||
TerminalPane pane = openPane(true);
|
if (tabs.size() > 1) {
|
||||||
panes.add(pane);
|
currentTab = (currentTab + 1) % tabs.size();
|
||||||
return pane;
|
version++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean navigateFloatingStack(Direction direction) {
|
public void previousTab() {
|
||||||
List<TerminalPane> floating = panes.stream()
|
if (tabs.size() > 1) {
|
||||||
.filter(TerminalPane::visible)
|
currentTab = (currentTab - 1 + tabs.size()) % tabs.size();
|
||||||
.filter(TerminalPane::floating)
|
version++;
|
||||||
.toList();
|
|
||||||
if (floating.size() < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int current = floating.indexOf(activePane());
|
|
||||||
if (current < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int next = switch (direction) {
|
|
||||||
case LEFT, UP -> current - 1;
|
|
||||||
case DOWN, RIGHT -> current + 1;
|
|
||||||
};
|
|
||||||
if (next < 0 || next >= floating.size()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
activeIndex = panes.indexOf(floating.get(next));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int firstVisibleFloatingIndex() {
|
|
||||||
for (int i = 0; i < panes.size(); i++) {
|
|
||||||
TerminalPane pane = panes.get(i);
|
|
||||||
if (pane.visible() && pane.floating()) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int firstVisibleNonFloatingIndex() {
|
|
||||||
for (int i = 0; i < panes.size(); i++) {
|
|
||||||
TerminalPane pane = panes.get(i);
|
|
||||||
if (pane.visible() && !pane.floating()) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int previousVisibleIndex(int index) {
|
|
||||||
for (int i = index - 1; i >= 0; i--) {
|
|
||||||
if (panes.get(i).visible()) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (int i = index + 1; i < panes.size(); i++) {
|
|
||||||
if (panes.get(i).visible()) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return firstVisibleNonFloatingIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int visibleIndexOrFallback(int index, int fallback) {
|
|
||||||
if (index >= 0 && index < panes.size() && panes.get(index).visible()) {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int adjustIndexAfterRemoval(int index, int removedIndex) {
|
|
||||||
if (index < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return index > removedIndex ? index - 1 : index;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int adjustHiddenFocusAfterRemoval(int index, int removedIndex) {
|
|
||||||
if (index < 0 || index == removedIndex) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return index > removedIndex ? index - 1 : index;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TerminalPane openPane(boolean floating) {
|
|
||||||
TerminalPane pane = TerminalPane.create(config.columns(), config.rows(), config.maxScrollback());
|
|
||||||
pane.setFloating(floating);
|
|
||||||
pane.attach(ShellSession.start(config.shell(), config.envOverride(), pane, config.columns(), config.rows()));
|
|
||||||
return pane;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean directionFilter(Direction direction, TerminalPane current, TerminalPane candidate) {
|
|
||||||
double currentCenterX = current.x() + current.width() / 2.0;
|
|
||||||
double currentCenterY = current.y() + current.height() / 2.0;
|
|
||||||
double candidateCenterX = candidate.x() + candidate.width() / 2.0;
|
|
||||||
double candidateCenterY = candidate.y() + candidate.height() / 2.0;
|
|
||||||
|
|
||||||
return switch (direction) {
|
|
||||||
case LEFT -> candidateCenterX < currentCenterX;
|
|
||||||
case DOWN -> candidateCenterY > currentCenterY;
|
|
||||||
case UP -> candidateCenterY < currentCenterY;
|
|
||||||
case RIGHT -> candidateCenterX > currentCenterX;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double distance(TerminalPane current, TerminalPane candidate) {
|
|
||||||
double dx = (current.x() + current.width() / 2.0) - (candidate.x() + candidate.width() / 2.0);
|
|
||||||
double dy = (current.y() + current.height() / 2.0) - (candidate.y() + candidate.height() / 2.0);
|
|
||||||
return Math.sqrt(dx * dx + dy * dy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
||||||
for (TerminalPane pane : panes) {
|
for (Tab tab : tabs) {
|
||||||
pane.close();
|
tab.close();
|
||||||
}
|
}
|
||||||
panes.clear();
|
tabs.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user