diff --git a/dist/text_texture_generator_v1.0.0.zip b/dist/text_texture_generator_v1.0.0.zip index b6062fa..de35f37 100644 Binary files a/dist/text_texture_generator_v1.0.0.zip and b/dist/text_texture_generator_v1.0.0.zip differ diff --git a/dist/text_texture_generator_v1.0.0_free.zip b/dist/text_texture_generator_v1.0.0_free.zip index f1fb9f0..e18e10d 100644 Binary files a/dist/text_texture_generator_v1.0.0_free.zip and b/dist/text_texture_generator_v1.0.0_free.zip differ diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc index 5820362..e879943 100644 Binary files a/src/__pycache__/__init__.cpython-311.pyc and b/src/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/operators/text_editor_ops.py b/src/operators/text_editor_ops.py index 140ed1f..6f930e8 100644 --- a/src/operators/text_editor_ops.py +++ b/src/operators/text_editor_ops.py @@ -1,19 +1,19 @@ import bpy from bpy.types import Operator from bpy.props import StringProperty -from ..utils.constants import has_multiline_text, get_version_limits +from ..utils.constants import has_multiline_text class TEXT_TEXTURE_OT_edit_multiline_text(Operator): - """Open a text editor for multiline input""" + """Open a text editor for single-line input""" bl_idname = "text_texture.edit_multiline_text" - bl_label = "Edit Multiline Text" - bl_description = "Open a text editor for multiline input" + bl_label = "Edit Text" + bl_description = "Open a text editor for single-line input" bl_options = {'REGISTER', 'UNDO'} temp_text: StringProperty( name="Text", - description="Text content being edited", + description="Single line text only - multiline not supported", default="", maxlen=4096 ) @@ -27,40 +27,59 @@ class TEXT_TEXTURE_OT_edit_multiline_text(Operator): """Copy current text to temp storage and open dialog""" props = context.scene.text_texture_props self.temp_text = props.text - return context.window_manager.invoke_props_dialog(self, width=600) + return context.window_manager.invoke_props_dialog(self, width=700) def draw(self, context): - """Draw the dialog UI""" + """Draw the dialog UI with single-line input""" layout = self.layout - limits = get_version_limits() - max_lines = limits.get("max_text_lines", 50) - max_chars = limits.get("max_text_length", 4096) - - current_lines = self.temp_text.count('\n') + 1 if self.temp_text else 0 + max_chars = 4096 current_chars = len(self.temp_text) + # Main column col = layout.column() - col.prop(self, "temp_text", text="") - row = layout.row() - row.label(text=f"Lines: {current_lines}/{max_lines} | Chars: {current_chars}/{max_chars}") + col.label(text="Edit Text (Single Line Only):", icon='EDITMODE_HLT') + + # Make the text input larger + text_row = col.row() + text_row.scale_y = 2.0 + text_row.prop(self, "temp_text", text="") + + # Warning box + col.separator() + info_box = col.box() + info_box.scale_y = 0.8 + info_box.label(text="⚠️ Single Line Only:", icon='ERROR') + row = info_box.row() + row.label(text="• Multiline text is not supported") + + # Statistics + col.separator() + stats_row = col.row() + stats_row.label(text=f"📝 Characters: {current_chars}/{max_chars}", icon='FONT_DATA') + + # Warnings + if current_chars > max_chars: + col.separator() + warning_box = col.box() + warning_box.alert = True + warning_box.label(text=f"⚠ Exceeds character limit! Will be trimmed to {max_chars} chars", icon='ERROR') def execute(self, context): - """Save temp text back to main property with limits enforced""" + """Save temp text back to main property - reject if contains newlines""" props = context.scene.text_texture_props - limits = get_version_limits() - max_lines = limits.get("max_text_lines", 50) - max_chars = limits.get("max_text_length", 4096) + # Check for newlines and reject + if '\n' in self.temp_text: + self.report({'ERROR'}, "Multiline text not supported. Please use single line only.") + return {'CANCELLED'} + max_chars = 4096 + + # Enforce character limit text = self.temp_text[:max_chars] - lines = text.split('\n') - if len(lines) > max_lines: - lines = lines[:max_lines] - text = '\n'.join(lines) - props.text = text return {'FINISHED'} diff --git a/src/utils/__pycache__/constants.cpython-311.pyc b/src/utils/__pycache__/constants.cpython-311.pyc index 4536649..b68e05a 100644 Binary files a/src/utils/__pycache__/constants.cpython-311.pyc and b/src/utils/__pycache__/constants.cpython-311.pyc differ diff --git a/tests/unit/operators/test_text_editor_ops.py b/tests/unit/operators/test_text_editor_ops.py index 9b6458f..49fdde4 100644 --- a/tests/unit/operators/test_text_editor_ops.py +++ b/tests/unit/operators/test_text_editor_ops.py @@ -5,6 +5,7 @@ Tests verify: - Operator class exists and is properly structured - PRO-only access control via poll() - Text processing and limit enforcement +- Single-line validation (rejects newlines) - Proper Blender operator contract """ @@ -53,13 +54,13 @@ class TestModalTextEditorOperator: operator = TEXT_TEXTURE_OT_edit_multiline_text() mock_context = Mock() - mock_context.scene.text_texture_props.text = "Line 1\nLine 2\nLine 3" + mock_context.scene.text_texture_props.text = "Single line text" mock_context.window_manager.invoke_props_dialog.return_value = {'RUNNING_MODAL'} mock_event = Mock() result = operator.invoke(mock_context, mock_event) - assert operator.temp_text == "Line 1\nLine 2\nLine 3" + assert operator.temp_text == "Single line text" assert result == {'RUNNING_MODAL'} mock_context.window_manager.invoke_props_dialog.assert_called_once() @@ -68,84 +69,83 @@ class TestModalTextEditorOperator: from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text operator = TEXT_TEXTURE_OT_edit_multiline_text() - operator.temp_text = "Edited Line 1\nEdited Line 2" + operator.temp_text = "Edited single line" mock_context = Mock() mock_context.scene.text_texture_props.text = "Old text" - with patch('src.operators.text_editor_ops.get_version_limits', return_value={ - 'max_text_lines': 50, - 'max_text_length': 4096 - }): - result = operator.execute(mock_context) + result = operator.execute(mock_context) assert result == {'FINISHED'} - assert mock_context.scene.text_texture_props.text == "Edited Line 1\nEdited Line 2" + assert mock_context.scene.text_texture_props.text == "Edited single line" def test_execute_enforces_character_limit(self): """execute() must truncate text exceeding max_text_length.""" from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text operator = TEXT_TEXTURE_OT_edit_multiline_text() - # Create text exceeding 100 char limit - operator.temp_text = "x" * 150 + # Create text exceeding 4096 char limit + operator.temp_text = "x" * 5000 mock_context = Mock() mock_context.scene.text_texture_props.text = "" - with patch('src.operators.text_editor_ops.get_version_limits', return_value={ - 'max_text_lines': 50, - 'max_text_length': 100 - }): - operator.execute(mock_context) + operator.execute(mock_context) saved_text = mock_context.scene.text_texture_props.text - assert len(saved_text) == 100, f"Expected 100 chars, got {len(saved_text)}" + assert len(saved_text) == 4096, f"Expected 4096 chars, got {len(saved_text)}" - def test_execute_enforces_line_limit(self): - """execute() must truncate lines exceeding max_text_lines.""" + def test_execute_rejects_text_with_newlines(self): + """execute() must reject text containing newline characters.""" from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text operator = TEXT_TEXTURE_OT_edit_multiline_text() - # Create 10 lines when limit is 5 - operator.temp_text = "\n".join([f"Line {i}" for i in range(10)]) + operator.temp_text = "Line 1\nLine 2" mock_context = Mock() - mock_context.scene.text_texture_props.text = "" + mock_context.scene.text_texture_props.text = "Old text" - with patch('src.operators.text_editor_ops.get_version_limits', return_value={ - 'max_text_lines': 5, - 'max_text_length': 4096 - }): - operator.execute(mock_context) + result = operator.execute(mock_context) - saved_text = mock_context.scene.text_texture_props.text - line_count = saved_text.count('\n') + 1 - assert line_count == 5, f"Expected 5 lines, got {line_count}" - assert saved_text == "Line 0\nLine 1\nLine 2\nLine 3\nLine 4" + # Should return CANCELLED + assert result == {'CANCELLED'} + # Should not modify the original text + assert mock_context.scene.text_texture_props.text == "Old text" + # Should report an error + operator.report.assert_called_once_with( + {'ERROR'}, + "Multiline text not supported. Please use single line only." + ) - def test_execute_handles_both_limits_simultaneously(self): - """execute() must enforce both character and line limits correctly.""" + def test_execute_rejects_multiple_newlines(self): + """execute() must reject text with multiple newline characters.""" from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text operator = TEXT_TEXTURE_OT_edit_multiline_text() - # Create 10 lines of 20 chars each (200 chars total) - operator.temp_text = "\n".join([f"Line {i:015d}" for i in range(10)]) + operator.temp_text = "Line 1\nLine 2\nLine 3\nLine 4" mock_context = Mock() - mock_context.scene.text_texture_props.text = "" + mock_context.scene.text_texture_props.text = "Old text" - with patch('src.operators.text_editor_ops.get_version_limits', return_value={ - 'max_text_lines': 5, - 'max_text_length': 150 - }): - operator.execute(mock_context) + result = operator.execute(mock_context) - saved_text = mock_context.scene.text_texture_props.text - # Should truncate to 150 chars first, then check lines - assert len(saved_text) <= 150 - line_count = saved_text.count('\n') + 1 - assert line_count <= 5 + assert result == {'CANCELLED'} + assert mock_context.scene.text_texture_props.text == "Old text" + + def test_execute_accepts_text_without_newlines(self): + """execute() must accept and save text without newlines.""" + from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text + + operator = TEXT_TEXTURE_OT_edit_multiline_text() + operator.temp_text = "This is a single line with no newlines" + + mock_context = Mock() + mock_context.scene.text_texture_props.text = "Old text" + + result = operator.execute(mock_context) + + assert result == {'FINISHED'} + assert mock_context.scene.text_texture_props.text == "This is a single line with no newlines" def test_operator_has_temp_text_property(self): """Operator must have StringProperty named temp_text.""" @@ -178,4 +178,50 @@ class TestModalTextEditorOperator: from src.operators import text_editor_ops assert hasattr(text_editor_ops, 'unregister') - assert callable(text_editor_ops.unregister) \ No newline at end of file + assert callable(text_editor_ops.unregister) + + +class TestSingleLineUIBehavior: + """Test suite for single-line text UI display and statistics.""" + + def test_draw_calculates_character_count(self): + """draw() must count characters correctly.""" + from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text + + operator = TEXT_TEXTURE_OT_edit_multiline_text() + mock_context = Mock() + + operator.temp_text = "Single line text" + operator.draw(mock_context) + + actual_chars = len(operator.temp_text) + expected_chars = 16 + assert actual_chars == expected_chars, f"Expected {expected_chars} chars, got {actual_chars}" + + def test_draw_handles_empty_text(self): + """draw() must handle empty text without errors.""" + from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text + + operator = TEXT_TEXTURE_OT_edit_multiline_text() + mock_context = Mock() + + operator.temp_text = "" + operator.draw(mock_context) + + char_count = len(operator.temp_text) + assert char_count == 0, f"Empty text should have 0 characters, got {char_count}" + + def test_draw_handles_long_single_line(self): + """draw() must handle long single-line text correctly.""" + from src.operators.text_editor_ops import TEXT_TEXTURE_OT_edit_multiline_text + + operator = TEXT_TEXTURE_OT_edit_multiline_text() + mock_context = Mock() + + # Create a very long single line + long_text = "x" * 5000 + operator.temp_text = long_text + operator.draw(mock_context) + + # Should track all characters + assert len(operator.temp_text) == 5000 \ No newline at end of file