Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supported multi-line input, fixed issue in aam with losing selection #6458

Merged
merged 8 commits into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## \[Unreleased]
### Added
- TDB
- Multi-line text attributes supported (<https://github.com/opencv/cvat/pull/6458>)

### Changed
- TDB
Expand Down
2 changes: 1 addition & 1 deletion cvat-canvas/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.17.0",
"version": "2.17.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
Expand Down
34 changes: 23 additions & 11 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2667,9 +2667,20 @@ export class CanvasViewImpl implements CanvasView, Listener {
});
}

for (const tspan of (text.lines() as any).members) {
tspan.attr('x', text.attr('x'));
function applyParentX(parentText: SVGTSpanElement | SVGTextElement): void {
for (let i = 0; i < parentText.children.length; i++) {
if (i === 0) {
// do not align the first child
continue;
}

const tspan = parentText.children[i];
tspan.setAttribute('x', parentText.getAttribute('x'));
applyParentX(tspan as SVGTSpanElement);
}
}

applyParentX(text.node as any as SVGTextElement);
}

private deleteText(clientID: number): void {
Expand Down Expand Up @@ -2733,15 +2744,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
if (withAttr) {
for (const attrID of Object.keys(attributes)) {
const value = attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID];
block
.tspan(`${attrNames[attrID]}: ${value}`)
.attr({
attrID,
dy: '1em',
x: 0,
})
.addClass('cvat_canvas_text_attribute');
const values = `${attributes[attrID] === undefinedAttrValue ? '' : attributes[attrID]}`.split('\n');
const parent = block.tspan(`${attrNames[attrID]}: `)
.attr({ attrID, dy: '1em', x: 0 }).addClass('cvat_canvas_text_attribute');
values.forEach((attrLine: string, index: number) => {
parent
.tspan(attrLine)
.attr({
dy: index === 0 ? 0 : '1em',
});
});
}
}
})
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.53.0",
"version": "1.53.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
//
// SPDX-License-Identifier: MIT

import React from 'react';
import React, { useState, useRef, useEffect } from 'react';
import Text from 'antd/lib/typography/Text';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Select, { SelectValue } from 'antd/lib/select';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import Input from 'antd/lib/input';
import { TextAreaRef } from 'antd/lib/input/TextArea';
import InputNumber from 'antd/lib/input-number';

import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';

import config from 'config';

interface InputElementParameters {
Expand All @@ -27,6 +28,17 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element {
inputType, attrID, clientID, values, currentValue, onChange,
} = parameters;

const ref = useRef<TextAreaRef>(null);
const [selectionStart, setSelectionStart] = useState<number>(currentValue.length);

useEffect(() => {
const textArea = ref?.current?.resizableTextArea?.textArea;
if (textArea instanceof HTMLTextAreaElement) {
textArea.selectionStart = selectionStart;
textArea.selectionEnd = selectionStart;
}
}, [currentValue]);

const renderCheckbox = (): JSX.Element => (
<>
<Text strong>Checkbox: </Text>
Expand Down Expand Up @@ -77,34 +89,55 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element {
</>
);

const handleKeydown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
const handleKeydown = (event: React.KeyboardEvent<HTMLTextAreaElement | HTMLInputElement>): void => {
if (['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift', 'Control'].includes(event.key)) {
event.preventDefault();
event.stopPropagation();
const copyEvent = new KeyboardEvent('keydown', event);
window.document.dispatchEvent(copyEvent);
}
};

const renderNumber = (): JSX.Element => {
const [min, max, step] = values;
return (
<>
<Text strong>Number: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<InputNumber
autoFocus
min={+min}
max={+max}
step={+step}
value={+currentValue}
key={`${clientID}:${attrID}`}
onChange={(value: number | null) => {
if (typeof value === 'number') {
onChange(`${value}`);
}
}}
onKeyDown={handleKeydown}
/>
</div>
</>
);
};

const renderText = (): JSX.Element => (
<>
{inputType === 'number' ? <Text strong>Number: </Text> : <Text strong>Text: </Text>}
<Text strong>Text: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Input
<Input.TextArea
autoFocus
ref={ref}
key={`${clientID}:${attrID}`}
defaultValue={currentValue}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
value={currentValue}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
const { value } = event.target;
if (inputType === 'number') {
if (value !== '') {
const numberValue = +value;
if (!Number.isNaN(numberValue)) {
onChange(`${numberValue}`);
}
}
} else {
onChange(value);
if (ref.current?.resizableTextArea?.textArea) {
setSelectionStart(ref.current.resizableTextArea.textArea.selectionStart);
}
onChange(value);
}}
onKeyDown={handleKeydown}
/>
Expand All @@ -119,6 +152,8 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element {
element = renderSelect();
} else if (inputType === 'radio') {
element = renderRadio();
} else if (inputType === 'number') {
element = renderNumber();
} else {
element = renderText();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { Col } from 'antd/lib/grid';
import Select from 'antd/lib/select';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import Text from 'antd/lib/typography/Text';

import config from 'config';
import { clamp } from 'utils/math';
import TextArea, { TextAreaRef } from 'antd/lib/input/TextArea';

interface Props {
readonly: boolean;
Expand All @@ -39,10 +39,21 @@ function attrIsTheSame(prevProps: Props, nextProps: Props): boolean {

function ItemAttributeComponent(props: Props): JSX.Element {
const {
attrInputType, attrValues, attrValue, attrName, attrID, readonly, changeAttribute,
attrInputType, attrValues, attrValue,
attrName, attrID, readonly, changeAttribute,
} = props;

const attrNameStyle: React.CSSProperties = { wordBreak: 'break-word', lineHeight: '1em', fontSize: 12 };
const ref = useRef<TextAreaRef>(null);
const [selectionStart, setSelectionStart] = useState<number>(attrValue.length);

useEffect(() => {
const textArea = ref?.current?.resizableTextArea?.textArea;
if (textArea instanceof HTMLTextAreaElement) {
textArea.selectionStart = selectionStart;
textArea.selectionEnd = selectionStart;
}
}, [attrValue]);

if (attrInputType === 'checkbox') {
return (
Expand Down Expand Up @@ -150,44 +161,24 @@ function ItemAttributeComponent(props: Props): JSX.Element {
);
}

const ref = useRef<Input>(null);
const [selection, setSelection] = useState<{
start: number | null;
end: number | null;
direction: 'forward' | 'backward' | 'none' | null;
}>({
start: null,
end: null,
direction: null,
});

useEffect(() => {
if (ref.current && ref.current.input) {
ref.current.input.selectionStart = selection.start;
ref.current.input.selectionEnd = selection.end;
ref.current.input.selectionDirection = selection.direction;
}
}, [attrValue]);

return (
<>
<Col span={8} style={attrNameStyle}>
<Text className='cvat-text'>{attrName}</Text>
</Col>
<Col span={16}>
<Input
<TextArea
ref={ref}
size='small'
disabled={readonly}
onChange={(event: React.ChangeEvent<HTMLInputElement>): void => {
if (ref.current && ref.current.input) {
setSelection({
start: ref.current.input.selectionStart,
end: ref.current.input.selectionEnd,
direction: ref.current.input.selectionDirection,
});
style={{
height: Math.min(120, attrValue.split('\n').length * 24),
minHeight: Math.min(120, attrValue.split('\n').length * 24),
}}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
if (ref.current?.resizableTextArea?.textArea) {
setSelectionStart(ref.current.resizableTextArea.textArea.selectionStart);
}

changeAttribute(attrID, event.target.value);
}}
value={attrValue}
Expand Down
8 changes: 8 additions & 0 deletions cvat-ui/src/components/labels-editor/label-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,14 @@ export default class LabelForm extends React.Component<Props> {
const { key } = fieldInstance;
const value = attr ? attr.values[0] : '';

if (attr?.input_type.toUpperCase() === 'TEXT') {
return (
<Form.Item name={[key, 'values']} fieldKey={[fieldInstance.fieldKey, 'values']} initialValue={value}>
<Input.TextArea className='cvat-attribute-values-input' placeholder='Default value' />
</Form.Item>
);
}

return (
<Form.Item name={[key, 'values']} fieldKey={[fieldInstance.fieldKey, 'values']} initialValue={value}>
<Input className='cvat-attribute-values-input' placeholder='Default value' />
Expand Down
2 changes: 1 addition & 1 deletion tests/cypress/e2e/actions_tasks/mutable_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ context('Mutable attribute.', () => {
function testChangingAttributeValue(expectedValue, value) {
cy.get('.cvat-player-next-button').click();
cy.get('.attribute-annotation-sidebar-attr-elem-wrapper')
.find('[type="text"]')
.find('textarea')
.should('have.value', expectedValue)
.clear()
.type(value);
Expand Down
27 changes: 27 additions & 0 deletions tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (C) 2020-2022 Intel Corporation
// Copyright (C) 2023 Intel Corporation
//
// SPDX-License-Identifier: MIT

Expand Down Expand Up @@ -30,9 +31,11 @@ context('Check label attribute changes', () => {
cy.createRectangle(createRectangleShape2Points);
cy.get('#cvat_canvas_shape_1').trigger('mousemove').rightclick();
});

it('Open object menu details', () => {
cy.get('.cvat-canvas-context-menu').contains('DETAILS').click();
});

it('Clear field of text attribute and write new value', () => {
cy.get('.cvat-canvas-context-menu')
.contains(attrName)
Expand All @@ -44,6 +47,7 @@ context('Check label attribute changes', () => {
.type(newLabelAttrValue);
});
});

it('Check what value of right panel is changed too', () => {
cy.get('#cvat-objects-sidebar-state-item-1')
.contains(attrName)
Expand All @@ -52,5 +56,28 @@ context('Check label attribute changes', () => {
cy.get('.cvat-object-item-text-attribute').should('have.value', newLabelAttrValue);
});
});

it('Specify many lines for a text attribute, update the page and check values', () => {
const multilineValue = 'This text attributes has many lines.\n - Line 1\n - Line 2';
cy.get('.cvat-canvas-context-menu')
.contains(attrName)
.parents('.cvat-object-item-attribute-wrapper')
.within(() => {
cy.get('.cvat-object-item-text-attribute')
.clear()
.type(multilineValue);
});
cy.saveJob();
cy.reload();
cy.get('.cvat-canvas-container').should('exist').and('be.visible');
cy.get('#cvat-objects-sidebar-state-item-1')
.contains('DETAILS').click();
cy.get('#cvat-objects-sidebar-state-item-1')
.contains(attrName)
.parents('.cvat-object-item-attribute-wrapper')
.within(() => {
cy.get('.cvat-object-item-text-attribute').should('have.value', multilineValue);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ context('The highlighted attribute in AAM should correspond to the chosen attrib
});
});
cy.get('.cvat-attribute-annotation-sidebar-attr-editor').within(() => {
cy.get('[type="text"]').should('have.value', textValue);
cy.get('textarea').should('have.value', textValue);
});
});
it('Go to next attribute and check again', () => {
Expand All @@ -49,7 +49,7 @@ context('The highlighted attribute in AAM should correspond to the chosen attrib
});
});
cy.get('.cvat-attribute-annotation-sidebar-attr-editor').within(() => {
cy.get('[type="text"]').should('have.value', textValue);
cy.get('textarea').should('have.value', textValue);
});
});
});
Expand Down
2 changes: 1 addition & 1 deletion tests/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ Cypress.Commands.add('createPolygon', (createPolygonParams) => {

Cypress.Commands.add('openSettings', () => {
cy.get('.cvat-right-header').find('.cvat-header-menu-user-dropdown').trigger('mouseover', { which: 1 });
cy.get('.anticon-setting').click();
cy.get('.anticon-setting').should('exist').and('be.visible').click();
cy.get('.cvat-settings-modal').should('be.visible');
});

Expand Down