Implemented the Widget

Re-introduced bottom panel tabs

Signed-off-by: jbicker <jan.bicker@typefox.io>
This commit is contained in:
jbicker
2019-07-26 09:57:42 +02:00
parent 206b65f138
commit 76d0f5a464
14 changed files with 894 additions and 18 deletions

View File

@@ -47,6 +47,7 @@ import { BoardsToolBarItem } from './boards/boards-toolbar-item';
import { BoardsConfig } from './boards/boards-config';
import { MonitorService } from '../common/protocol/monitor-service';
import { ConfigService } from '../common/protocol/config-service';
import { MonitorConnection } from './monitor/monitor-connection';
export namespace ArduinoMenus {
export const SKETCH = [...MAIN_MENU_BAR, '3_sketch'];
@@ -143,6 +144,8 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
@inject(ConfigService)
protected readonly configService: ConfigService;
@inject(MonitorConnection)
protected readonly monitorConnection: MonitorConnection;
protected boardsToolbarItem: BoardsToolBarItem | null;
protected wsSketchCount: number = 0;
@@ -244,6 +247,9 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
return;
}
const connectionConfig = this.monitorConnection.connectionConfig;
await this.monitorConnection.disconnect();
try {
const { boardsConfig } = this.boardsServiceClient;
if (!boardsConfig || !boardsConfig.selectedBoard) {
@@ -256,6 +262,10 @@ export class ArduinoFrontendContribution implements TabBarToolbarContribution, C
await this.coreService.upload({ uri: uri.toString(), board: boardsConfig.selectedBoard, port: selectedPort });
} catch (e) {
await this.messageService.error(e.toString());
} finally {
if (connectionConfig) {
await this.monitorConnection.connect(connectionConfig);
}
}
}
});

View File

@@ -57,12 +57,12 @@ import { BoardItemRenderer } from './boards/boards-item-renderer';
import { MonitorServiceClientImpl } from './monitor/monitor-service-client-impl';
import { MonitorServicePath, MonitorService, MonitorServiceClient } from '../common/protocol/monitor-service';
import { ConfigService, ConfigServicePath } from '../common/protocol/config-service';
import { MonitorWidget } from './monitor/monitor-widget';
import { MonitorViewContribution } from './monitor/monitor-view-contribution';
import { MonitorConnection } from './monitor/monitor-connection';
import { MonitorModel } from './monitor/monitor-model';
const ElementQueries = require('css-element-queries/src/ElementQueries');
if (!ARDUINO_PRO_MODE) {
require('../../src/browser/style/silent-bottom-panel-tabs.css');
}
export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Unbind, isBound: interfaces.IsBound, rebind: interfaces.Rebind) => {
ElementQueries.listen();
ElementQueries.init();
@@ -155,12 +155,23 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
return workspaceServiceExt;
});
// Serial Monitor
bind(MonitorModel).toSelf().inSingletonScope();
bind(MonitorWidget).toSelf();
bindViewContribution(bind, MonitorViewContribution);
bind(TabBarToolbarContribution).toService(MonitorViewContribution);
bind(WidgetFactory).toDynamicValue(context => ({
id: MonitorWidget.ID,
createWidget: () => context.container.get(MonitorWidget)
}));
// Frontend binding for the monitor service.
bind(MonitorService).toDynamicValue(context => {
const connection = context.container.get(WebSocketConnectionProvider);
const client = context.container.get(MonitorServiceClientImpl);
return connection.createProxy(MonitorServicePath, client);
}).inSingletonScope();
// MonitorConnection
bind(MonitorConnection).toSelf().inSingletonScope();
// Monitor service client to receive and delegate notifications from the backend.
bind(MonitorServiceClientImpl).toSelf().inSingletonScope();
bind(MonitorServiceClient).toDynamicValue(context => {

View File

@@ -0,0 +1,60 @@
import { injectable, inject } from "inversify";
import { MonitorService, ConnectionConfig } from "../../common/protocol/monitor-service";
import { Emitter, Event } from "@theia/core";
@injectable()
export class MonitorConnection {
@inject(MonitorService)
protected readonly monitorService: MonitorService;
protected _connectionId: string | undefined;
protected _connectionConfig: ConnectionConfig;
protected readonly onConnectionChangedEmitter = new Emitter<string | undefined>();
readonly onConnectionChanged: Event<string | undefined> = this.onConnectionChangedEmitter.event;
get connectionId(): string | undefined {
return this._connectionId;
}
set connectionId(cid: string | undefined) {
this._connectionId = cid;
}
get connectionConfig(): ConnectionConfig {
return this._connectionConfig;
}
async connect(config: ConnectionConfig): Promise<string | undefined> {
if (this._connectionId) {
await this.disconnect();
}
const { connectionId } = await this.monitorService.connect(config);
this._connectionId = connectionId;
this._connectionConfig = config;
this.onConnectionChangedEmitter.fire(this._connectionId);
return connectionId;
}
async disconnect(): Promise<boolean> {
let result = true;
const connections = await this.monitorService.getConnectionIds();
if (this._connectionId && connections.findIndex(id => id === this._connectionId) >= 0) {
console.log('>>> Disposing existing monitor connection before establishing a new one...');
result = await this.monitorService.disconnect(this._connectionId);
if (!result) {
// TODO: better!!!
console.error(`Could not close connection: ${this._connectionId}. Check the backend logs.`);
} else {
console.log(`<<< Disposed ${this._connectionId} connection.`);
this._connectionId = undefined;
}
this.onConnectionChangedEmitter.fire(this._connectionId);
}
return result;
}
}

View File

@@ -0,0 +1,31 @@
import { injectable } from "inversify";
import { Emitter } from "@theia/core";
@injectable()
export class MonitorModel {
protected readonly onChangeEmitter = new Emitter<void>();
readonly onChange = this.onChangeEmitter.event;
protected _autoscroll: boolean = true;
protected _timestamp: boolean = false;
get autoscroll(): boolean {
return this._autoscroll;
}
get timestamp(): boolean {
return this._timestamp;
}
toggleAutoscroll(): void {
this._autoscroll = !this._autoscroll;
this.onChangeEmitter.fire(undefined);
}
toggleTimestamp(): void {
this._timestamp = !this._timestamp;
this.onChangeEmitter.fire(undefined);
}
}

View File

@@ -0,0 +1,120 @@
import * as React from 'react';
import { injectable, inject } from "inversify";
import { AbstractViewContribution } from "@theia/core/lib/browser";
import { MonitorWidget } from "./monitor-widget";
import { MenuModelRegistry, Command, CommandRegistry } from "@theia/core";
import { ArduinoMenus } from "../arduino-frontend-contribution";
import { TabBarToolbarContribution, TabBarToolbarRegistry } from "@theia/core/lib/browser/shell/tab-bar-toolbar";
import { MonitorModel } from './monitor-model';
export namespace SerialMonitor {
export namespace Commands {
export const AUTOSCROLL: Command = {
id: 'serial-monitor-autoscroll',
label: 'Autoscroll'
}
export const TIMESTAMP: Command = {
id: 'serial-monitor-timestamp',
label: 'Timestamp'
}
export const CLEAR_OUTPUT: Command = {
id: 'serial-monitor-clear-output',
label: 'Clear Output',
iconClass: 'clear-all'
}
}
}
@injectable()
export class MonitorViewContribution extends AbstractViewContribution<MonitorWidget> implements TabBarToolbarContribution {
static readonly OPEN_SERIAL_MONITOR = MonitorWidget.ID + ':toggle';
@inject(MonitorModel) protected readonly model: MonitorModel;
constructor() {
super({
widgetId: MonitorWidget.ID,
widgetName: 'Serial Monitor',
defaultWidgetOptions: {
area: 'bottom'
},
toggleCommandId: MonitorViewContribution.OPEN_SERIAL_MONITOR,
toggleKeybinding: 'ctrl+shift+m'
})
}
registerMenus(menus: MenuModelRegistry): void {
if (this.toggleCommand) {
menus.registerMenuAction(ArduinoMenus.TOOLS, {
commandId: this.toggleCommand.id,
label: 'Serial Monitor'
});
}
}
async registerToolbarItems(registry: TabBarToolbarRegistry) {
registry.registerItem({
id: 'monitor-autoscroll',
tooltip: 'Toggle Autoscroll',
render: () => this.renderAutoScrollButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
registry.registerItem({
id: 'monitor-timestamp',
tooltip: 'Toggle Timestamp',
render: () => this.renderTimestampButton(),
isVisible: widget => widget instanceof MonitorWidget,
onDidChange: this.model.onChange
});
registry.registerItem({
id: SerialMonitor.Commands.CLEAR_OUTPUT.id,
command: SerialMonitor.Commands.CLEAR_OUTPUT.id,
tooltip: 'Clear Output'
});
}
registerCommands(commands: CommandRegistry): void {
super.registerCommands(commands);
commands.registerCommand(SerialMonitor.Commands.CLEAR_OUTPUT, {
isEnabled: widget => widget instanceof MonitorWidget,
isVisible: widget => widget instanceof MonitorWidget,
execute: widget => {
if (widget instanceof MonitorWidget) {
widget.clear();
}
}
});
}
protected renderAutoScrollButton(): React.ReactNode {
return <React.Fragment>
<div
className={`item enabled fa fa-angle-double-down arduino-monitor ${this.model.autoscroll ? 'toggled' : ''}`}
onClick={this.toggleAutoScroll}
></div>
</React.Fragment>;
}
protected readonly toggleAutoScroll = () => this.doToggleAutoScroll();
protected async doToggleAutoScroll() {
this.model.toggleAutoscroll();
}
protected renderTimestampButton(): React.ReactNode {
return <React.Fragment>
<div
className={`item enabled fa fa-clock-o arduino-monitor ${this.model.timestamp ? 'toggled' : ''}`}
onClick={this.toggleTimestamp}
></div>
</React.Fragment>;
}
protected readonly toggleTimestamp = () => this.doToggleTimestamp();
protected async doToggleTimestamp() {
this.model.toggleTimestamp();
}
}

View File

@@ -0,0 +1,346 @@
import { ReactWidget, Message } from "@theia/core/lib/browser";
import { postConstruct, injectable, inject } from "inversify";
import * as React from 'react';
import Select, { components } from 'react-select';
import { Styles } from "react-select/src/styles";
import { ThemeConfig } from "react-select/src/theme";
import { OptionsType } from "react-select/src/types";
import { MonitorServiceClientImpl } from "./monitor-service-client-impl";
import { MessageService } from "@theia/core";
import { ConnectionConfig, MonitorService } from "../../common/protocol/monitor-service";
import { MonitorConnection } from "./monitor-connection";
import { BoardsServiceClientImpl } from "../boards/boards-service-client-impl";
import { AttachedSerialBoard, BoardsService, Board } from "../../common/protocol/boards-service";
import { BoardsConfig } from "../boards/boards-config";
import { MonitorModel } from "./monitor-model";
export namespace SerialMonitorSendField {
export interface Props {
onSend: (text: string) => void
}
export interface State {
value: string;
}
}
export class SerialMonitorSendField extends React.Component<SerialMonitorSendField.Props, SerialMonitorSendField.State> {
constructor(props: SerialMonitorSendField.Props) {
super(props);
this.state = { value: '' };
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
render() {
return <React.Fragment>
<form onSubmit={this.handleSubmit}>
<input type='text' id='serial-monitor-send' autoComplete='off' value={this.state.value} onChange={this.handleChange} />
<input className="btn" type="submit" value="Submit" />
</form>
</React.Fragment>
}
protected handleChange(event: React.ChangeEvent<HTMLInputElement>) {
this.setState({ value: event.target.value });
}
protected handleSubmit(event: React.FormEvent<HTMLFormElement>) {
this.props.onSend(this.state.value);
event.preventDefault();
}
}
export namespace SerialMonitorOutput {
export interface Props {
lines: string[];
model: MonitorModel;
}
}
export class SerialMonitorOutput extends React.Component<SerialMonitorOutput.Props> {
protected theEnd: HTMLDivElement | null;
render() {
let result = '';
const style: React.CSSProperties = {
whiteSpace: 'pre',
fontFamily: 'monospace',
};
for (let text of this.props.lines) {
result += text;
}
if (result.length === 0) {
result = '';
}
return <React.Fragment>
<div style={style}>{result}</div>
<div style={{ float: "left", clear: "both" }}
ref={(el) => { this.theEnd = el; }}>
</div>
</React.Fragment>;
}
protected scrollToBottom() {
if (this.theEnd) {
this.theEnd.scrollIntoView();
}
}
componentDidMount() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
componentDidUpdate() {
if (this.props.model.autoscroll) {
this.scrollToBottom();
}
}
}
export interface SelectOption {
label: string;
value: string | number;
}
@injectable()
export class MonitorWidget extends ReactWidget {
static readonly ID = 'serial-monitor';
protected lines: string[];
protected tempData: string;
protected _baudRate: number;
protected _lineEnding: string;
constructor(
@inject(MonitorServiceClientImpl) protected readonly serviceClient: MonitorServiceClientImpl,
@inject(MonitorConnection) protected readonly connection: MonitorConnection,
@inject(MonitorService) protected readonly monitorService: MonitorService,
@inject(BoardsServiceClientImpl) protected readonly boardsServiceClient: BoardsServiceClientImpl,
@inject(MessageService) protected readonly messageService: MessageService,
@inject(BoardsService) protected readonly boardsService: BoardsService,
@inject(MonitorModel) protected readonly model: MonitorModel
) {
super();
this.id = MonitorWidget.ID;
this.title.label = 'Serial Monitor';
this.lines = [];
this.tempData = '';
this._lineEnding = '\n';
this.toDisposeOnDetach.push(serviceClient.onRead(({ data, connectionId }) => {
this.tempData += data;
if (this.tempData.endsWith('\n')) {
if (this.model.timestamp) {
const nu = new Date();
this.tempData = `${nu.getHours()}:${nu.getMinutes()}:${nu.getSeconds()}.${nu.getMilliseconds()} -> ` + this.tempData;
}
this.lines.push(this.tempData);
this.tempData = '';
this.update();
}
}));
// TODO onError
}
@postConstruct()
protected init(): void {
this.update();
}
clear(): void {
this.lines = [];
this.update();
}
get baudRate(): number | undefined {
return this._baudRate;
}
protected onAfterAttach(msg: Message) {
super.onAfterAttach(msg);
this.clear();
this.connect();
this.toDisposeOnDetach.push(this.boardsServiceClient.onBoardsChanged(async states => {
const currentConnectionConfig = this.connection.connectionConfig;
const connectedBoard = states.newState.boards
.filter(AttachedSerialBoard.is)
.find(board => {
const potentiallyConnected = currentConnectionConfig && currentConnectionConfig.board;
if (AttachedSerialBoard.is(potentiallyConnected)) {
return Board.equals(board, potentiallyConnected) && board.port === potentiallyConnected.port;
}
return false;
});
if (!connectedBoard) {
this.close();
}
}));
this.toDisposeOnDetach.push(this.connection.onConnectionChanged(() => {
this.clear();
}));
}
protected onBeforeDetach(msg: Message) {
super.onBeforeDetach(msg);
this.connection.disconnect();
}
protected async connect() {
const config = await this.getConnectionConfig();
if (config) {
this.connection.connect(config);
}
}
protected async getConnectionConfig(): Promise<ConnectionConfig | undefined> {
const baudRate = this.baudRate;
const { boardsConfig } = this.boardsServiceClient;
const { selectedBoard, selectedPort } = boardsConfig;
if (!selectedBoard) {
this.messageService.warn('No boards selected.');
return;
}
const { name } = selectedBoard;
if (!selectedPort) {
this.messageService.warn(`No ports selected for board: '${name}'.`);
return;
}
const attachedBoards = await this.boardsService.getAttachedBoards();
const connectedBoard = attachedBoards.boards.filter(AttachedSerialBoard.is).find(board => BoardsConfig.Config.sameAs(boardsConfig, board));
if (!connectedBoard) {
this.messageService.warn(`The selected '${name}' board is not connected on ${selectedPort}.`);
return;
}
return {
baudRate,
board: selectedBoard,
port: selectedPort
}
}
protected getLineEndings(): OptionsType<SelectOption> {
return [
{
label: 'No Line Ending',
value: ''
},
{
label: 'Newline',
value: '\n'
},
{
label: 'Carriage Return',
value: '\r'
},
{
label: 'Both NL & CR',
value: '\r\n'
}
]
}
protected getBaudRates(): OptionsType<SelectOption> {
const baudRates = [300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200];
return baudRates.map<SelectOption>(baudRate => ({ label: baudRate + ' baud', value: baudRate }))
}
protected render(): React.ReactNode {
const le = this.getLineEndings();
const br = this.getBaudRates();
return <React.Fragment>
<div className='serial-monitor-container'>
<div className='head'>
<div className='send'>
<SerialMonitorSendField onSend={this.onSend} />
</div>
<div className='config'>
{this.renderSelectField('arduino-serial-monitor-line-endings', le, le[1], this.onChangeLineEnding)}
{this.renderSelectField('arduino-serial-monitor-baud-rates', br, br[4], this.onChangeBaudRate)}
</div>
</div>
<div id='serial-monitor-output-container'>
<SerialMonitorOutput model={this.model} lines={this.lines} />
</div>
</div>
</React.Fragment>;
}
protected readonly onSend = (value: string) => this.doSend(value);
protected async doSend(value: string) {
const { connectionId } = this.connection;
if (connectionId) {
this.monitorService.send(connectionId, value + this._lineEnding);
}
}
protected readonly onChangeLineEnding = (le: SelectOption) => {
this._lineEnding = typeof le.value === 'string' ? le.value : '\n';
}
protected readonly onChangeBaudRate = (br: SelectOption) => {
this._baudRate = typeof br.value === 'number' ? br.value : 9600;
}
protected renderSelectField(id: string, options: OptionsType<SelectOption>, defaultVal: SelectOption, onChange: (v: SelectOption) => void): React.ReactNode {
const height = 25;
const selectStyles: Styles = {
control: (provided, state) => ({
...provided,
width: 200
}),
dropdownIndicator: (p, s) => ({
...p,
padding: 0
}),
indicatorSeparator: (p, s) => ({
display: 'none'
}),
indicatorsContainer: (p, s) => ({
padding: '0 5px'
}),
menu: (p, s) => ({
...p,
marginTop: 0
})
};
const theme: ThemeConfig = theme => ({
...theme,
borderRadius: 0,
spacing: {
controlHeight: height,
baseUnit: 2,
menuGutter: 4
}
});
const DropdownIndicator = (
props: React.Props<typeof components.DropdownIndicator>
) => {
return (
<span className='fa fa-caret-down caret'></span>
);
};
return <Select
options={options}
defaultValue={defaultVal}
onChange={onChange}
components={{ DropdownIndicator }}
theme={theme}
styles={selectStyles}
classNamePrefix='sms'
className='serial-monitor-select'
id={id}
/>
}
}

View File

@@ -196,6 +196,7 @@ button.theia-button.main {
background: #f7f7f7;
border: 3px solid var(--theia-border-color2);
margin: -3px;
z-index: 1000;
}
.arduino-boards-dropdown-item {

View File

@@ -1,4 +1,5 @@
@import './list-widget.css';
@import './board-select-dialog.css';
@import './main.css';
@import './editor.css';
@import './editor.css';
@import './serial-monitor.css';

View File

@@ -0,0 +1,86 @@
.serial-monitor-container {
height: 100%;
display: flex;
flex-direction: column;
}
.serial-monitor-container .head {
display: flex;
padding: 5px;
background: var(--theia-brand-color2);
height: 27px;
}
.serial-monitor-container .head .send {
display: flex;
flex:1;
}
.serial-monitor-container .head .send .btn {
display: flex;
padding: 0 5px;
align-items: center;
background: var(--theia-brand-color3);
color: var(--theia-ui-dialog-font-color);
}
.serial-monitor-container .head .send form {
flex: 1;
display: flex;
}
.serial-monitor-container .head .send input#serial-monitor-send {
background: var(--theia-layout-color0);
flex: 1;
}
.serial-monitor-container .head .send input:focus {
outline: none;
}
.serial-monitor-container .head .config {
display: flex;
}
.serial-monitor-container .head .config .serial-monitor-select {
margin-left: 5px;
}
#serial-monitor-output-container {
overflow: auto;
flex: 1;
padding: 6px;
}
.p-TabBar-toolbar .item.arduino-monitor {
width: 24px;
justify-content: center;
font-size: medium;
box-sizing: border-box;
}
.p-TabBar-toolbar .item.arduino-monitor.toggled {
border: var(--theia-ui-button-color-hover) var(--theia-border-width) solid;
}
.p-TabBar-toolbar .item .clear-all {
background: var(--theia-icon-clear) no-repeat;
}
/* React Select Styles */
.serial-monitor-select .sms__control {
border: var(--theia-border-color1) var(--theia-border-width) solid;
}
.serial-monitor-select .sms__option--is-selected {
background-color: var(--theia-ui-button-color-secondary-hover);
color: var(--theia-content-font-color0);
}
.serial-monitor-select .sms__option--is-focused {
background-color: var(--theia-ui-button-color-secondary-hover);
}
.serial-monitor-select .sms__menu {
background-color: var(--theia-layout-color1);
}

View File

@@ -1,3 +0,0 @@
.p-Widget.p-TabBar.theia-app-centers.theia-app-bottom .p-TabBar-content-container.ps {
display: none;
}