Skip to content
This repository has been archived by the owner on Nov 14, 2023. It is now read-only.

Commit

Permalink
Supported multi-line input, fixed issue in aam with losing selection (c…
Browse files Browse the repository at this point in the history
…vat-ai#6458)

<!-- Raise an issue to propose your change
(https://github.com/opencv/cvat/issues).
It helps to avoid duplication of efforts from multiple independent
contributors.
Discuss your ideas with maintainers to be sure that changes will be
approved and merged.
Read the [Contribution
guide](https://opencv.github.io/cvat/docs/contributing/). -->

<!-- Provide a general summary of your changes in the Title above -->

### Motivation and context
<!-- Why is this change required? What problem does it solve? If it
fixes an open
issue, please link to the issue here. Describe your changes in detail,
add
screenshots. -->

### How has this been tested?
<!-- Please describe in detail how you tested your changes.
Include details of your testing environment, and the tests you ran to
see how your change affects other areas of the code, etc. -->

### Checklist
<!-- Go over all the following points, and put an `x` in all the boxes
that apply.
If an item isn't applicable for some reason, then ~~explicitly
strikethrough~~ the whole
line. If you don't do that, GitHub will show incorrect progress for the
pull request.
If you're unsure about any of these, don't hesitate to ask. We're here
to help! -->
- [x] I submit my changes into the `develop` branch
- [x] I have added a description of my changes into the
[CHANGELOG](https://github.com/opencv/cvat/blob/develop/CHANGELOG.md)
file
- [ ] I have updated the documentation accordingly
- [x] I have added tests to cover my changes
- [ ] I have linked related issues (see [GitHub docs](

https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
- [x] I have increased versions of npm packages if it is necessary

([cvat-canvas](https://github.com/opencv/cvat/tree/develop/cvat-canvas#versioning),

[cvat-core](https://github.com/opencv/cvat/tree/develop/cvat-core#versioning),

[cvat-data](https://github.com/opencv/cvat/tree/develop/cvat-data#versioning)
and

[cvat-ui](https://github.com/opencv/cvat/tree/develop/cvat-ui#versioning))

### License

- [x] I submit _my code changes_ under the same [MIT License](
https://github.com/opencv/cvat/blob/develop/LICENSE) that covers the
project.
  Feel free to contact the maintainers if that's a concern.
  • Loading branch information
bsekachev authored and mikhail-treskin committed Oct 25, 2023
1 parent e4c7be9 commit b218fbe
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 64 deletions.
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

0 comments on commit b218fbe

Please sign in to comment.