mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-27 03:54:24 +01:00
Move messageList near its only consumer, messageService
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
|
||||
import errors = require('vs/base/common/errors');
|
||||
import types = require('vs/base/common/types');
|
||||
import {MessageList, Severity as BaseSeverity} from 'vs/base/browser/ui/messagelist/messageList';
|
||||
import {MessageList, Severity as BaseSeverity} from 'vs/workbench/services/message/browser/messagelist/messageList';
|
||||
import {IDisposable} from 'vs/base/common/lifecycle';
|
||||
import {IMessageService, IMessageWithAction, IConfirmation, Severity} from 'vs/platform/message/common/message';
|
||||
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.global-message-list {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
z-index: 2000; /* on top of the world */
|
||||
color: #eee;
|
||||
list-style-type: none;
|
||||
line-height: 35px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 70%;
|
||||
left: 15%;
|
||||
}
|
||||
|
||||
.vs .global-message-list {
|
||||
box-shadow: 0 2px 8px #A8A8A8;
|
||||
}
|
||||
|
||||
.vs-dark .global-message-list {
|
||||
box-shadow: 0 2px 8px #000;
|
||||
}
|
||||
|
||||
.global-message-list.transition {
|
||||
-webkit-transition: top 200ms linear;
|
||||
-ms-transition: top 200ms linear;
|
||||
-moz-transition: top 200ms linear;
|
||||
-khtml-transition: top 200ms linear;
|
||||
-o-transition: top 200ms linear;
|
||||
transition: top 200ms linear;
|
||||
}
|
||||
|
||||
.global-message-list ul.message-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
overflow: hidden;
|
||||
-webkit-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box; /* Important so that borders added don't add to the overal size */
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry-with-action {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry .message-left-side {
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-o-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry .message-left-side.severity {
|
||||
padding: 2px 4px;
|
||||
margin-right: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: -moz-none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vs .global-message-list li.message-list-entry .message-left-side.severity.app-error {
|
||||
background-color: #BE1100;
|
||||
}
|
||||
|
||||
.vs-dark .global-message-list li.message-list-entry .message-left-side.severity.app-error {
|
||||
background-color: #BE1100;
|
||||
}
|
||||
|
||||
.vs .global-message-list li.message-list-entry .message-left-side.severity.app-warning {
|
||||
background-color: #B89500;
|
||||
}
|
||||
|
||||
.vs-dark .global-message-list li.message-list-entry .message-left-side.severity.app-warning {
|
||||
background-color: #B89500;
|
||||
}
|
||||
|
||||
.vs .global-message-list li.message-list-entry .message-left-side.severity.app-info {
|
||||
background-color: #007ACC;
|
||||
}
|
||||
|
||||
.vs-dark .global-message-list li.message-list-entry .message-left-side.severity.app-info {
|
||||
background-color: #007ACC;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry .message-left-side.message-overflow-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry .actions-container {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry .actions-container .message-action {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.vs .global-message-list li.message-list-entry .actions-container .message-action {
|
||||
background-color: #1382CE;
|
||||
}
|
||||
|
||||
.vs .global-message-list li.message-list-entry .actions-container .message-action:hover {
|
||||
background-color: rgb(0, 107, 179);
|
||||
}
|
||||
|
||||
.vs-dark .global-message-list li.message-list-entry .actions-container .message-action {
|
||||
background-color: #0E639C;
|
||||
}
|
||||
|
||||
.vs-dark .global-message-list li.message-list-entry .actions-container .message-action:hover {
|
||||
background-color: #007ACC;
|
||||
}
|
||||
|
||||
.global-message-list li.message-list-entry .actions-container .message-action .action-button {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.vs .global-message-list {
|
||||
background-color: #2C2C2C;
|
||||
}
|
||||
|
||||
.vs-dark .global-message-list {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
/* High Contrast Theming */
|
||||
.hc-black .global-message-list {
|
||||
background-color: #000;
|
||||
outline: 2px solid #6FC3DF;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.hc-black .global-message-list {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hc-black .global-message-list li.message-list-entry .message-left-side.severity {
|
||||
border: 1px solid #6FC3DF;
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./messageList';
|
||||
import nls = require('vs/nls');
|
||||
import {TPromise} from 'vs/base/common/winjs.base';
|
||||
import {Builder, $} from 'vs/base/browser/builder';
|
||||
import DOM = require('vs/base/browser/dom');
|
||||
import errors = require('vs/base/common/errors');
|
||||
import aria = require('vs/base/browser/ui/aria/aria');
|
||||
import types = require('vs/base/common/types');
|
||||
import Event, {Emitter} from 'vs/base/common/event';
|
||||
import {Action} from 'vs/base/common/actions';
|
||||
import htmlRenderer = require('vs/base/browser/htmlContentRenderer');
|
||||
import {StandardKeyboardEvent} from 'vs/base/browser/keyboardEvent';
|
||||
import {CommonKeybindings} from 'vs/base/common/keyCodes';
|
||||
|
||||
export enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
export interface IMessageWithAction {
|
||||
message: string;
|
||||
actions: Action[];
|
||||
}
|
||||
|
||||
interface IMessageEntry {
|
||||
id: any;
|
||||
text: string;
|
||||
severity: Severity;
|
||||
time?: number;
|
||||
count?: number;
|
||||
actions?: Action[];
|
||||
}
|
||||
|
||||
export class IMessageListOptions {
|
||||
purgeInterval: number;
|
||||
maxMessages: number;
|
||||
maxMessageLength: number;
|
||||
}
|
||||
|
||||
export interface IUsageLogger {
|
||||
publicLog(eventName: string, data?: any): void;
|
||||
}
|
||||
|
||||
export class MessageList {
|
||||
|
||||
private static DEFAULT_MESSAGE_PURGER_INTERVAL = 10000;
|
||||
private static DEFAULT_MAX_MESSAGES = 5;
|
||||
private static DEFAULT_MAX_MESSAGE_LENGTH = 500;
|
||||
|
||||
private messages: IMessageEntry[];
|
||||
private messageListPurger: TPromise<void>;
|
||||
private messageListContainer: Builder;
|
||||
|
||||
private container: HTMLElement;
|
||||
private options: IMessageListOptions;
|
||||
private usageLogger: IUsageLogger;
|
||||
|
||||
private _onMessagesShowing: Emitter<void>;
|
||||
private _onMessagesCleared: Emitter<void>;
|
||||
|
||||
constructor(container: HTMLElement, usageLogger?: IUsageLogger, options: IMessageListOptions = { purgeInterval: MessageList.DEFAULT_MESSAGE_PURGER_INTERVAL, maxMessages: MessageList.DEFAULT_MAX_MESSAGES, maxMessageLength: MessageList.DEFAULT_MAX_MESSAGE_LENGTH }) {
|
||||
this.messages = [];
|
||||
this.messageListPurger = null;
|
||||
this.container = container;
|
||||
this.usageLogger = usageLogger;
|
||||
this.options = options;
|
||||
|
||||
this._onMessagesShowing = new Emitter<void>();
|
||||
this._onMessagesCleared = new Emitter<void>();
|
||||
}
|
||||
|
||||
public get onMessagesShowing(): Event<void> {
|
||||
return this._onMessagesShowing.event;
|
||||
}
|
||||
|
||||
public get onMessagesCleared(): Event<void> {
|
||||
return this._onMessagesCleared.event;
|
||||
}
|
||||
|
||||
public showMessage(severity: Severity, message: string): () => void;
|
||||
public showMessage(severity: Severity, message: Error): () => void;
|
||||
public showMessage(severity: Severity, message: string[]): () => void;
|
||||
public showMessage(severity: Severity, message: Error[]): () => void;
|
||||
public showMessage(severity: Severity, message: IMessageWithAction): () => void;
|
||||
public showMessage(severity: Severity, message: any): () => void {
|
||||
if (Array.isArray(message)) {
|
||||
const closeFns: Function[] = [];
|
||||
message.forEach((msg: any) => closeFns.push(this.showMessage(severity, msg)));
|
||||
|
||||
return () => closeFns.forEach((fn) => fn());
|
||||
}
|
||||
|
||||
// Return only if we are unable to extract a message text
|
||||
const messageText = this.getMessageText(message);
|
||||
if (!messageText || typeof messageText !== 'string') {
|
||||
return () => {/* empty */ };
|
||||
}
|
||||
|
||||
// Show message
|
||||
return this.doShowMessage(message, messageText, severity);
|
||||
}
|
||||
|
||||
private getMessageText(message: any): string {
|
||||
if (types.isString(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
if (message instanceof Error) {
|
||||
return errors.toErrorMessage(message, false);
|
||||
}
|
||||
|
||||
if ((<IMessageWithAction>message).message) {
|
||||
return (<IMessageWithAction>message).message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private doShowMessage(id: string, message: string, severity: Severity): () => void;
|
||||
private doShowMessage(id: Error, message: string, severity: Severity): () => void;
|
||||
private doShowMessage(id: IMessageWithAction, message: string, severity: Severity): () => void;
|
||||
private doShowMessage(id: any, message: string, severity: Severity): () => void {
|
||||
|
||||
// Trigger Auto-Purge of messages to keep list small
|
||||
this.purgeMessages();
|
||||
|
||||
// Store in Memory (new messages come first so that they show up on top)
|
||||
this.messages.unshift({
|
||||
id: id,
|
||||
text: message,
|
||||
severity: severity,
|
||||
time: Date.now(),
|
||||
actions: (<IMessageWithAction>id).actions
|
||||
});
|
||||
|
||||
// Render
|
||||
this.renderMessages(true, 1);
|
||||
|
||||
// Support in Screen Readers too
|
||||
let alertText: string;
|
||||
if (severity === Severity.Error) {
|
||||
alertText = nls.localize('alertErrorMessage', "Error: {0}", message);
|
||||
} else if (severity === Severity.Warning) {
|
||||
alertText = nls.localize('alertWarningMessage', "Warning: {0}", message);
|
||||
} else {
|
||||
alertText = nls.localize('alertInfoMessage', "Info: {0}", message);
|
||||
}
|
||||
|
||||
aria.alert(alertText);
|
||||
|
||||
return () => {
|
||||
this.hideMessage(id);
|
||||
};
|
||||
}
|
||||
|
||||
private renderMessages(animate: boolean, delta: number): void {
|
||||
const container = $(this.container);
|
||||
|
||||
// Lazily create, otherwise clear old
|
||||
if (!this.messageListContainer) {
|
||||
this.messageListContainer = $('.global-message-list').appendTo(container);
|
||||
} else {
|
||||
$(this.messageListContainer).empty();
|
||||
$(this.messageListContainer).removeClass('transition');
|
||||
}
|
||||
|
||||
// Support animation for messages by moving the container out of view and then in
|
||||
if (animate) {
|
||||
$(this.messageListContainer).style('top', '-35px');
|
||||
}
|
||||
|
||||
// Render Messages as List Items
|
||||
$(this.messageListContainer).ul({ 'class': 'message-list' }, (ul: Builder) => {
|
||||
const messages = this.prepareMessages();
|
||||
if (messages.length > 0) {
|
||||
this._onMessagesShowing.fire();
|
||||
} else {
|
||||
this._onMessagesCleared.fire();
|
||||
}
|
||||
|
||||
messages.forEach((message: IMessageEntry, index: number) => {
|
||||
this.renderMessage(message, $(ul), messages.length, delta);
|
||||
});
|
||||
|
||||
// Support animation for messages by moving the container out of view and then in
|
||||
if (animate) {
|
||||
setTimeout(() => {
|
||||
$(this.messageListContainer).addClass('transition');
|
||||
$(this.messageListContainer).style('top', '0');
|
||||
}, 50 /* Need this delay to reliably get the animation on some browsers */);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private renderMessage(message: IMessageEntry, container: Builder, total: number, delta: number): void {
|
||||
container.li({ class: 'message-list-entry message-list-entry-with-action' }, (li) => {
|
||||
|
||||
// Actions (if none provided, add one default action to hide message)
|
||||
const messageActions = this.getMessageActions(message);
|
||||
li.div({ class: 'actions-container' }, (actionContainer) => {
|
||||
for (let i = 0; i < messageActions.length; i++) {
|
||||
const action = messageActions[i];
|
||||
actionContainer.div({ class: 'message-action' }, (div) => {
|
||||
div.a({ class: 'action-button', tabindex: '0', role: 'button' }).text(action.label).on([DOM.EventType.CLICK, DOM.EventType.KEY_DOWN], (e) => {
|
||||
if (e instanceof KeyboardEvent) {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (!event.equals(CommonKeybindings.ENTER) && !event.equals(CommonKeybindings.SPACE)) {
|
||||
return; // Only handle Enter/Escape for keyboard access
|
||||
}
|
||||
}
|
||||
|
||||
DOM.EventHelper.stop(e, true);
|
||||
|
||||
if (this.usageLogger) {
|
||||
this.usageLogger.publicLog('workbenchActionExecuted', { id: action.id, from: 'message' });
|
||||
}
|
||||
|
||||
(action.run() || TPromise.as(null))
|
||||
.then<any>(null, error => this.showMessage(Severity.Error, error))
|
||||
.done((r) => {
|
||||
if (r === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideMessage(message.text); // hide all matching the text since there may be duplicates
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Text
|
||||
const text = message.text.substr(0, this.options.maxMessageLength);
|
||||
li.div({ class: 'message-left-side' }, (div) => {
|
||||
div.addClass('message-overflow-ellipsis');
|
||||
|
||||
// Severity indicator
|
||||
const sev = message.severity;
|
||||
const label = (sev === Severity.Error) ? nls.localize('error', "Error") : (sev === Severity.Warning) ? nls.localize('warning', "Warn") : nls.localize('info', "Info");
|
||||
$().span({ class: 'message-left-side severity ' + ((sev === Severity.Error) ? 'app-error' : (sev === Severity.Warning) ? 'app-warning' : 'app-info'), text: label }).appendTo(div);
|
||||
|
||||
// Error message
|
||||
const messageContentElement: HTMLElement = <any>htmlRenderer.renderHtml({
|
||||
tagName: 'span',
|
||||
className: 'message-left-side',
|
||||
formattedText: text
|
||||
});
|
||||
|
||||
$(messageContentElement).title(messageContentElement.textContent).appendTo(div);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getMessageActions(message: IMessageEntry): Action[] {
|
||||
let messageActions: Action[];
|
||||
if (message.actions && message.actions.length > 0) {
|
||||
messageActions = message.actions;
|
||||
} else {
|
||||
messageActions = [
|
||||
new Action('close.message.action', nls.localize('close', "Close"), null, true, () => {
|
||||
this.hideMessage(message.text); // hide all matching the text since there may be duplicates
|
||||
|
||||
return TPromise.as(true);
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
return messageActions;
|
||||
}
|
||||
|
||||
private prepareMessages(): IMessageEntry[] {
|
||||
|
||||
// Aggregate Messages by text to reduce their count
|
||||
const messages: IMessageEntry[] = [];
|
||||
const handledMessages: { [message: string]: number; } = {};
|
||||
|
||||
let offset = 0;
|
||||
for (let i = 0; i < this.messages.length; i++) {
|
||||
const message = this.messages[i];
|
||||
if (types.isUndefinedOrNull(handledMessages[message.text])) {
|
||||
message.count = 1;
|
||||
messages.push(message);
|
||||
handledMessages[message.text] = offset++;
|
||||
} else {
|
||||
messages[handledMessages[message.text]].count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (messages.length > this.options.maxMessages) {
|
||||
return messages.splice(messages.length - this.options.maxMessages, messages.length);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private disposeMessages(messages: IMessageEntry[]): void {
|
||||
messages.forEach((message) => {
|
||||
if (message.actions) {
|
||||
message.actions.forEach((action) => {
|
||||
action.dispose();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public hideMessages(): void {
|
||||
this.hideMessage();
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
if (this.messageListContainer && this.messageListContainer.isHidden()) {
|
||||
this.messageListContainer.show();
|
||||
}
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this.messageListContainer && !this.messageListContainer.isHidden()) {
|
||||
this.messageListContainer.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private hideMessage(messageText?: string): void;
|
||||
private hideMessage(messageObj?: any): void {
|
||||
let messageFound = false;
|
||||
|
||||
for (let i = 0; i < this.messages.length; i++) {
|
||||
const message = this.messages[i];
|
||||
let hide = false;
|
||||
|
||||
// Hide specific message
|
||||
if (messageObj) {
|
||||
hide = ((types.isString(messageObj) && message.text === messageObj) || message.id === messageObj);
|
||||
}
|
||||
|
||||
// Hide all messages
|
||||
else {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
if (hide) {
|
||||
this.disposeMessages(this.messages.splice(i, 1));
|
||||
i--;
|
||||
messageFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (messageFound) {
|
||||
this.renderMessages(false, -1);
|
||||
}
|
||||
}
|
||||
|
||||
private purgeMessages(): void {
|
||||
|
||||
// Cancel previous
|
||||
if (this.messageListPurger) {
|
||||
this.messageListPurger.cancel();
|
||||
}
|
||||
|
||||
// Configure
|
||||
this.messageListPurger = TPromise.timeout(this.options.purgeInterval).then(() => {
|
||||
let needsUpdate = false;
|
||||
let counter = 0;
|
||||
|
||||
for (let i = 0; i < this.messages.length; i++) {
|
||||
const message = this.messages[i];
|
||||
|
||||
// Only purge infos and warnings and only if they are not providing actions
|
||||
if (message.severity !== Severity.Error && !message.actions) {
|
||||
this.disposeMessages(this.messages.splice(i, 1));
|
||||
counter--;
|
||||
i--;
|
||||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
this.renderMessages(false, counter);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user