From b218fbe039f7081b1511a68ea8ce067d36196f7c Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 12 Jul 2023 14:42:05 +0300 Subject: [PATCH] Supported multi-line input, fixed issue in aam with losing selection (#6458) ### Motivation and context ### How has this been tested? ### Checklist - [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. --- CHANGELOG.md | 2 +- cvat-canvas/package.json | 2 +- cvat-canvas/src/typescript/canvasView.ts | 34 +++++++--- cvat-ui/package.json | 2 +- .../attribute-editor.tsx | 67 ++++++++++++++----- .../object-item-attribute.tsx | 51 ++++++-------- .../components/labels-editor/label-form.tsx | 8 +++ .../e2e/actions_tasks/mutable_attributes.js | 2 +- .../issues_prs/issue_1919_check_text_attr.js | 27 ++++++++ ...d_attribute_correspond_chosen_attribute.js | 4 +- tests/cypress/support/commands.js | 2 +- 11 files changed, 137 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7800f0fc6bb7..3ae14be49c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 () ### Changed - TDB diff --git a/cvat-canvas/package.json b/cvat-canvas/package.json index 68b60997cca0..64ac4c6e81f7 100644 --- a/cvat-canvas/package.json +++ b/cvat-canvas/package.json @@ -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": { diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts index fa886c872831..220a3de19028 100644 --- a/cvat-canvas/src/typescript/canvasView.ts +++ b/cvat-canvas/src/typescript/canvasView.ts @@ -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 { @@ -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', + }); + }); } } }) diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f96a51c6dc7a..f506b0e81a1b 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -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": { diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx index e472692e1b01..707a7a6c556c 100644 --- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx +++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-editor.tsx @@ -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 { @@ -27,6 +28,17 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element { inputType, attrID, clientID, values, currentValue, onChange, } = parameters; + const ref = useRef(null); + const [selectionStart, setSelectionStart] = useState(currentValue.length); + + useEffect(() => { + const textArea = ref?.current?.resizableTextArea?.textArea; + if (textArea instanceof HTMLTextAreaElement) { + textArea.selectionStart = selectionStart; + textArea.selectionEnd = selectionStart; + } + }, [currentValue]); + const renderCheckbox = (): JSX.Element => ( <> Checkbox: @@ -77,34 +89,55 @@ function renderInputElement(parameters: InputElementParameters): JSX.Element { ); - const handleKeydown = (event: React.KeyboardEvent): void => { + const handleKeydown = (event: React.KeyboardEvent): 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 ( + <> + Number: +
+ { + if (typeof value === 'number') { + onChange(`${value}`); + } + }} + onKeyDown={handleKeydown} + /> +
+ + ); + }; + const renderText = (): JSX.Element => ( <> - {inputType === 'number' ? Number: : Text: } + Text:
- ) => { + value={currentValue} + onChange={(event: React.ChangeEvent) => { 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} /> @@ -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(); } diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx index ca930a0fc818..53f28619fb4e 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-attribute.tsx @@ -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; @@ -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(null); + const [selectionStart, setSelectionStart] = useState(attrValue.length); + + useEffect(() => { + const textArea = ref?.current?.resizableTextArea?.textArea; + if (textArea instanceof HTMLTextAreaElement) { + textArea.selectionStart = selectionStart; + textArea.selectionEnd = selectionStart; + } + }, [attrValue]); if (attrInputType === 'checkbox') { return ( @@ -150,44 +161,24 @@ function ItemAttributeComponent(props: Props): JSX.Element { ); } - const ref = useRef(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 ( <> {attrName} - ): 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): void => { + if (ref.current?.resizableTextArea?.textArea) { + setSelectionStart(ref.current.resizableTextArea.textArea.selectionStart); } - changeAttribute(attrID, event.target.value); }} value={attrValue} diff --git a/cvat-ui/src/components/labels-editor/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index 7bf295af5645..72fdbc90b0a5 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -317,6 +317,14 @@ export default class LabelForm extends React.Component { const { key } = fieldInstance; const value = attr ? attr.values[0] : ''; + if (attr?.input_type.toUpperCase() === 'TEXT') { + return ( + + + + ); + } + return ( diff --git a/tests/cypress/e2e/actions_tasks/mutable_attributes.js b/tests/cypress/e2e/actions_tasks/mutable_attributes.js index 27b40aa92b7d..04a91b99191d 100644 --- a/tests/cypress/e2e/actions_tasks/mutable_attributes.js +++ b/tests/cypress/e2e/actions_tasks/mutable_attributes.js @@ -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); diff --git a/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js b/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js index 44462446c1c1..abb6ea94f44a 100644 --- a/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js +++ b/tests/cypress/e2e/issues_prs/issue_1919_check_text_attr.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 Intel Corporation // // SPDX-License-Identifier: MIT @@ -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) @@ -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) @@ -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); + }); + }); }); }); diff --git a/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js b/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js index 9727f8e6442b..6b67969f8193 100644 --- a/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js +++ b/tests/cypress/e2e/issues_prs2/issue_1425_highlighted_attribute_correspond_chosen_attribute.js @@ -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', () => { @@ -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); }); }); }); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 88fbf076ff84..2aa532b492ab 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -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'); });