1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-04-29 19:45:01 +02:00

Add ability to prevent terminal title updates from ANSI Escape sequence

When a more complete implementation of ANSI Escape sequence for
renaming terminal titles was added in
[CDT 10.2](https://github.com/eclipse-cdt/cdt/blob/main/NewAndNoteworthy/CDT-10.2.md#rename-terminal-tab)
it caused a regression in use cases where extenders of the terminal
wanted to retain control of the terminal's title.

This commit adds a new flag that will prevent the title of the
terminal tab from being updated from ANSI escape sequences.

Fixes #494
This commit is contained in:
Maksym Oleksiv 2023-08-19 03:36:56 +03:00 committed by GitHub
parent c9a38e541f
commit 422aea14e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 199 additions and 30 deletions

View file

@ -18,6 +18,13 @@ In cases where the configured binary parser does not support the launching of GN
# API Changes, current and planned
## New API to create terminal that blocks updating title from ANSI escape sequence
When calling `ITerminalService.openConsole(Map<String, Object> properties, Done done)` the `properties` understands a new key that allows the option to disable updating titles from ASNI Escape Sequences.
By setting `ITerminalsConnectorConstants.PROP_TITLE_DISABLE_ANSI_TITLE` to `true` the title of the tab terminal will not be affected by ANSI escape sequences.
## Breaking API changes
Please see [CHANGELOG-API](CHANGELOG-API.md) for details on the breaking API changes in this release as well as future planned API changes.
# Noteworthy Issues and Pull Requests

View file

@ -604,3 +604,15 @@ spelled BuiltinDetectionArgsGeneric instead.
These APIs will be removed and remote connection for attach launch will be moved in the implementation of `IGDBProcesses.attachDebuggerToProcess()`.
See https://github.com/eclipse-cdt/cdt/pull/336
## API Removals after September 2025
### Terminal Control API for setting title without `requestor` will be removed
These APIs will be removed and are replaced by versions with `requestor` parameter.
- org.eclipse.tm.internal.terminal.control.ITerminalListener.setTerminalTitle(String)
- org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl.setTerminalTitle(String)
- org.eclipse.tm.internal.terminal.control.ITerminalViewControl.setTerminalTitle(String)
See https://github.com/eclipse-cdt/cdt/issues/494

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.connector.remote;singleton:=true
Bundle-Version: 4.8.0.qualifier
Bundle-Version: 4.8.100.qualifier
Bundle-Vendor: %providerName
Bundle-Localization: plugin
Require-Bundle: org.eclipse.ui,
@ -13,8 +13,8 @@ Require-Bundle: org.eclipse.ui,
org.eclipse.core.resources,
org.eclipse.swt,
org.eclipse.jface,
org.eclipse.tm.terminal.view.core;bundle-version="4.5.0";resolution:=optional,
org.eclipse.tm.terminal.view.ui;bundle-version="4.5.0";resolution:=optional,
org.eclipse.tm.terminal.view.core;bundle-version="4.10.0";resolution:=optional,
org.eclipse.tm.terminal.view.ui;bundle-version="4.11.100";resolution:=optional,
org.eclipse.core.expressions
Bundle-RequiredExecutionEnvironment: JavaSE-17
Export-Package: org.eclipse.tm.terminal.connector.remote,

View file

@ -33,6 +33,7 @@ import org.eclipse.remote.core.IRemoteProcessService;
import org.eclipse.remote.core.IRemoteProcessTerminalService;
import org.eclipse.remote.core.IRemoteServicesManager;
import org.eclipse.remote.core.exception.RemoteConnectionException;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.emulator.VT100Emulator;
import org.eclipse.tm.internal.terminal.emulator.VT100TerminalControl;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
@ -126,7 +127,7 @@ public class RemoteConnectionManager extends Job {
control.setVT100LineWrapping(true);
connector.setInputStream(remoteProcess.getInputStream());
control.setState(TerminalState.CONNECTED);
control.setTerminalTitle(remoteConnection.getName());
control.setTerminalTitle(remoteConnection.getName(), TerminalTitleRequestor.OTHER);
connector.setOutputStream(remoteProcess.getOutputStream());
// Initialize terminal size
VT100Emulator text = ((VT100TerminalControl) control).getTerminalText();

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.control; singleton:=true
Bundle-Version: 5.4.100.qualifier
Bundle-Version: 5.5.0.qualifier
Bundle-Activator: org.eclipse.tm.internal.terminal.control.impl.TerminalPlugin
Bundle-Vendor: %providerName
Bundle-Localization: plugin

View file

@ -28,8 +28,10 @@ public interface ITerminalListener {
void setState(TerminalState state);
/**
* Set the title of the terminal.
* @deprecated Migrate to implementing {@link ITerminalListener3} and
* override {@link ITerminalListener3#setTerminalTitle(String, String)
* @param title
*/
@Deprecated(forRemoval = true)
void setTerminalTitle(String title);
}

View file

@ -0,0 +1,40 @@
/*******************************************************************************
* Copyright (c) 2023 Infineon Technologies AG. All Rights Reserved.
*
* This program and the accompanying materials are made available under the terms
* of the Eclipse Public License 2.0 which accompanies this distribution, and is
* available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*******************************************************************************/
package org.eclipse.tm.internal.terminal.control;
/**
* Terminal listener allowing to listen to terminal selection changes.
* The interface overrides the deprecated {@link #setTerminalTitle(String)} with
* {@link #setTerminalTitle(String, String)} that should be called instead.
*
* @since 5.5
*/
public interface ITerminalListener3 extends ITerminalListener2 {
/**
* Enum defines terminal title change requestors for
* setTerminalTitle method.
*
* @since 5.5
*/
enum TerminalTitleRequestor {
ANSI, // Terminal tab title change requested using ANSI command in terminal.
MENU, // Terminal tab title change requested from menu.
OTHER; // Terminal tab title change requested by other requestors.
}
/**
* Set the title of the terminal.
*
* @param title Terminal title.
* @param requestor Item that requests terminal title update.
*/
void setTerminalTitle(String title, TerminalTitleRequestor requestor);
}

View file

@ -22,6 +22,7 @@ import java.nio.charset.Charset;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.widgets.Control;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalConnector;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
@ -192,9 +193,19 @@ public interface ITerminalViewControl {
/**
* @since 5.1
* @deprecated call {@link #setTerminalTitle(String, String)} instead
*/
@Deprecated(forRemoval = true)
void setTerminalTitle(String newTitle);
/**
* Set the title of the terminal.
* @param newTitle
* @param requestor Item that requests terminal title update.
* @since 5.5
*/
void setTerminalTitle(String newTitle, TerminalTitleRequestor requestor);
/**
* @since 5.2
*/

View file

@ -14,6 +14,7 @@ package org.eclipse.tm.internal.terminal.control.impl;
import java.io.OutputStream;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalConnector;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
@ -28,7 +29,13 @@ public interface ITerminalControlForText {
void setState(TerminalState state);
void setTerminalTitle(String title);
/**
* Set the title of the terminal.
*
* @param title Termianl title.
* @param requestor Item that requests terminal title update.
*/
void setTerminalTitle(String title, TerminalTitleRequestor requestor);
ITerminalConnector getTerminalConnector();

View file

@ -45,6 +45,7 @@ import java.util.Arrays;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.control.impl.ITerminalControlForText;
import org.eclipse.tm.internal.terminal.control.impl.TerminalPlugin;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalConnector;
@ -486,7 +487,7 @@ public class VT100Emulator implements ControlListener {
Logger.log("Ignoring unsupported ANSI OSC sequence: '" + ansiOsCommand + "'"); //$NON-NLS-1$ //$NON-NLS-2$
return;
}
terminal.setTerminalTitle(ansiOsCommand.substring(2));
terminal.setTerminalTitle(ansiOsCommand.substring(2), TerminalTitleRequestor.ANSI);
}
/**

View file

@ -98,6 +98,8 @@ import org.eclipse.swt.widgets.Shell;
import org.eclipse.tm.internal.terminal.control.ICommandInputField;
import org.eclipse.tm.internal.terminal.control.ITerminalListener;
import org.eclipse.tm.internal.terminal.control.ITerminalListener2;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.control.ITerminalMouseListener;
import org.eclipse.tm.internal.terminal.control.ITerminalViewControl;
import org.eclipse.tm.internal.terminal.control.impl.ITerminalControlForText;
@ -1261,7 +1263,17 @@ public class VT100TerminalControl implements ITerminalControlForText, ITerminalC
@Override
public void setTerminalTitle(String title) {
fTerminalListener.setTerminalTitle(title);
setTerminalTitle(title, TerminalTitleRequestor.OTHER);
}
@Override
public void setTerminalTitle(String title, TerminalTitleRequestor requestor) {
if (fTerminalListener instanceof ITerminalListener3 listener3) {
listener3.setTerminalTitle(title, requestor);
} else {
fTerminalListener.setTerminalTitle(title);
}
}
@Override

View file

@ -22,6 +22,7 @@ import java.nio.charset.Charset;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
/**
* Represents the terminal view as seen by a terminal connection.
@ -155,11 +156,19 @@ public interface ITerminalControl {
OutputStream getRemoteToTerminalOutputStream();
/**
* Set the title of the terminal view.
* @param title
* @deprecated call {@link #setTerminalTitle(String, String)} instead
*/
@Deprecated(forRemoval = true)
void setTerminalTitle(String title);
/**
* Set the title of the terminal view.
* @param title Termianl title.
* @param requestor Item that requests terminal title update.
* @since 5.5
*/
void setTerminalTitle(String title, TerminalTitleRequestor requestor);
/**
* Show an error message during connect.
* @param msg

View file

@ -22,6 +22,7 @@ import java.nio.charset.Charset;
import org.eclipse.core.runtime.Platform;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
@ -99,6 +100,10 @@ public class TerminalConnectorFactoryTest extends TestCase {
public void setTerminalTitle(String title) {
}
@Override
public void setTerminalTitle(String title, TerminalTitleRequestor requestor) {
}
@Override
public void setupTerminal(Composite parent) {
}

View file

@ -22,6 +22,7 @@ import java.nio.charset.Charset;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.tm.internal.terminal.connector.TerminalConnector.Factory;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
@ -99,6 +100,10 @@ public class TerminalConnectorTest extends TestCase {
public void setTerminalTitle(String title) {
}
@Override
public void setTerminalTitle(String title, TerminalTitleRequestor requestor) {
}
@Override
public void setupTerminal(Composite parent) {
}

View file

@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.control.impl.ITerminalControlForText;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalConnector;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
@ -32,8 +33,10 @@ public class MockTerminalControlForText implements ITerminalControlForText {
}
@Override
public void setTerminalTitle(String title) {
allTitles.add(title);
public void setTerminalTitle(String title, TerminalTitleRequestor requestor) {
if (requestor == TerminalTitleRequestor.ANSI) {
allTitles.add(title);
}
}
public List<String> getAllTitles() {

View file

@ -17,6 +17,7 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
import org.eclipse.tm.internal.terminal.provisional.api.Logger;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
@ -114,7 +115,7 @@ public class SpeedTestConnection extends Thread {
}
private void setTitle(final String title) {
Display.getDefault().asyncExec(() -> fControl.setTerminalTitle(title));
Display.getDefault().asyncExec(() -> fControl.setTerminalTitle(title, TerminalTitleRequestor.OTHER));
}
}

View file

@ -19,6 +19,7 @@ import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.provisional.api.ISettingsStore;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalControl;
import org.eclipse.tm.internal.terminal.provisional.api.Logger;
@ -48,7 +49,7 @@ public class SpeedTestConnector extends TerminalConnectorImpl {
return;
}
fOutputStream = System.out;
fControl.setTerminalTitle(fSettings.getInputFile());
fControl.setTerminalTitle(fSettings.getInputFile(), TerminalTitleRequestor.OTHER);
fConnection = new SpeedTestConnection(fInputStream, fSettings, fControl);
fConnection.start();
}

View file

@ -25,6 +25,7 @@ import java.io.OutputStream;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.control.impl.ITerminalControlForText;
import org.eclipse.tm.internal.terminal.emulator.VT100Emulator;
import org.eclipse.tm.internal.terminal.provisional.api.ITerminalConnector;
@ -212,7 +213,7 @@ final class VT100DataSource implements IDataSource {
}
@Override
public void setTerminalTitle(String title) {
public void setTerminalTitle(String title, TerminalTitleRequestor requestor) {
}
@Override

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.view.core;singleton:=true
Bundle-Version: 4.9.100.qualifier
Bundle-Version: 4.10.0.qualifier
Bundle-Activator: org.eclipse.tm.terminal.view.core.activator.CoreBundleActivator
Bundle-Vendor: %providerName
Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400",

View file

@ -71,6 +71,14 @@ public interface ITerminalsConnectorConstants {
*/
public static final String PROP_TITLE = "title"; //$NON-NLS-1$
/**
* Property: Flag to disable updating the terminal title from ANSI escape sequences.
* <p>
* Property Type: {@link Boolean}
* @since 4.10
*/
public static final String PROP_TITLE_DISABLE_ANSI_TITLE = "titleDisableAnsiTitle"; //$NON-NLS-1$
/**
* Property: The encoding of the terminal tab to open.
* <p>

View file

@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.tm.terminal.view.ui;singleton:=true
Bundle-Version: 4.11.100.qualifier
Bundle-Version: 4.11.200.qualifier
Bundle-Activator: org.eclipse.tm.terminal.view.ui.activator.UIPlugin
Bundle-Vendor: %providerName
Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400",
@ -11,8 +11,8 @@ Require-Bundle: org.eclipse.core.expressions;bundle-version="3.4.400",
org.eclipse.core.variables;bundle-version="3.2.600",
org.eclipse.debug.ui;bundle-version="3.8.1";resolution:=optional,
org.eclipse.egit.ui;bundle-version="2.0.0";resolution:=optional,
org.eclipse.tm.terminal.view.core;bundle-version="[4.8.0,5.0.0)",
org.eclipse.tm.terminal.control;bundle-version="[5.2.0,6.0.0)",
org.eclipse.tm.terminal.view.core;bundle-version="[4.10.0,5.0.0)",
org.eclipse.tm.terminal.control;bundle-version="[5.5.0,6.0.0)",
org.eclipse.ui;bundle-version="3.8.0",
org.eclipse.ui.ide;bundle-version="3.18.0";resolution:=optional,
org.eclipse.ui.editors;bundle-version="3.14.0";resolution:=optional,

View file

@ -15,6 +15,7 @@ import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.window.Window;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3.TerminalTitleRequestor;
import org.eclipse.tm.internal.terminal.control.ITerminalViewControl;
import org.eclipse.tm.internal.terminal.control.actions.AbstractTerminalAction;
import org.eclipse.tm.terminal.view.ui.nls.Messages;
@ -51,7 +52,7 @@ public class RenameTerminalAction extends AbstractTerminalAction {
if (inputDialog.open() == Window.OK) {
String value = inputDialog.getValue();
if (value != null) {
target.setTerminalTitle(value);
target.setTerminalTitle(value, TerminalTitleRequestor.MENU);
}
}

View file

@ -293,6 +293,12 @@ public class TerminalService implements ITerminalService {
flags.put(ITerminalsConnectorConstants.PROP_DATA_NO_RECONNECT,
(Boolean) properties.get(ITerminalsConnectorConstants.PROP_DATA_NO_RECONNECT));
}
if (properties.get(ITerminalsConnectorConstants.PROP_TITLE_DISABLE_ANSI_TITLE) instanceof Boolean) {
flags.put(ITerminalsConnectorConstants.PROP_TITLE_DISABLE_ANSI_TITLE,
(Boolean) properties.get(ITerminalsConnectorConstants.PROP_TITLE_DISABLE_ANSI_TITLE));
} else {
flags.put(ITerminalsConnectorConstants.PROP_TITLE_DISABLE_ANSI_TITLE, false);
}
// Open the new console
CTabItem item;
item = ConsoleManager.getInstance().openConsole(id, secondaryId, title, encoding, connector, data,

View file

@ -11,19 +11,22 @@
*******************************************************************************/
package org.eclipse.tm.terminal.view.ui.tabs;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tm.internal.terminal.control.ITerminalListener2;
import org.eclipse.tm.internal.terminal.control.ITerminalListener3;
import org.eclipse.tm.internal.terminal.provisional.api.TerminalState;
import org.eclipse.tm.terminal.view.core.interfaces.constants.ITerminalsConnectorConstants;
import org.eclipse.tm.terminal.view.ui.nls.Messages;
/**
* Terminal tab default terminal listener implementation.
*/
public class TabTerminalListener implements ITerminalListener2 {
public class TabTerminalListener implements ITerminalListener3 {
private static final String TAB_TERMINAL_LISTENER = "TabTerminalListener"; //$NON-NLS-1$
/* default */ final TabFolderManager tabFolderManager;
private CTabItem tabItem;
@ -79,7 +82,7 @@ public class TabTerminalListener implements ITerminalListener2 {
@Override
public void setState(final TerminalState state) {
this.state = state;
updateTitle();
updateTitle(null, TerminalTitleRequestor.OTHER);
// The tab item must have been not yet disposed
final CTabItem item = getTabItem();
@ -104,7 +107,7 @@ public class TabTerminalListener implements ITerminalListener2 {
});
}
private void updateTitle() {
private void updateTitle(final String title, final TerminalTitleRequestor requestor) {
if (state == null) {
// first setState hasn't happened yet, it will
// soon and the title will be update then.
@ -114,11 +117,33 @@ public class TabTerminalListener implements ITerminalListener2 {
if (item == null || item.isDisposed()) {
return;
}
// Run asynchronously in the display thread
item.getDisplay().asyncExec(() -> {
if (item.isDisposed()) {
// tab has been closed
return;
}
// Get the original terminal properties associated with the tab item
@SuppressWarnings({ "unchecked" })
final Map<String, Object> properties = (Map<String, Object>) item.getData("properties"); //$NON-NLS-1$
if (properties.containsKey(ITerminalsConnectorConstants.PROP_TITLE_DISABLE_ANSI_TITLE)) {
if (properties.get(
ITerminalsConnectorConstants.PROP_TITLE_DISABLE_ANSI_TITLE) instanceof Boolean disableAnsi) {
// Check if terminal title can be updated from ANSI escape sequence
if (disableAnsi && requestor == TerminalTitleRequestor.ANSI) {
return;
}
}
}
// New title must have value.
if (title != null) {
tabItemTitle = title;
}
// Update the tab item title
String newTitle = getTerminalConsoleTabTitle(state);
final String newTitle = getTerminalConsoleTabTitle(state);
if (newTitle != null)
item.setText(newTitle);
});
@ -162,9 +187,20 @@ public class TabTerminalListener implements ITerminalListener2 {
}
@Override
public void setTerminalTitle(String title) {
tabItemTitle = title;
updateTitle();
public void setTerminalTitle(final String title) {
throw new UnsupportedOperationException("Should not be called as this class implements ITerminalListener3"); //$NON-NLS-1$
}
/**
* Sets Terminal title and checks if originator is ANSI command.
* If originator is ANSI command in terminal and user does not want to use
* ANSI command to update terminal then return else update title.
* @param title Title to update.
* @param requestor Item that requests terminal title update.
*/
@Override
public void setTerminalTitle(final String title, final TerminalTitleRequestor requestor) {
updateTitle(title, requestor);
}
/**