1
0
mirror of https://github.com/arduino/arduino-ide.git synced 2025-05-03 19:48:42 +00:00
2021-07-09 10:14:42 +02:00

395 lines
11 KiB
TypeScript

import * as React from 'react';
import * as dateFormat from 'dateformat';
import { postConstruct, injectable, inject } from 'inversify';
import { OptionsType } from 'react-select/src/types';
import { isOSX } from '@theia/core/lib/common/os';
import { Event, Emitter } from '@theia/core/lib/common/event';
import { Key, KeyCode } from '@theia/core/lib/browser/keys';
import {
DisposableCollection,
Disposable,
} from '@theia/core/lib/common/disposable';
import {
ReactWidget,
Message,
Widget,
MessageLoop,
} from '@theia/core/lib/browser/widgets';
import { Board, Port } from '../../common/protocol/boards-service';
import { MonitorConfig } from '../../common/protocol/monitor-service';
import { ArduinoSelect } from '../widgets/arduino-select';
import { MonitorModel } from './monitor-model';
import { MonitorConnection } from './monitor-connection';
import { MonitorServiceClientImpl } from './monitor-service-client-impl';
@injectable()
export class MonitorWidget extends ReactWidget {
static readonly ID = 'serial-monitor';
@inject(MonitorModel)
protected readonly monitorModel: MonitorModel;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
@inject(MonitorServiceClientImpl)
protected readonly monitorServiceClient: MonitorServiceClientImpl;
protected widgetHeight: number;
/**
* Do not touch or use it. It is for setting the focus on the `input` after the widget activation.
*/
protected focusNode: HTMLElement | undefined;
/**
* Guard against re-rendering the view after the close was requested.
* See: https://github.com/eclipse-theia/theia/issues/6704
*/
protected closing = false;
protected readonly clearOutputEmitter = new Emitter<void>();
constructor() {
super();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.title.iconClass = 'monitor-tab-icon';
this.title.closable = true;
this.scrollOptions = undefined;
this.toDispose.push(this.clearOutputEmitter);
this.toDispose.push(
Disposable.create(() => {
this.monitorConnection.autoConnect = false;
if (this.monitorConnection.connected) {
this.monitorConnection.disconnect();
}
})
);
}
@postConstruct()
protected init(): void {
this.update();
this.toDispose.push(
this.monitorConnection.onConnectionChanged(() => this.clearConsole())
);
}
clearConsole(): void {
this.clearOutputEmitter.fire(undefined);
this.update();
}
dispose(): void {
super.dispose();
}
protected onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.monitorConnection.autoConnect = true;
}
onCloseRequest(msg: Message): void {
this.closing = true;
super.onCloseRequest(msg);
}
protected onUpdateRequest(msg: Message): void {
// TODO: `this.isAttached`
// See: https://github.com/eclipse-theia/theia/issues/6704#issuecomment-562574713
if (!this.closing && this.isAttached) {
super.onUpdateRequest(msg);
}
}
protected onResize(msg: Widget.ResizeMessage): void {
super.onResize(msg);
this.widgetHeight = msg.height;
this.update();
}
protected onActivateRequest(msg: Message): void {
super.onActivateRequest(msg);
(this.focusNode || this.node).focus();
}
protected onFocusResolved = (element: HTMLElement | undefined) => {
if (this.closing || !this.isAttached) {
return;
}
this.focusNode = element;
requestAnimationFrame(() =>
MessageLoop.sendMessage(this, Widget.Msg.ActivateRequest)
);
};
protected get lineEndings(): OptionsType<SelectOption<MonitorModel.EOL>> {
return [
{
label: 'No Line Ending',
value: '',
},
{
label: 'New Line',
value: '\n',
},
{
label: 'Carriage Return',
value: '\r',
},
{
label: 'Both NL & CR',
value: '\r\n',
},
];
}
protected get baudRates(): OptionsType<SelectOption<MonitorConfig.BaudRate>> {
const baudRates: Array<MonitorConfig.BaudRate> = [
300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200,
];
return baudRates.map((baudRate) => ({
label: baudRate + ' baud',
value: baudRate,
}));
}
protected render(): React.ReactNode {
const { baudRates, lineEndings } = this;
const lineEnding =
lineEndings.find((item) => item.value === this.monitorModel.lineEnding) ||
lineEndings[1]; // Defaults to `\n`.
const baudRate =
baudRates.find((item) => item.value === this.monitorModel.baudRate) ||
baudRates[4]; // Defaults to `9600`.
return (
<div className="serial-monitor">
<div className="head">
<div className="send">
<SerialMonitorSendInput
monitorConfig={this.monitorConnection.monitorConfig}
resolveFocus={this.onFocusResolved}
onSend={this.onSend}
/>
</div>
<div className="config">
<div className="select">
<ArduinoSelect
maxMenuHeight={this.widgetHeight - 40}
options={lineEndings}
defaultValue={lineEnding}
onChange={this.onChangeLineEnding}
/>
</div>
<div className="select">
<ArduinoSelect
className="select"
maxMenuHeight={this.widgetHeight - 40}
options={baudRates}
defaultValue={baudRate}
onChange={this.onChangeBaudRate}
/>
</div>
</div>
</div>
<div className="body">
<SerialMonitorOutput
monitorModel={this.monitorModel}
monitorConnection={this.monitorConnection}
clearConsoleEvent={this.clearOutputEmitter.event}
/>
</div>
</div>
);
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string): Promise<void> {
this.monitorConnection.send(value);
}
protected readonly onChangeLineEnding = (
option: SelectOption<MonitorModel.EOL>
) => {
this.monitorModel.lineEnding = option.value;
};
protected readonly onChangeBaudRate = (
option: SelectOption<MonitorConfig.BaudRate>
) => {
this.monitorModel.baudRate = option.value;
};
}
export namespace SerialMonitorSendInput {
export interface Props {
readonly monitorConfig?: MonitorConfig;
readonly onSend: (text: string) => void;
readonly resolveFocus: (element: HTMLElement | undefined) => void;
}
export interface State {
text: string;
}
}
export class SerialMonitorSendInput extends React.Component<
SerialMonitorSendInput.Props,
SerialMonitorSendInput.State
> {
constructor(props: Readonly<SerialMonitorSendInput.Props>) {
super(props);
this.state = { text: '' };
this.onChange = this.onChange.bind(this);
this.onSend = this.onSend.bind(this);
this.onKeyDown = this.onKeyDown.bind(this);
}
render(): React.ReactNode {
return (
<input
ref={this.setRef}
type="text"
className={`theia-input ${this.props.monitorConfig ? '' : 'warning'}`}
placeholder={this.placeholder}
value={this.state.text}
onChange={this.onChange}
onKeyDown={this.onKeyDown}
/>
);
}
protected get placeholder(): string {
const { monitorConfig } = this.props;
if (!monitorConfig) {
return 'Not connected. Select a board and a port to connect automatically.';
}
const { board, port } = monitorConfig;
return `Message (${
isOSX ? '⌘' : 'Ctrl'
}+Enter to send message to '${Board.toString(board, {
useFqbn: false,
})}' on '${Port.toString(port)}')`;
}
protected setRef = (element: HTMLElement | null) => {
if (this.props.resolveFocus) {
this.props.resolveFocus(element || undefined);
}
};
protected onChange(event: React.ChangeEvent<HTMLInputElement>): void {
this.setState({ text: event.target.value });
}
protected onSend(): void {
this.props.onSend(this.state.text);
this.setState({ text: '' });
}
protected onKeyDown(event: React.KeyboardEvent<HTMLInputElement>): void {
const keyCode = KeyCode.createKeyCode(event.nativeEvent);
if (keyCode) {
const { key, meta, ctrl } = keyCode;
if (key === Key.ENTER && ((isOSX && meta) || (!isOSX && ctrl))) {
this.onSend();
}
}
}
}
export namespace SerialMonitorOutput {
export interface Props {
readonly monitorModel: MonitorModel;
readonly monitorConnection: MonitorConnection;
readonly clearConsoleEvent: Event<void>;
}
export interface State {
content: string;
timestamp: boolean;
}
}
export class SerialMonitorOutput extends React.Component<
SerialMonitorOutput.Props,
SerialMonitorOutput.State
> {
/**
* Do not touch it. It is used to be able to "follow" the serial monitor log.
*/
protected anchor: HTMLElement | null;
protected toDisposeBeforeUnmount = new DisposableCollection();
constructor(props: Readonly<SerialMonitorOutput.Props>) {
super(props);
this.state = {
content: '',
timestamp: this.props.monitorModel.timestamp,
};
}
render(): React.ReactNode {
return (
<React.Fragment>
<div style={{ whiteSpace: 'pre', fontFamily: 'monospace' }}>
{this.state.content}
</div>
<div
style={{ float: 'left', clear: 'both' }}
ref={(element) => {
this.anchor = element;
}}
/>
</React.Fragment>
);
}
componentDidMount(): void {
this.scrollToBottom();
this.toDisposeBeforeUnmount.pushAll([
this.props.monitorConnection.onRead(({ message }) => {
const rawLines = message.split('\n');
const lines: string[] = [];
const timestamp = () =>
this.state.timestamp
? `${dateFormat(new Date(), 'H:M:ss.l')} -> `
: '';
for (let i = 0; i < rawLines.length; i++) {
if (i === 0 && this.state.content.length !== 0) {
lines.push(rawLines[i]);
} else {
lines.push(timestamp() + rawLines[i]);
}
}
const content = this.state.content + lines.join('\n');
this.setState({ content });
}),
this.props.clearConsoleEvent(() => this.setState({ content: '' })),
this.props.monitorModel.onChange(({ property }) => {
if (property === 'timestamp') {
const { timestamp } = this.props.monitorModel;
this.setState({ timestamp });
}
}),
]);
}
componentDidUpdate(): void {
this.scrollToBottom();
}
componentWillUnmount(): void {
// TODO: "Your preferred browser's local storage is almost full." Discard `content` before saving layout?
this.toDisposeBeforeUnmount.dispose();
}
protected scrollToBottom(): void {
if (this.props.monitorModel.autoscroll && this.anchor) {
this.anchor.scrollIntoView();
}
}
}
export interface SelectOption<T> {
readonly label: string;
readonly value: T;
}