mirror of
https://github.com/wavetermdev/backup.git
synced 2026-04-22 15:26:58 -07:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c44d11cf54 | |||
| 6dec479133 | |||
| bfa549ef22 | |||
| 376e339dfe | |||
| 646e260488 | |||
| d3c48e3a3e | |||
| c0c53edb84 | |||
| 923cf71e0a | |||
| f705a4df0a | |||
| 75a82de5bf | |||
| 5ae6a32222 | |||
| ef27b074db | |||
| 45244a1fb9 |
@@ -1,7 +1,8 @@
|
||||
[
|
||||
{
|
||||
"command": "system:toggleDeveloperTools",
|
||||
"keys": ["Cmd:Option:i"]
|
||||
"keys": ["Cmd:Option:i"],
|
||||
"info": "Opens the chrome developer tool menu"
|
||||
},
|
||||
{
|
||||
"command": "system:hideWindow",
|
||||
@@ -15,10 +16,62 @@
|
||||
"command": "generic:confirm",
|
||||
"keys": ["Enter"]
|
||||
},
|
||||
{
|
||||
"command": "generic:expandTextInput",
|
||||
"keys": ["Shift:Enter", "Ctrl:Enter"]
|
||||
},
|
||||
{
|
||||
"command": "generic:deleteItem",
|
||||
"keys": ["Backspace", "Delete"]
|
||||
},
|
||||
{
|
||||
"command": "generic:space",
|
||||
"keys": ["Space"]
|
||||
},
|
||||
{
|
||||
"command": "generic:tab",
|
||||
"keys": ["Tab"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-0",
|
||||
"keys": ["0"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-1",
|
||||
"keys": ["1"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-2",
|
||||
"keys": ["2"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-3",
|
||||
"keys": ["3"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-4",
|
||||
"keys": ["4"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-5",
|
||||
"keys": ["5"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-6",
|
||||
"keys": ["6"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-7",
|
||||
"keys": ["7"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-8",
|
||||
"keys": ["8"]
|
||||
},
|
||||
{
|
||||
"command": "generic:numpad-9",
|
||||
"keys": ["9"]
|
||||
},
|
||||
{
|
||||
"command": "generic:selectAbove",
|
||||
"keys": ["ArrowUp"]
|
||||
@@ -27,6 +80,14 @@
|
||||
"command": "generic:selectBelow",
|
||||
"keys": ["ArrowDown"]
|
||||
},
|
||||
{
|
||||
"command": "generic:selectLeft",
|
||||
"keys": ["ArrowLeft"]
|
||||
},
|
||||
{
|
||||
"command": "generic:selectRight",
|
||||
"keys": ["ArrowRight"]
|
||||
},
|
||||
{
|
||||
"command": "generic:selectPageAbove",
|
||||
"keys": ["PageUp"]
|
||||
@@ -67,7 +128,8 @@
|
||||
},
|
||||
{
|
||||
"command": "app:restartCommand",
|
||||
"keys": ["Cmd:r"]
|
||||
"keys": ["Cmd:r"],
|
||||
"info": "Restarts the command running in the current selected line"
|
||||
},
|
||||
{
|
||||
"command": "app:restartLastCommand",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React, { useState, useEffect, useRef, createRef } from "react";
|
||||
import * as mobx from "mobx";
|
||||
import ReactDOM from "react-dom";
|
||||
import dayjs from "dayjs";
|
||||
import cs from "classnames";
|
||||
import { Button } from "@/elements";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import "./datepicker.less";
|
||||
|
||||
@@ -36,6 +39,10 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
||||
MM: selDate.format("MM"),
|
||||
DD: selDate.format("DD"),
|
||||
});
|
||||
let curUuid = uuidv4();
|
||||
let keybindsRegistered = mobx.observable.box(false, {
|
||||
name: "datepicker-keybinds-registered",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
inputRefs.current = {
|
||||
@@ -273,6 +280,11 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
||||
setShowYearAccordion(false);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
setShowYearAccordion(false);
|
||||
};
|
||||
|
||||
const dayPickerModal = isOpen
|
||||
? ReactDOM.createPortal(
|
||||
<div ref={modalRef} className="day-picker-modal" style={calculatePosition()}>
|
||||
@@ -300,13 +312,13 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
||||
}
|
||||
};
|
||||
|
||||
const handleArrowNavigation = (event, currentPart) => {
|
||||
const handleArrowNavigation = (key, currentPart) => {
|
||||
const currentIndex = formatParts.indexOf(currentPart);
|
||||
let targetInput;
|
||||
|
||||
if (event.key === "ArrowLeft" && currentIndex > 0) {
|
||||
if (key == "ArrowLeft" && currentIndex > 0) {
|
||||
targetInput = inputRefs.current[formatParts[currentIndex - 1]].current;
|
||||
} else if (event.key === "ArrowRight" && currentIndex < formatParts.length - 1) {
|
||||
} else if (key == "ArrowRight" && currentIndex < formatParts.length - 1) {
|
||||
targetInput = inputRefs.current[formatParts[currentIndex + 1]].current;
|
||||
}
|
||||
|
||||
@@ -315,51 +327,91 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event, currentPart) => {
|
||||
const key = event.key;
|
||||
const handleKeyDown = (event, currentPart) => {};
|
||||
|
||||
if (key === "ArrowLeft" || key === "ArrowRight") {
|
||||
// Handle arrow navigation without selecting text
|
||||
handleArrowNavigation(event, currentPart);
|
||||
const handleFocus = (event, part) => {
|
||||
event.target.select();
|
||||
registerKeybindings(event, part);
|
||||
};
|
||||
|
||||
const registerKeybindings = (event: any, part: string) => {
|
||||
if (keybindsRegistered.get() == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === " ") {
|
||||
// Handle spacebar press to toggle the modal
|
||||
mobx.action(() => {
|
||||
keybindsRegistered.set(true);
|
||||
})();
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
let domain = "datepicker-" + curUuid + "-" + part;
|
||||
keybindManager.registerKeybinding("control", domain, "generic:selectLeft", (waveEvent) => {
|
||||
handleArrowNavigation("ArrowLeft", part);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:selectRight", (waveEvent) => {
|
||||
handleArrowNavigation("ArrowRight", part);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:space", (waveEvent) => {
|
||||
toggleModal();
|
||||
return;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
|
||||
toggleModal();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
|
||||
closeModal();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:tab", (waveEvent) => {
|
||||
handleArrowNavigation("ArrowRight", part);
|
||||
return true;
|
||||
});
|
||||
for (let numpadKey = 0; numpadKey <= 9; numpadKey++) {
|
||||
keybindManager.registerKeybinding(
|
||||
"control",
|
||||
domain,
|
||||
"generic:numpad-" + numpadKey.toString(),
|
||||
(waveEvent) => {
|
||||
let currentPart = part;
|
||||
const maxLength = currentPart === "YYYY" ? 4 : 2;
|
||||
const newValue = event.target.value.length < maxLength ? event.target.value + numpadKey : numpadKey;
|
||||
let selectionTimeoutId = null;
|
||||
handleDatePartChange(currentPart, newValue);
|
||||
|
||||
if (key.match(/[0-9]/)) {
|
||||
// Handle numeric keys
|
||||
event.preventDefault();
|
||||
const maxLength = currentPart === "YYYY" ? 4 : 2;
|
||||
const newValue = event.target.value.length < maxLength ? event.target.value + key : key;
|
||||
let selectionTimeoutId = null;
|
||||
handleDatePartChange(currentPart, newValue);
|
||||
// Clear any existing timeout
|
||||
if (selectionTimeoutId !== null) {
|
||||
clearTimeout(selectionTimeoutId);
|
||||
}
|
||||
|
||||
// Clear any existing timeout
|
||||
if (selectionTimeoutId !== null) {
|
||||
clearTimeout(selectionTimeoutId);
|
||||
}
|
||||
|
||||
// Re-focus and select the input after state update
|
||||
selectionTimeoutId = setTimeout(() => {
|
||||
event.target.focus();
|
||||
event.target.select();
|
||||
}, 0);
|
||||
// Re-focus and select the input after state update
|
||||
selectionTimeoutId = setTimeout(() => {
|
||||
event.target.focus();
|
||||
event.target.select();
|
||||
}, 0);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (event) => {
|
||||
event.target.select();
|
||||
const handleBlur = (event, part) => {
|
||||
unregisterKeybindings(part);
|
||||
};
|
||||
|
||||
const unregisterKeybindings = (part) => {
|
||||
mobx.action(() => {
|
||||
keybindsRegistered.set(false);
|
||||
})();
|
||||
let domain = "datepicker-" + curUuid + "-" + part;
|
||||
GlobalModel.keybindManager.unregisterDomain(domain);
|
||||
};
|
||||
|
||||
// Prevent use from selecting text in the input
|
||||
const handleMouseDown = (event) => {
|
||||
const handleMouseDown = (event, part) => {
|
||||
event.preventDefault();
|
||||
|
||||
handleFocus(event);
|
||||
handleFocus(event, part);
|
||||
};
|
||||
|
||||
const handleIconKeyDown = (event) => {
|
||||
@@ -413,8 +465,9 @@ const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/Y
|
||||
value={dateParts[part]}
|
||||
onChange={(e) => handleDatePartChange(part, e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, part)}
|
||||
onMouseDown={handleMouseDown}
|
||||
onFocus={handleFocus}
|
||||
onMouseDown={(e) => handleMouseDown(e, part)}
|
||||
onFocus={(e) => handleFocus(e, part)}
|
||||
onBlur={(e) => handleBlur(e, part)}
|
||||
maxLength={part === "YYYY" ? 4 : 2}
|
||||
className="date-input"
|
||||
placeholder={part}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import ReactDOM from "react-dom";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { GlobalModel } from "@/models";
|
||||
|
||||
import "./dropdown.less";
|
||||
|
||||
@@ -39,6 +41,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||
wrapperRef: React.RefObject<HTMLDivElement>;
|
||||
menuRef: React.RefObject<HTMLDivElement>;
|
||||
timeoutId: any;
|
||||
curUuid: string;
|
||||
|
||||
constructor(props: DropdownProps) {
|
||||
super(props);
|
||||
@@ -50,6 +53,7 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||
};
|
||||
this.wrapperRef = React.createRef();
|
||||
this.menuRef = React.createRef();
|
||||
this.curUuid == uuidv4();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -102,49 +106,76 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||
@boundMethod
|
||||
handleFocus() {
|
||||
this.setState({ isTouched: true });
|
||||
this.registerKeybindings();
|
||||
}
|
||||
|
||||
registerKeybindings() {
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
let domain = "dropdown-" + this.curUuid;
|
||||
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
|
||||
this.handleConfirm();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:space", (waveEvent) => {
|
||||
this.handleConfirm();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
|
||||
this.setState({ isOpen: false });
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:selectAbove", (waveEvent) => {
|
||||
const { isOpen } = this.state;
|
||||
const { options } = this.props;
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:selectBelow", (waveEvent) => {
|
||||
const { isOpen } = this.state;
|
||||
const { options } = this.props;
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:tab", (waveEvent) => {
|
||||
this.setState({ isOpen: false });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
const { options } = this.props;
|
||||
const { isOpen, highlightedIndex } = this.state;
|
||||
if (isOpen) {
|
||||
const option = options[highlightedIndex];
|
||||
if (option) {
|
||||
this.handleSelect(option.value, undefined);
|
||||
}
|
||||
} else {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.unregisterKeybindings();
|
||||
}
|
||||
|
||||
unregisterKeybindings() {
|
||||
let domain = "dropdown-" + this.curUuid;
|
||||
GlobalModel.keybindManager.unregisterDomain(domain);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(event: React.KeyboardEvent) {
|
||||
const { options } = this.props;
|
||||
const { isOpen, highlightedIndex } = this.state;
|
||||
|
||||
switch (event.key) {
|
||||
case "Enter":
|
||||
case " ":
|
||||
if (isOpen) {
|
||||
const option = options[highlightedIndex];
|
||||
if (option) {
|
||||
this.handleSelect(option.value, undefined);
|
||||
}
|
||||
} else {
|
||||
this.toggleDropdown();
|
||||
}
|
||||
break;
|
||||
case "Escape":
|
||||
this.setState({ isOpen: false });
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "ArrowDown":
|
||||
if (isOpen) {
|
||||
this.setState((prevState) => ({
|
||||
highlightedIndex:
|
||||
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "Tab":
|
||||
this.setState({ isOpen: false });
|
||||
break;
|
||||
}
|
||||
}
|
||||
handleKeyDown(event: React.KeyboardEvent) {}
|
||||
|
||||
@boundMethod
|
||||
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
|
||||
@@ -228,7 +259,8 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||
tabIndex={0}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onClick={this.handleClick}
|
||||
onFocus={this.handleFocus}
|
||||
onFocus={this.handleFocus.bind(this)}
|
||||
onBlur={this.handleBlur.bind(this)}
|
||||
>
|
||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||
<If condition={label}>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import "./inlinesettingstextedit.less";
|
||||
|
||||
@@ -27,6 +29,11 @@ class InlineSettingsTextEdit extends React.Component<
|
||||
tempText: OV<string>;
|
||||
shouldFocus: boolean = false;
|
||||
inputRef: React.RefObject<any> = React.createRef();
|
||||
curId: string;
|
||||
|
||||
componentDidMount(): void {
|
||||
this.curId = uuidv4();
|
||||
}
|
||||
|
||||
componentDidUpdate(): void {
|
||||
if (this.shouldFocus) {
|
||||
@@ -52,6 +59,7 @@ class InlineSettingsTextEdit extends React.Component<
|
||||
this.tempText = null;
|
||||
this.props.onChange(newText);
|
||||
})();
|
||||
this.unregisterKeybindings();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@@ -60,24 +68,38 @@ class InlineSettingsTextEdit extends React.Component<
|
||||
this.isEditing.set(false);
|
||||
this.tempText = null;
|
||||
})();
|
||||
this.unregisterKeybindings();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
handleKeyDown(e: any): void {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleFocus() {
|
||||
this.registerKeybindings();
|
||||
}
|
||||
|
||||
registerKeybindings() {
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
let domain = "inline-settings" + this.curId;
|
||||
keybindManager.registerKeybinding("control", domain, "generic:confirm", (waveEvent) => {
|
||||
this.confirmChange();
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("control", domain, "generic:cancel", (waveEvent) => {
|
||||
this.cancelChange();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
unregisterKeybindings() {
|
||||
let domain = "inline-settings" + this.curId;
|
||||
GlobalModel.keybindManager.unregisterDomain(domain);
|
||||
}
|
||||
|
||||
handleBlur() {
|
||||
this.unregisterKeybindings();
|
||||
this.cancelChange();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.unregisterKeybindings();
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@@ -99,7 +121,8 @@ class InlineSettingsTextEdit extends React.Component<
|
||||
ref={this.inputRef}
|
||||
className="input"
|
||||
type="text"
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onFocus={this.handleFocus.bind(this)}
|
||||
onBlur={this.handleBlur.bind(this)}
|
||||
placeholder={this.props.placeholder}
|
||||
onChange={this.handleChangeText}
|
||||
value={this.tempText.get()}
|
||||
|
||||
@@ -6,8 +6,11 @@ import * as mobx from "mobx";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Button } from "./button";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { GlobalModel } from "@/models";
|
||||
|
||||
import "./modal.less";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
|
||||
interface ModalHeaderProps {
|
||||
onClose?: () => void;
|
||||
@@ -30,10 +33,52 @@ interface ModalFooterProps {
|
||||
onOk?: () => void;
|
||||
cancelLabel?: string;
|
||||
okLabel?: string;
|
||||
keybindings?: boolean;
|
||||
}
|
||||
|
||||
const ModalFooter: React.FC<ModalFooterProps> = ({ onCancel, onOk, cancelLabel = "Cancel", okLabel = "Ok" }) => (
|
||||
class ModalKeybindings extends React.Component<{ onOk; onCancel }, {}> {
|
||||
curId: string;
|
||||
|
||||
@boundMethod
|
||||
componentDidMount(): void {
|
||||
this.curId = uuidv4();
|
||||
let domain = "modal-" + this.curId;
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
if (this.props.onOk) {
|
||||
keybindManager.registerKeybinding("modal", domain, "generic:confirm", (waveEvent) => {
|
||||
this.props.onOk();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (this.props.onCancel) {
|
||||
keybindManager.registerKeybinding("modal", domain, "generic:cancel", (waveEvent) => {
|
||||
this.props.onCancel();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
componentWillUnmount(): void {
|
||||
GlobalModel.keybindManager.unregisterDomain("modal-" + this.curId);
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const ModalFooter: React.FC<ModalFooterProps> = ({
|
||||
onCancel,
|
||||
onOk,
|
||||
cancelLabel = "Cancel",
|
||||
okLabel = "Ok",
|
||||
keybindings = true,
|
||||
}) => (
|
||||
<div className="wave-modal-footer">
|
||||
<If condition={keybindings}>
|
||||
<ModalKeybindings onOk={onOk} onCancel={onCancel}></ModalKeybindings>
|
||||
</If>
|
||||
{onCancel && (
|
||||
<Button className="secondary" onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
@@ -79,4 +124,4 @@ class Modal extends React.Component<ModalProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export { Modal };
|
||||
export { Modal, ModalKeybindings };
|
||||
|
||||
@@ -18,6 +18,8 @@ interface TextFieldProps {
|
||||
className?: string;
|
||||
onChange?: (value: string) => void;
|
||||
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
decoration?: TextFieldDecorationProps;
|
||||
@@ -78,6 +80,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
||||
@boundMethod
|
||||
handleFocus() {
|
||||
this.setState({ focused: true });
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
@@ -91,6 +96,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
||||
this.setState({ error: false, focused: false });
|
||||
}
|
||||
}
|
||||
if (this.props.onBlur) {
|
||||
this.props.onBlur();
|
||||
}
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Markdown, Modal, Button, Checkbox } from "@/elements";
|
||||
import { GlobalModel, GlobalCommandRunner } from "@/models";
|
||||
|
||||
import "./alert.less";
|
||||
import { ModalKeybindings } from "../elements/modal";
|
||||
|
||||
@mobxReact.observer
|
||||
class AlertModal extends React.Component<{}, {}> {
|
||||
@@ -54,6 +55,7 @@ class AlertModal extends React.Component<{}, {}> {
|
||||
</div>
|
||||
<div className="wave-modal-footer">
|
||||
<If condition={isConfirm}>
|
||||
<ModalKeybindings onOk={this.handleOK} onCancel={this.closeModal}></ModalKeybindings>
|
||||
<Button className="secondary" onClick={this.closeModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -62,6 +64,7 @@ class AlertModal extends React.Component<{}, {}> {
|
||||
</Button>
|
||||
</If>
|
||||
<If condition={!isConfirm}>
|
||||
<ModalKeybindings onOk={this.handleOK} onCancel={null}></ModalKeybindings>
|
||||
<Button autoFocus={true} onClick={this.handleOK}>
|
||||
Ok
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { Choose, When, If } from "tsx-control-statements/components";
|
||||
import { Modal, PasswordField, TextField, Markdown, Checkbox } from "@/elements";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent, KeybindManager } from "@/util/keyutil";
|
||||
|
||||
import "./userinput.less";
|
||||
|
||||
@@ -44,17 +44,20 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
[userInputRequest]
|
||||
);
|
||||
|
||||
function handleTextKeyDown(e: React.KeyboardEvent<HTMLInputElement>) {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
function handleTextFocus() {
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
keybindManager.registerKeybinding("modal", "userinput", "generic:confirm", (waveEvent) => {
|
||||
handleSendText();
|
||||
} else if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("modal", "userinput", "generic:cancel", (waveEvent) => {
|
||||
handleSendCancel();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function handleTextBlur() {
|
||||
GlobalModel.keybindManager.unregisterDomain("userinput");
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -89,7 +92,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
value={responseText}
|
||||
maxLength={400}
|
||||
autoFocus={true}
|
||||
onKeyDown={(e) => handleTextKeyDown(e)}
|
||||
onFocus={() => handleTextFocus()}
|
||||
onBlur={() => handleTextBlur()}
|
||||
/>
|
||||
</If>
|
||||
<If condition={!userInputRequest.publictext}>
|
||||
@@ -98,7 +102,8 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
|
||||
value={responseText}
|
||||
maxLength={400}
|
||||
autoFocus={true}
|
||||
onKeyDown={(e) => handleTextKeyDown(e)}
|
||||
onFocus={() => handleTextFocus()}
|
||||
onBlur={() => handleTextBlur()}
|
||||
/>
|
||||
</If>
|
||||
</If>
|
||||
|
||||
@@ -14,6 +14,7 @@ import * as textmeasure from "@/util/textmeasure";
|
||||
import * as appconst from "@/app/appconst";
|
||||
|
||||
import "./viewremoteconndetail.less";
|
||||
import { ModalKeybindings } from "../elements/modal";
|
||||
|
||||
@mobxReact.observer
|
||||
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
@@ -382,6 +383,20 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
</div>
|
||||
</div>
|
||||
<div className="wave-modal-footer">
|
||||
<ModalKeybindings
|
||||
onOk={() => {
|
||||
if (selectedRemoteStatus == "connecting") {
|
||||
return;
|
||||
}
|
||||
this.handleClose();
|
||||
}}
|
||||
onCancel={() => {
|
||||
if (selectedRemoteStatus == "connecting") {
|
||||
return;
|
||||
}
|
||||
this.handleClose();
|
||||
}}
|
||||
></ModalKeybindings>
|
||||
<Button
|
||||
className="secondary"
|
||||
disabled={selectedRemoteStatus == "connecting"}
|
||||
|
||||
@@ -8,21 +8,64 @@ import { GlobalModel } from "@/models";
|
||||
import { isBlank } from "@/util/util";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import { If, For } from "tsx-control-statements/components";
|
||||
import { Markdown } from "@/elements";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||
|
||||
class AIChatKeybindings extends React.Component<{ AIChatObject: AIChat }, {}> {
|
||||
componentDidMount(): void {
|
||||
let AIChatObject = this.props.AIChatObject;
|
||||
let keybindManager = GlobalModel.keybindManager;
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:confirm", (waveEvent) => {
|
||||
AIChatObject.onEnterKeyPressed();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:expandTextInput", (waveEvent) => {
|
||||
AIChatObject.onExpandInputPressed();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:cancel", (waveEvent) => {
|
||||
inputModel.closeAIAssistantChat(true);
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "aichat:clearHistory", (waveEvent) => {
|
||||
inputModel.clearAIAssistantChat();
|
||||
return true;
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectAbove", (waveEvent) => {
|
||||
return AIChatObject.onArrowUpPressed();
|
||||
});
|
||||
keybindManager.registerKeybinding("pane", "aichat", "generic:selectBelow", (waveEvent) => {
|
||||
return AIChatObject.onArrowDownPressed();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
GlobalModel.keybindManager.unregisterDomain("aichat");
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class AIChat extends React.Component<{}, {}> {
|
||||
chatListKeyCount: number = 0;
|
||||
textAreaNumLines: mobx.IObservableValue<number> = mobx.observable.box(1, { name: "textAreaNumLines" });
|
||||
chatWindowScrollRef: React.RefObject<HTMLDivElement>;
|
||||
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
isFocused: OV<boolean>;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.chatWindowScrollRef = React.createRef();
|
||||
this.textAreaRef = React.createRef();
|
||||
this.isFocused = mobx.observable.box(false, {
|
||||
name: "aichat-isfocused",
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -66,67 +109,82 @@ class AIChat extends React.Component<{}, {}> {
|
||||
return { numLines, linePos };
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
@boundMethod
|
||||
onKeyDown(e: any) {
|
||||
onTextAreaFocused(e: any) {
|
||||
mobx.action(() => {
|
||||
let model = GlobalModel;
|
||||
let inputModel = model.inputModel;
|
||||
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
||||
let resetCodeSelect = !ctrlMod;
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
if (!ctrlMod) {
|
||||
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
||||
let messageStr = e.target.value;
|
||||
this.submitChatMessage(messageStr);
|
||||
e.target.value = "";
|
||||
} else {
|
||||
inputModel.grabCodeSelectSelection();
|
||||
}
|
||||
} else {
|
||||
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
|
||||
}
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
inputModel.closeAIAssistantChat(true);
|
||||
}
|
||||
|
||||
if (checkKeyPressed(waveEvent, "Ctrl:l")) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
inputModel.clearAIAssistantChat();
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||
if (this.getLinePos(e.target).linePos > 1) {
|
||||
// normal up arrow
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
inputModel.codeSelectSelectNextOldestCodeBlock();
|
||||
resetCodeSelect = false;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
inputModel.codeSelectSelectNextNewestCodeBlock();
|
||||
resetCodeSelect = false;
|
||||
}
|
||||
|
||||
if (resetCodeSelect) {
|
||||
inputModel.codeSelectDeselectAll();
|
||||
}
|
||||
|
||||
// set height of textarea based on number of newlines
|
||||
this.textAreaNumLines.set(e.target.value.split(/\n/).length);
|
||||
this.isFocused.set(true);
|
||||
})();
|
||||
}
|
||||
|
||||
onTextAreaBlur(e: any) {
|
||||
mobx.action(() => {
|
||||
this.isFocused.set(false);
|
||||
})();
|
||||
}
|
||||
|
||||
onTextAreaChange(e: any) {
|
||||
// set height of textarea based on number of newlines
|
||||
mobx.action(() => {
|
||||
this.textAreaNumLines.set(e.target.value.split(/\n/).length);
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
})();
|
||||
}
|
||||
|
||||
onEnterKeyPressed() {
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
let currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return;
|
||||
}
|
||||
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
||||
let messageStr = currentRef.value;
|
||||
this.submitChatMessage(messageStr);
|
||||
currentRef.value = "";
|
||||
} else {
|
||||
inputModel.grabCodeSelectSelection();
|
||||
}
|
||||
}
|
||||
|
||||
onExpandInputPressed() {
|
||||
let currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return;
|
||||
}
|
||||
currentRef.setRangeText("\n", currentRef.selectionStart, currentRef.selectionEnd, "end");
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
}
|
||||
|
||||
onArrowUpPressed(): boolean {
|
||||
let currentRef = this.textAreaRef.current;
|
||||
if (currentRef == null) {
|
||||
return false;
|
||||
}
|
||||
if (this.getLinePos(currentRef).linePos > 1) {
|
||||
// normal up arrow
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
return false;
|
||||
}
|
||||
GlobalModel.inputModel.codeSelectSelectNextOldestCodeBlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
onArrowDownPressed(): boolean {
|
||||
let currentRef = this.textAreaRef.current;
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
if (currentRef == null) {
|
||||
return false;
|
||||
}
|
||||
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
||||
GlobalModel.inputModel.codeSelectDeselectAll();
|
||||
return false;
|
||||
}
|
||||
inputModel.codeSelectSelectNextNewestCodeBlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
@mobx.action
|
||||
@boundMethod
|
||||
onKeyDown(e: any) {}
|
||||
|
||||
renderError(err: string): any {
|
||||
return <div className="chat-msg-error">{err}</div>;
|
||||
}
|
||||
@@ -192,9 +250,13 @@ class AIChat extends React.Component<{}, {}> {
|
||||
const textAreaPadding = 2 * 0.5 * termFontSize;
|
||||
let textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines + textAreaPadding;
|
||||
let textAreaInnerHeight = this.textAreaNumLines.get() * textAreaLineHeight + textAreaPadding;
|
||||
let isFocused = this.isFocused.get();
|
||||
|
||||
return (
|
||||
<div className="cmd-aichat">
|
||||
<If condition={isFocused}>
|
||||
<AIChatKeybindings AIChatObject={this}></AIChatKeybindings>
|
||||
</If>
|
||||
<div className="cmdinput-titlebar">
|
||||
<div className="title-icon">
|
||||
<i className="fa-sharp fa-solid fa-sparkles" />
|
||||
@@ -217,6 +279,9 @@ class AIChat extends React.Component<{}, {}> {
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
id="chat-cmd-input"
|
||||
onFocus={this.onTextAreaFocused.bind(this)}
|
||||
onBlur={this.onTextAreaBlur.bind(this)}
|
||||
onChange={this.onTextAreaChange.bind(this)}
|
||||
onKeyDown={this.onKeyDown}
|
||||
style={{ height: textAreaInnerHeight, maxHeight: textAreaMaxHeight, fontSize: termFontSize }}
|
||||
className={cn("chat-textarea")}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@ import * as appconst from "@/app/appconst";
|
||||
import * as textmeasure from "@/util/textmeasure";
|
||||
|
||||
import "./screenview.less";
|
||||
import "./tabs.less";
|
||||
// import "./tabs.less";
|
||||
import { MagicLayout } from "../../magiclayout";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
.screen-tab {
|
||||
font: var(--base-font);
|
||||
font-size: var(--screentabs-font-size);
|
||||
line-height: var(--screentabs-line-height);
|
||||
border-top: 2px solid transparent;
|
||||
background: var(--app-bg-color);
|
||||
.background {
|
||||
// This applies a transparency mask to the background color, as set above, so that it will blend with whatever the theme's background color is.
|
||||
z-index: 1;
|
||||
width: var(--screen-tab-width);
|
||||
mask-image: linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
&.is-active {
|
||||
opacity: 1;
|
||||
font-weight: var(--screentabs-selected-font-weight);
|
||||
border-top: 2px solid var(--tab-color);
|
||||
}
|
||||
&.is-archived {
|
||||
.fa.fa-archive {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
svg.svg-icon-inner path {
|
||||
fill: var(--tab-color);
|
||||
}
|
||||
.tabicon i {
|
||||
color: var(--tab-color);
|
||||
}
|
||||
&.color-green,
|
||||
&.color-default {
|
||||
--tab-color: var(--tab-green);
|
||||
}
|
||||
&.color-orange {
|
||||
--tab-color: var(--tab-orange);
|
||||
}
|
||||
&.color-red {
|
||||
--tab-color: var(--tab-red);
|
||||
}
|
||||
&.color-yellow {
|
||||
--tab-color: var(--tab-yellow);
|
||||
}
|
||||
&.color-blue {
|
||||
--tab-color: var(--tab-blue);
|
||||
}
|
||||
&.color-mint {
|
||||
--tab-color: var(--tab-mint);
|
||||
}
|
||||
&.color-cyan {
|
||||
--tab-color: var(--tab-cyan);
|
||||
}
|
||||
&.color-white {
|
||||
--tab-color: var(--tab-white);
|
||||
}
|
||||
&.color-violet {
|
||||
--tab-color: var(--tab-violet);
|
||||
}
|
||||
&.color-pink {
|
||||
--tab-color: var(--tab-pink);
|
||||
}
|
||||
.screen-tab-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
min-width: var(--screen-tab-width);
|
||||
max-width: var(--screen-tab-width);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 8px 8px 4px 8px; // extra 4px of tab padding to account for horizontal scrollbar (to make tab text look centered)
|
||||
.front-icon {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
// Only one of these will be visible at a time
|
||||
.end-icons {
|
||||
// This adjusts the position of the icon to account for the default 8px margin on the parent. We want the positional calculations for this icon to assume it is flush with the edge of the screen tab.
|
||||
margin: 0 -5px 0 0;
|
||||
line-height: normal;
|
||||
.tab-index {
|
||||
font-size: 12.5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.vertical-line {
|
||||
border-left: 1px solid var(--app-border-color);
|
||||
margin: 10px 0 8px 0;
|
||||
}
|
||||
&:not(:hover) .status-indicator {
|
||||
.status-indicator-visible;
|
||||
}
|
||||
&:hover {
|
||||
.actions {
|
||||
.positional-icon-visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useRef } from "react";
|
||||
import cn from "classnames";
|
||||
import { ActionsIcon, StatusIndicator, CenteredIcon } from "@/common/icons/icons";
|
||||
import { TabIcon } from "@/elements/tabicon";
|
||||
import { GlobalModel, Screen } from "@/models";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
import * as constants from "@/app/appconst";
|
||||
|
||||
import "./tab2.less";
|
||||
|
||||
type ScreenTabProps = {
|
||||
screen: Screen;
|
||||
activeScreenId: string;
|
||||
onDragStart: (name: string, ref: React.RefObject<HTMLDivElement>) => void;
|
||||
onSwitchScreen: (screenId: string) => void;
|
||||
};
|
||||
|
||||
const ScreenTab: React.FC<ScreenTabProps> = mobxReact.observer(
|
||||
({ screen, activeScreenId, onSwitchScreen, onDragStart }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const openScreenSettings = (e: any, screen: Screen): void => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
mobx.action(() => {
|
||||
GlobalModel.screenSettingsModal.set({ sessionId: screen.sessionId, screenId: screen.screenId });
|
||||
})();
|
||||
GlobalModel.modalsModel.pushModal(constants.SCREEN_SETTINGS);
|
||||
};
|
||||
|
||||
const archived = screen.archived.get() ? (
|
||||
<i title="archived" className="fa-sharp fa-solid fa-box-archive" />
|
||||
) : null;
|
||||
const statusIndicatorLevel = screen.statusIndicator.get();
|
||||
const runningCommands = screen.numRunningCmds.get() > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-screenid={screen.screenId}
|
||||
className={cn(
|
||||
"screen-tab",
|
||||
{ "is-active": activeScreenId == screen.screenId, "is-archived": screen.archived.get() },
|
||||
"color-" + screen.getTabColor()
|
||||
)}
|
||||
onMouseDown={() => onDragStart(screen.name.get(), ref)}
|
||||
onClick={() => onSwitchScreen(screen.screenId)}
|
||||
data-screentab-name={screen.name.get()}
|
||||
>
|
||||
<div className="background"></div>
|
||||
<div className="screen-tab-inner">
|
||||
<CenteredIcon className="front-icon">
|
||||
<TabIcon icon={screen.getTabIcon()} color={screen.getTabColor()} />
|
||||
</CenteredIcon>
|
||||
<div className="tab-name truncate">
|
||||
{archived}
|
||||
{screen.name.get()}
|
||||
</div>
|
||||
<div className="end-icons">
|
||||
<StatusIndicator level={statusIndicatorLevel} runningCommands={runningCommands} />
|
||||
<ActionsIcon onClick={(e) => openScreenSettings(e, screen)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="vertical-line"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export { ScreenTab };
|
||||
@@ -0,0 +1,45 @@
|
||||
.screen-tabs-container {
|
||||
position: relative;
|
||||
height: var(--screentabs-height);
|
||||
|
||||
.screen-tabs-container-inner {
|
||||
position: relative; // Needed for absolute positioning of child tabs
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
margin-right: 42px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.new-screen {
|
||||
width: 42px;
|
||||
height: 40px;
|
||||
background-color: var(--app-bg-color);
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
line-height: 38px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
// This ensures the tab bar does not collide with the floating logo. The floating logo sits above the sidebar when it is not collapsed, so no additional margin is needed in that case.
|
||||
// More margin is given on macOS to account for the traffic light buttons
|
||||
#main.platform-darwin.mainsidebar-collapsed .screen-tabs-container {
|
||||
margin-left: var(--floating-logo-width-darwin);
|
||||
}
|
||||
|
||||
#main:not(.platform-darwin).mainsidebar-collapsed .screen-tabs-container {
|
||||
margin-left: var(--floating-logo-width);
|
||||
}
|
||||
|
||||
// This ensures the tab bar does not collide with the right sidebar triggers.
|
||||
#main.platform-darwin.rightsidebar-collapsed .screen-tabs-container {
|
||||
margin-right: var(--floating-right-sidebar-triggers-width-darwin);
|
||||
}
|
||||
|
||||
#main:not(.platform-darwin).rightsidebar-collapsed .screen-tabs-container {
|
||||
margin-left: var(--floating-right-sidebar-triggers-width);
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import React, { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { reaction } from "mobx";
|
||||
import { ScreenTab } from "./tab2";
|
||||
import { observer, useLocalObservable } from "mobx-react";
|
||||
import { For } from "tsx-control-statements/components";
|
||||
import { GlobalModel, GlobalCommandRunner, Session, Screen } from "@/models";
|
||||
import AddIcon from "@/assets/icons/add.svg";
|
||||
|
||||
import "./tabs2.less";
|
||||
|
||||
const DEFAULT_TAB_WIDTH = 170;
|
||||
|
||||
type ScreenTabsProps = {
|
||||
session: Session;
|
||||
};
|
||||
|
||||
const ScreenTabs: React.FC<ScreenTabsProps> = observer(({ session }) => {
|
||||
const [screens, setScreens] = useState<Screen[]>([]);
|
||||
const [tabWidth, setTabWidth] = useState(DEFAULT_TAB_WIDTH);
|
||||
const [_, setDraggedTab] = useState<string | null>(null);
|
||||
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
|
||||
const tabContainerRef = useRef<HTMLDivElement>(null);
|
||||
const addBtnRef = useRef<HTMLDivElement>(null);
|
||||
const mainSidebarWidth = GlobalModel.mainSidebarModel.getWidth();
|
||||
const rightSidebarWidth = GlobalModel.rightSidebarModel.getWidth();
|
||||
let prevDelta: number;
|
||||
let prevDragDirection: string;
|
||||
let draggedRemoved: boolean;
|
||||
let shrunk: boolean;
|
||||
|
||||
const store = useLocalObservable(() => ({
|
||||
get activeScreenId() {
|
||||
return session?.activeScreenId.get();
|
||||
},
|
||||
get screens() {
|
||||
let activeScreenId = store.activeScreenId;
|
||||
if (!activeScreenId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let screens = GlobalModel.getSessionScreens(session.sessionId);
|
||||
let filteredScreens = screens.filter(
|
||||
(screen) => !screen.archived.get() || activeScreenId === screen.screenId
|
||||
);
|
||||
|
||||
filteredScreens.sort((a, b) => a.screenIdx.get() - b.screenIdx.get());
|
||||
return filteredScreens;
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
// Update tabs when screens change
|
||||
const dispose = reaction(
|
||||
() => store.screens,
|
||||
(screens) => {
|
||||
setScreens(screens);
|
||||
}
|
||||
);
|
||||
// Clean up
|
||||
return () => {
|
||||
if (dispose) dispose();
|
||||
};
|
||||
}, [screens.length]);
|
||||
|
||||
const getActiveScreenId = (): string | null => {
|
||||
if (session) {
|
||||
return session.activeScreenId.get();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const updateTabPositions = useCallback(() => {
|
||||
if (tabContainerRef.current) {
|
||||
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
|
||||
let newStartPositions = [];
|
||||
let cumulativeLeft = 0; // Start from the left edge
|
||||
|
||||
tabElements.forEach((tab) => {
|
||||
newStartPositions.push(cumulativeLeft);
|
||||
cumulativeLeft += tab.getBoundingClientRect().width; // Add each tab's actual width to the cumulative position
|
||||
});
|
||||
|
||||
setDragStartPositions(newStartPositions);
|
||||
}
|
||||
}, [screens]);
|
||||
|
||||
useEffect(() => {
|
||||
updateTabPositions();
|
||||
}, [screens, updateTabPositions]);
|
||||
|
||||
const resizeTabs = useCallback(() => {
|
||||
if (tabContainerRef.current) {
|
||||
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
|
||||
const numberOfTabs = screens.length;
|
||||
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
|
||||
|
||||
if (totalDefaultTabWidth > containerWidth) {
|
||||
// Case where resizing is needed due to limited container width
|
||||
shrunk = true;
|
||||
const newTabWidth = containerWidth / numberOfTabs;
|
||||
setTabWidth(newTabWidth);
|
||||
screens.forEach((screen, index) => {
|
||||
const tabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${screen.name.get()}"]`
|
||||
) as HTMLElement;
|
||||
tabElement.style.width = `${newTabWidth}px`;
|
||||
tabElement.style.left = `${index * newTabWidth}px`;
|
||||
});
|
||||
} else if (shrunk || totalDefaultTabWidth < containerWidth) {
|
||||
// Case where tabs were previously shrunk or there is enough space for default width tabs
|
||||
shrunk = false;
|
||||
setTabWidth(DEFAULT_TAB_WIDTH);
|
||||
screens.forEach((screen, index) => {
|
||||
const tabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${screen.name.get()}"]`
|
||||
) as HTMLElement;
|
||||
tabElement.style.width = `${DEFAULT_TAB_WIDTH}px`;
|
||||
tabElement.style.left = `${index * DEFAULT_TAB_WIDTH}px`;
|
||||
});
|
||||
}
|
||||
|
||||
// Update the position of the Add Tab button
|
||||
const addButtonElement = addBtnRef.current;
|
||||
if (addButtonElement && tabContainerRef.current) {
|
||||
const tabElements = Array.from(tabContainerRef.current.querySelectorAll(".screen-tab"));
|
||||
const lastTab = tabElements[tabElements.length - 1];
|
||||
|
||||
if (lastTab) {
|
||||
const lastTabRect = lastTab.getBoundingClientRect();
|
||||
const containerRect = tabContainerRef.current.getBoundingClientRect();
|
||||
|
||||
// Calculate the left position relative to the tab container
|
||||
addButtonElement.style.left = `${lastTabRect.right - containerRect.left}px`;
|
||||
}
|
||||
}
|
||||
updateTabPositions();
|
||||
}
|
||||
}, [screens.length, updateTabPositions]);
|
||||
|
||||
// Resize tabs when the number of tabs or the window size changes
|
||||
useEffect(() => {
|
||||
resizeTabs();
|
||||
window.addEventListener("resize", resizeTabs);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", resizeTabs);
|
||||
};
|
||||
}, [resizeTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
resizeTabs();
|
||||
}, [mainSidebarWidth, rightSidebarWidth]);
|
||||
|
||||
const onDragStart = useCallback(
|
||||
(screenId: string, ref: React.RefObject<HTMLDivElement>) => {
|
||||
setDraggedTab(screenId);
|
||||
let tabIndex = screens.findIndex((screen) => screen.screenId === screenId);
|
||||
const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab
|
||||
const containerWidth = tabContainerRef.current.getBoundingClientRect().width;
|
||||
|
||||
if (ref.current) {
|
||||
let initialOffsetX: number | null = null;
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (initialOffsetX === null) {
|
||||
initialOffsetX = event.clientX - tabStartX;
|
||||
}
|
||||
let currentX = event.clientX - initialOffsetX;
|
||||
|
||||
// Constrain movement within the container bounds
|
||||
if (tabContainerRef.current) {
|
||||
const numberOfTabs = screens.length;
|
||||
const totalDefaultTabWidth = numberOfTabs * DEFAULT_TAB_WIDTH;
|
||||
const containerRect = tabContainerRef.current.getBoundingClientRect();
|
||||
let containerRectWidth = containerRect.width;
|
||||
// Set to the total default tab width if there's vacant space
|
||||
if (totalDefaultTabWidth < containerRectWidth) {
|
||||
containerRectWidth = totalDefaultTabWidth;
|
||||
}
|
||||
|
||||
const minLeft = 0;
|
||||
const maxRight = containerRectWidth - tabWidth;
|
||||
|
||||
// Adjust currentX to stay within bounds
|
||||
currentX = Math.min(Math.max(currentX, minLeft), maxRight);
|
||||
}
|
||||
|
||||
ref.current.style.transform = `translateX(${currentX - tabStartX}px)`;
|
||||
ref.current.style.zIndex = "100";
|
||||
|
||||
let dragDirection;
|
||||
if (currentX - prevDelta > 0) {
|
||||
dragDirection = "+";
|
||||
} else if (currentX - prevDelta === 0) {
|
||||
dragDirection = prevDragDirection;
|
||||
} else {
|
||||
dragDirection = "-";
|
||||
}
|
||||
prevDelta = currentX;
|
||||
prevDragDirection = dragDirection;
|
||||
|
||||
let newTabIndex = tabIndex;
|
||||
|
||||
if (dragDirection === "+") {
|
||||
// Dragging to the right
|
||||
for (let i = tabIndex + 1; i < screens.length; i++) {
|
||||
const otherTabStart = dragStartPositions[i];
|
||||
if (currentX + tabWidth > otherTabStart + tabWidth / 2) {
|
||||
newTabIndex = i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dragging to the left
|
||||
for (let i = tabIndex - 1; i >= 0; i--) {
|
||||
const otherTabEnd = dragStartPositions[i] + tabWidth;
|
||||
if (currentX < otherTabEnd - tabWidth / 2) {
|
||||
newTabIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rearrange the tabs temporarily
|
||||
if (newTabIndex !== tabIndex) {
|
||||
const tempTabs = Array.from(screens);
|
||||
|
||||
// Remove the dragged tab if not already done
|
||||
if (!draggedRemoved) {
|
||||
screens.splice(tabIndex, 1);
|
||||
draggedRemoved = true;
|
||||
}
|
||||
|
||||
// Find current index of the dragged tab in tempTabs
|
||||
const currentIndexOfDraggedTab = tabs.indexOf(name);
|
||||
|
||||
// Move the dragged tab to its new position
|
||||
if (currentIndexOfDraggedTab !== -1) {
|
||||
tabs.splice(currentIndexOfDraggedTab, 1);
|
||||
}
|
||||
tabs.splice(newTabIndex, 0, name);
|
||||
|
||||
// Update visual positions of the tabs
|
||||
tabs.forEach((tempTab, index) => {
|
||||
const tabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${tempTab}"]`
|
||||
) as HTMLElement;
|
||||
if (tempTab !== name) {
|
||||
tabElement.style.left = `${index * tabWidth}px`;
|
||||
}
|
||||
});
|
||||
|
||||
tabIndex = newTabIndex;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
const handleMouseUp = (event: MouseEvent) => {
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
document.removeEventListener("mouseup", handleMouseUp);
|
||||
|
||||
if (ref.current) {
|
||||
// Reset transform for all tabs
|
||||
const tabElements = tabContainerRef.current.querySelectorAll(".screen-tab");
|
||||
tabElements.forEach((tab) => {
|
||||
const htmlTab = tab as HTMLElement;
|
||||
htmlTab.style.transform = "";
|
||||
htmlTab.style.zIndex = "0";
|
||||
});
|
||||
|
||||
// Update the final position of the dragged tab
|
||||
const draggedTab = screens[tabIndex];
|
||||
const finalLeftPosition = tabIndex * tabWidth;
|
||||
const draggedTabElement = tabContainerRef.current.querySelector(
|
||||
`[data-screentab-name="${draggedTab.name.get()}"]`
|
||||
) as HTMLElement;
|
||||
if (draggedTabElement) {
|
||||
draggedTabElement.style.left = `${finalLeftPosition}px`;
|
||||
}
|
||||
}
|
||||
|
||||
setDraggedTab(null);
|
||||
draggedRemoved = false;
|
||||
};
|
||||
|
||||
document.addEventListener("mouseup", handleMouseUp);
|
||||
}
|
||||
},
|
||||
[screens, dragStartPositions]
|
||||
);
|
||||
|
||||
const onSwitchScreen = (screenId: string) => {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
if (session.activeScreenId.get() == screenId) {
|
||||
return;
|
||||
}
|
||||
let screen = session.getScreenById(screenId);
|
||||
if (screen == null) {
|
||||
return;
|
||||
}
|
||||
GlobalCommandRunner.switchScreen(screenId);
|
||||
};
|
||||
|
||||
const handleNewScreen = () => {
|
||||
GlobalCommandRunner.createNewScreen();
|
||||
};
|
||||
|
||||
if (session == null) {
|
||||
return null;
|
||||
}
|
||||
const screen: Screen | null = null;
|
||||
const activeScreenId = getActiveScreenId();
|
||||
|
||||
return (
|
||||
<div className="screen-tabs-container">
|
||||
<div className="screen-tabs-container-inner" ref={tabContainerRef}>
|
||||
<For each="screen" of={tabs}>
|
||||
<ScreenTab
|
||||
key={screen.screenId}
|
||||
screen={screen}
|
||||
activeScreenId={activeScreenId}
|
||||
onSwitchScreen={onSwitchScreen}
|
||||
onDragStart={onDragStart}
|
||||
/>
|
||||
</For>
|
||||
</div>
|
||||
<div ref={addBtnRef} className="new-screen" onClick={handleNewScreen} style={{ left: DEFAULT_TAB_WIDTH }}>
|
||||
<AddIcon className="icon hoverEffect" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export { ScreenTabs };
|
||||
@@ -11,7 +11,7 @@ import { If } from "tsx-control-statements/components";
|
||||
import { GlobalModel } from "@/models";
|
||||
import { CmdInput } from "./cmdinput/cmdinput";
|
||||
import { ScreenView } from "./screen/screenview";
|
||||
import { ScreenTabs } from "./screen/tabs";
|
||||
import { ScreenTabs } from "./screen/tabs2";
|
||||
import { ErrorBoundary } from "@/common/error/errorboundary";
|
||||
import * as textmeasure from "@/util/textmeasure";
|
||||
import "./workspace.less";
|
||||
|
||||
@@ -630,6 +630,9 @@ class InputModel {
|
||||
}
|
||||
|
||||
codeSelectDeselectAll(direction: number = this.codeSelectBottom) {
|
||||
if (this.codeSelectSelectedIndex.get() == direction) {
|
||||
return;
|
||||
}
|
||||
mobx.action(() => {
|
||||
this.codeSelectSelectedIndex.set(direction);
|
||||
this.codeSelectBlockRefArray = [];
|
||||
|
||||
@@ -476,23 +476,6 @@ class Model {
|
||||
if (isModKeyPress(e)) {
|
||||
return;
|
||||
}
|
||||
if (this.alertMessage.get() != null) {
|
||||
if (checkKeyPressed(waveEvent, "Escape")) {
|
||||
e.preventDefault();
|
||||
this.modalsModel.popModal(() => this.cancelAlert());
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "Enter")) {
|
||||
e.preventDefault();
|
||||
this.confirmAlert();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "Escape") && this.modalsModel.store.length > 0) {
|
||||
this.modalsModel.popModal();
|
||||
return;
|
||||
}
|
||||
if (this.keybindManager.processKeyEvent(e, waveEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
+4
-67
@@ -12,7 +12,7 @@ import { MagicLayout } from "@/app/magiclayout";
|
||||
import * as appconst from "@/app/appconst";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||
import { Model } from "./model";
|
||||
import { GlobalCommandRunner } from "./global";
|
||||
import { GlobalCommandRunner, GlobalModel } from "./global";
|
||||
import { Cmd } from "./cmd";
|
||||
import { ScreenLines } from "./screenlines";
|
||||
import { getTermPtyData } from "@/util/modelutil";
|
||||
@@ -469,7 +469,7 @@ class Screen {
|
||||
this.renderers[lineId] = renderer;
|
||||
}
|
||||
|
||||
setLineFocus(lineNum: number, focus: boolean): void {
|
||||
setLineFocus(lineNum: number, lineid: string, focus: boolean): void {
|
||||
mobx.action(() => this.termLineNumFocus.set(focus ? lineNum : 0))();
|
||||
if (focus && this.selectedLine.get() != lineNum) {
|
||||
GlobalCommandRunner.screenSelectLine(String(lineNum), "cmd");
|
||||
@@ -498,71 +498,8 @@ class Screen {
|
||||
})();
|
||||
}
|
||||
|
||||
termCustomKeyHandlerInternal(e: any, termWrap: TermWrap): void {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (checkKeyPressed(waveEvent, "ArrowUp")) {
|
||||
termWrap.terminal.scrollLines(-1);
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "ArrowDown")) {
|
||||
termWrap.terminal.scrollLines(1);
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "PageUp")) {
|
||||
termWrap.terminal.scrollPages(-1);
|
||||
return;
|
||||
}
|
||||
if (checkKeyPressed(waveEvent, "PageDown")) {
|
||||
termWrap.terminal.scrollPages(1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
isTermCapturedKey(e: any): boolean {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (
|
||||
checkKeyPressed(waveEvent, "ArrowUp") ||
|
||||
checkKeyPressed(waveEvent, "ArrowDown") ||
|
||||
checkKeyPressed(waveEvent, "PageUp") ||
|
||||
checkKeyPressed(waveEvent, "PageDown")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
termCustomKeyHandler(e: any, termWrap: TermWrap): boolean {
|
||||
let waveEvent = adaptFromReactOrNativeKeyEvent(e);
|
||||
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
let sel = termWrap.terminal.getSelection();
|
||||
navigator.clipboard.writeText(sel);
|
||||
return false;
|
||||
}
|
||||
if (e.type == "keypress" && checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
let p = navigator.clipboard.readText();
|
||||
p.then((text) => {
|
||||
termWrap.dataHandler?.(text, termWrap);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (termWrap.isRunning) {
|
||||
return true;
|
||||
}
|
||||
let isCaptured = this.isTermCapturedKey(e);
|
||||
if (!isCaptured) {
|
||||
return true;
|
||||
}
|
||||
if (e.type != "keydown" || isModKeyPress(e)) {
|
||||
return false;
|
||||
}
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.termCustomKeyHandlerInternal(e, termWrap);
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
loadTerminalRenderer(elem: Element, line: LineType, cmd: Cmd, width: number) {
|
||||
@@ -588,7 +525,7 @@ class Screen {
|
||||
termOpts: cmd.getTermOpts(),
|
||||
winSize: { height: 0, width: width },
|
||||
dataHandler: cmd.handleData.bind(cmd),
|
||||
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, focus),
|
||||
focusHandler: (focus: boolean) => this.setLineFocus(line.linenum, line.lineid, focus),
|
||||
isRunning: cmd.isRunning(),
|
||||
customKeyHandler: this.termCustomKeyHandler.bind(this),
|
||||
fontSize: this.globalModel.getTermFontSize(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user