summaryrefslogtreecommitdiffstats
path: root/sw/qa/uitest/writer_tests/trackedChanges.py
diff options
context:
space:
mode:
Diffstat (limited to 'sw/qa/uitest/writer_tests/trackedChanges.py')
-rw-r--r--sw/qa/uitest/writer_tests/trackedChanges.py471
1 files changed, 471 insertions, 0 deletions
diff --git a/sw/qa/uitest/writer_tests/trackedChanges.py b/sw/qa/uitest/writer_tests/trackedChanges.py
new file mode 100644
index 000000000..04834aef0
--- /dev/null
+++ b/sw/qa/uitest/writer_tests/trackedChanges.py
@@ -0,0 +1,471 @@
+# -*- tab-width: 4; indent-tabs-mode: nil; py-indent-offset: 4 -*-
+#
+# This file is part of the LibreOffice project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+
+# tests for tracked changes ; tdf912270
+from uitest.framework import UITestCase
+from uitest.uihelper.common import get_state_as_dict, get_url_for_data_file, type_text, select_by_text
+from libreoffice.uno.propertyvalue import mkPropertyValues
+from tempfile import TemporaryDirectory
+from org.libreoffice.unotest import systemPathToFileUrl
+import os.path
+
+class trackedchanges(UITestCase):
+
+ def test_tdf91270(self):
+
+ with self.ui_test.create_doc_in_start_center("writer") as document:
+
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+ type_text(xWriterEdit, "Test")
+
+ self.xUITest.executeCommand(".uno:TrackChanges")
+
+ self.xUITest.executeCommand(".uno:SelectAll") #select whole text
+ self.xUITest.executeCommand(".uno:Cut") #cut text
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close"):
+ pass
+
+ def test_tracked_changes_accept(self):
+
+ with self.ui_test.create_doc_in_start_center("writer") as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, "Test LibreOffice")
+ self.xUITest.executeCommand(".uno:ShowTrackedChanges")
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ xAccBtn = xTrackDlg.getChild("accept")
+ xAccBtn.executeAction("CLICK", tuple())
+
+ self.assertEqual(document.Text.String[0:16], "Test LibreOffice")
+
+ def test_tracked_changes_acceptall(self):
+
+ with self.ui_test.create_doc_in_start_center("writer") as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, "Test LibreOffice")
+ self.xUITest.executeCommand(".uno:ShowTrackedChanges")
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+
+ xAccBtn = xTrackDlg.getChild("acceptall")
+ xAccBtn.executeAction("CLICK", tuple())
+
+
+ self.assertEqual(document.Text.String[0:16], "Test LibreOffice")
+
+ def test_tracked_changes_reject(self):
+
+ with self.ui_test.create_doc_in_start_center("writer") as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, "Test LibreOffice")
+ self.xUITest.executeCommand(".uno:ShowTrackedChanges")
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+
+ xRejBtn = xTrackDlg.getChild("reject")
+ xRejBtn.executeAction("CLICK", tuple())
+
+ self.assertEqual(document.Text.String[0:1], "")
+
+ def test_tracked_changes_rejectall(self):
+
+ with self.ui_test.create_doc_in_start_center("writer") as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, "Test LibreOffice")
+ self.xUITest.executeCommand(".uno:ShowTrackedChanges")
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+
+ xAccBtn = xTrackDlg.getChild("rejectall")
+ xAccBtn.executeAction("CLICK", tuple())
+
+ self.assertEqual(document.Text.String[0:1], "")
+
+ def test_tracked_changes_zprev_next(self):
+
+ with self.ui_test.create_doc_in_start_center("writer") as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, "Test LibreOffice")
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, " Test2")
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, " Test3")
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, " Test4")
+ self.xUITest.executeCommand(".uno:TrackChanges")
+ type_text(xWriterEdit, " Test5")
+ self.xUITest.executeCommand(".uno:PreviousTrackedChange")
+ self.xUITest.executeCommand(".uno:RejectTrackedChange")
+ self.assertEqual(document.Text.String[0:37], "Test LibreOffice Test2 Test3 Test4")
+
+ self.xUITest.executeCommand(".uno:PreviousTrackedChange")
+ self.xUITest.executeCommand(".uno:PreviousTrackedChange")
+ self.xUITest.executeCommand(".uno:AcceptTrackedChange")
+ self.assertEqual(document.Text.String[0:37], "Test LibreOffice Test2 Test3 Test4")
+
+ self.xUITest.executeCommand(".uno:NextTrackedChange")
+ self.xUITest.executeCommand(".uno:RejectTrackedChange")
+ self.assertEqual(document.Text.String[0:30], "Test LibreOffice Test2 Test4")
+
+
+ def test_list_of_changes(self):
+ with self.ui_test.load_file(get_url_for_data_file("trackedChanges.odt")) as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ listText = [
+ "Unknown Author\t01/24/2020 16:19:32\t",
+ "Unknown Author\t01/24/2020 16:19:35\t",
+ "Unknown Author\t01/24/2020 16:19:39\t",
+ "Unknown Author\t01/24/2020 16:19:39\t",
+ "Xisco Fauli\t01/27/2020 17:42:55\t"]
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+
+ resultsAccept = [
+ "The tennis ball is a small ball. The baskedtball is much bigger.",
+ "The tennis ball is a small ball. The baskedtball is much bigger.",
+ "The tennis ball is a small ball. The baskedtball is much bigger.",
+ "The tennis ball is a small ball. The basketball is much bigger.",
+ "The tennis ball is a small ball. The basketball is much bigger.",
+ "The tennis ball is a small ball. The basketball is much bigger."]
+
+ for i in range(len(listText)):
+ self.assertEqual(document.Text.String.strip(), resultsAccept[i])
+ self.assertEqual(get_state_as_dict(changesList.getChild('0'))["Text"], listText[i] )
+ xAccBtn = xTrackDlg.getChild("accept")
+ xAccBtn.executeAction("CLICK", tuple())
+
+ self.assertEqual(document.Text.String.strip(), resultsAccept[5])
+ #List is empty
+ self.assertFalse('0' in changesList.getChildren())
+
+ for i in reversed(range(len(listText))):
+ xUndoBtn = xTrackDlg.getChild("undo")
+ xUndoBtn.executeAction("CLICK", tuple())
+ self.assertEqual(document.Text.String.strip(), resultsAccept[i])
+ self.assertEqual(get_state_as_dict(changesList.getChild('0'))["Text"], listText[i] )
+
+ resultsReject = [
+ "The tennis ball is a small ball. The baskedtball is much bigger.",
+ "The tenis ball is a small ball. The baskedtball is much bigger.",
+ "The tenis ball is a small bal. The baskedtball is much bigger.",
+ "The tenis ball is a small bal. The baskedtball is much bigger.",
+ "The tenis ball is a small bal. The baskedball is much bigger.",
+ "The tenis ball is a small bal. The baskedball is much biger."]
+
+ for i in range(len(listText)):
+ self.assertEqual(document.Text.String.strip(), resultsReject[i])
+ self.assertEqual(get_state_as_dict(changesList.getChild('0'))["Text"], listText[i] )
+ xAccBtn = xTrackDlg.getChild("reject")
+ xAccBtn.executeAction("CLICK", tuple())
+
+ self.assertEqual(document.Text.String.strip(), resultsReject[5])
+ #List is empty
+ self.assertFalse('0' in changesList.getChildren())
+
+ for i in reversed(range(len(listText))):
+ xUndoBtn = xTrackDlg.getChild("undo")
+ xUndoBtn.executeAction("CLICK", tuple())
+ self.assertEqual(document.Text.String.strip(), resultsReject[i])
+ self.assertEqual(get_state_as_dict(changesList.getChild('0'))["Text"], listText[i] )
+
+
+ def test_tdf135018(self):
+ with self.ui_test.load_file(get_url_for_data_file("tdf135018.odt")) as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ self.assertEqual(5, document.CurrentController.PageCount)
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+ self.assertEqual(111, len(changesList.getChildren()))
+
+ # Without the fix in place, it would have crashed here
+ xAccBtn = xTrackDlg.getChild("acceptall")
+ xAccBtn.executeAction("CLICK", tuple())
+
+ self.assertEqual(0, len(changesList.getChildren()))
+
+ xUndoBtn = xTrackDlg.getChild("undo")
+ xUndoBtn.executeAction("CLICK", tuple())
+
+ self.assertEqual(111, len(changesList.getChildren()))
+
+
+ # Check the changes are shown after opening the Manage Tracked Changes dialog
+ self.assertGreater(document.CurrentController.PageCount, 5)
+
+ def test_tdf144270_tracked_table_rows(self):
+ with self.ui_test.load_file(get_url_for_data_file("TC-table-del-add.docx")) as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ tables = document.getTextTables()
+ self.assertEqual(3, len(tables))
+
+ # Accept
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+
+ # This was 14 (every cell is a different change instead of counting rows or tables)
+ # Now: 4 changes (2 deleted/inserted rows and 2 deleted/inserted tables)
+ self.assertEqual(4, len(changesList.getChildren()))
+
+ # Without the fix in place, it would have crashed here
+ for i in (3, 2, 1, 0):
+ xAccBtn = xTrackDlg.getChild("accept")
+ xAccBtn.executeAction("CLICK", tuple())
+ self.assertEqual(i, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(2, len(tables))
+
+ for i in range(1, 5):
+ xUndoBtn = xTrackDlg.getChild("undo")
+ xUndoBtn.executeAction("CLICK", tuple())
+ self.assertEqual(i, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(3, len(tables))
+
+ # Accept All
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+
+ # This was 14 (every cell is a different change instead of counting rows or tables)
+ # Now: 4 changes (2 deleted/inserted rows and 2 deleted/inserted tables)
+ self.assertEqual(4, len(changesList.getChildren()))
+
+ xAccBtn = xTrackDlg.getChild("acceptall")
+ xAccBtn.executeAction("CLICK", tuple())
+ self.assertEqual(0, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(2, len(tables))
+
+ xUndoBtn = xTrackDlg.getChild("undo")
+ xUndoBtn.executeAction("CLICK", tuple())
+ self.assertEqual(4, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(3, len(tables))
+
+ # goto to the start of the document to reject from the first tracked table row change
+ self.xUITest.executeCommand(".uno:GoToStartOfDoc")
+
+ # Reject
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+
+ # This was 14 (every cell is a different change instead of counting rows or tables)
+ # Now: 4 changes (2 deleted/inserted rows and 2 deleted/inserted tables)
+ self.assertEqual(4, len(changesList.getChildren()))
+
+ # Without the fix in place, it would have crashed here
+ for i in (3, 2, 1, 0):
+ xAccBtn = xTrackDlg.getChild("reject")
+ xAccBtn.executeAction("CLICK", tuple())
+ self.assertEqual(i, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(2, len(tables))
+
+ for i in range(1, 5):
+ xUndoBtn = xTrackDlg.getChild("undo")
+ xUndoBtn.executeAction("CLICK", tuple())
+ self.assertEqual(i, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(3, len(tables))
+
+ # Reject All
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+
+ # This was 14 (every cell is a different change instead of counting rows or tables)
+ # Now: 4 changes (2 deleted/inserted rows and 2 deleted/inserted tables)
+ self.assertEqual(4, len(changesList.getChildren()))
+
+ xAccBtn = xTrackDlg.getChild("rejectall")
+ xAccBtn.executeAction("CLICK", tuple())
+ self.assertEqual(0, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(2, len(tables))
+
+ xUndoBtn = xTrackDlg.getChild("undo")
+ xUndoBtn.executeAction("CLICK", tuple())
+ self.assertEqual(4, len(changesList.getChildren()))
+
+ tables = document.getTextTables()
+ self.assertEqual(3, len(tables))
+
+ def test_tdf148032(self):
+
+ with self.ui_test.load_file(get_url_for_data_file("trackedChanges.odt")) as document:
+
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ # adding new Comment
+ self.xUITest.executeCommand(".uno:InsertAnnotation")
+
+ # wait until the comment is available
+ xComment1 = self.ui_test.wait_until_child_is_available('Comment1')
+
+ xEditView1 = xComment1.getChild("editview")
+ xEditView1.executeAction("TYPE", mkPropertyValues({"TEXT": "This is the First Comment"}))
+ self.assertEqual(get_state_as_dict(xComment1)["Text"], "This is the First Comment" )
+ self.assertEqual(get_state_as_dict(xComment1)["Resolved"], "false" )
+ self.assertEqual(get_state_as_dict(xComment1)["Author"], "Unknown Author" )
+ self.assertEqual(get_state_as_dict(xComment1)["ReadOnly"], "false" )
+
+ xComment1.executeAction("LEAVE", mkPropertyValues({}))
+
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+ self.assertEqual(6, len(changesList.getChildren()))
+
+ xChild = changesList.getChild(0)
+ # This was False (missing comment)
+ self.assertEqual(True, get_state_as_dict(xChild)["Text"].endswith('\tComment added'))
+
+ def test_tdf147179(self):
+ with self.ui_test.load_file(get_url_for_data_file("TC-table-del-add.docx")) as document:
+ xWriterDoc = self.xUITest.getTopFocusWindow()
+ xWriterEdit = xWriterDoc.getChild("writer_edit")
+
+ tables = document.getTextTables()
+ self.assertEqual(3, len(tables))
+
+ # Select text of the tracked row, not only text of its first cell
+ with self.ui_test.execute_modeless_dialog_through_command(".uno:AcceptTrackedChanges", close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild("writerchanges")
+
+ # select second tracked table row in tree list
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "DOWN"}))
+ xToolkit = self.xContext.ServiceManager.createInstance('com.sun.star.awt.Toolkit')
+ while get_state_as_dict(xWriterEdit)["SelectedText"] != 'klj':
+ xToolkit.processEventsToIdle()
+
+ # this was "j" (only text of the first cell was selected, not text of the row)
+ self.assertEqual(get_state_as_dict(xWriterEdit)["SelectedText"], "klj" )
+
+ # select first tracked table row in tree list
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "UP"}))
+ while get_state_as_dict(xWriterEdit)["SelectedText"] != 'bca':
+ xToolkit.processEventsToIdle()
+
+ # this was "a" (only text of the first cell was selected, not text of the row)
+ self.assertEqual(get_state_as_dict(xWriterEdit)["SelectedText"], "bca" )
+
+ def test_RedlineSuccessorData(self):
+ with TemporaryDirectory() as tempdir:
+ xFilePath = os.path.join(tempdir, "redlinesuccessordata-temp.odt")
+ with self.ui_test.load_file(get_url_for_data_file("redlinesuccessordata.docx")) as document:
+
+ # check tracked deletion in tracked insertion
+ with self.ui_test.execute_modeless_dialog_through_command('.uno:AcceptTrackedChanges', close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild('writerchanges')
+ # four children, but only three visible
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['Children'], '4')
+ self.assertEqual(state['VisibleCount'], '3')
+
+ # select tracked deletion with RedlineSuccessorData in tree list
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "DOWN"}))
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['SelectEntryText'], 'Kelemen Gábor 2\t05/19/2021 12:35:00\t')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['Children'], '1')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['VisibleChildCount'], '0')
+
+ # open tree node with the tracked insertion: four visible children
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "RIGHT"}))
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['Children'], '4')
+ self.assertEqual(state['VisibleCount'], '4')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['Children'], '1')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['VisibleChildCount'], '1')
+
+ # select tracked insertion in tree list
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "DOWN"}))
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['SelectEntryText'], 'First Person\t10/21/2012 23:45:00\t')
+
+ # Save the DOCX document as ODT with a tracked deletion in a tracked insertion
+ with self.ui_test.execute_dialog_through_command(".uno:SaveAs", close_button="open") as xSaveDialog:
+ xFileName = xSaveDialog.getChild("file_name")
+ xFileName.executeAction("TYPE", mkPropertyValues({"KEYCODE":"CTRL+A"}))
+ xFileName.executeAction("TYPE", mkPropertyValues({"KEYCODE":"BACKSPACE"}))
+ xFileName.executeAction("TYPE", mkPropertyValues({"TEXT": xFilePath}))
+ xFileTypeCombo = xSaveDialog.getChild("file_type")
+ select_by_text(xFileTypeCombo, "ODF Text Document (.odt)")
+
+ self.ui_test.wait_until_file_is_available(xFilePath)
+ # load the temporary file, and check ODF roundtrip of the tracked deletion in a tracked insertion
+ with self.ui_test.load_file(systemPathToFileUrl(xFilePath)) as document:
+ # check tracked deletion in tracked insertion
+ with self.ui_test.execute_modeless_dialog_through_command('.uno:AcceptTrackedChanges', close_button="close") as xTrackDlg:
+ changesList = xTrackDlg.getChild('writerchanges')
+ # four children, but only three visible
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['Children'], '4')
+ self.assertEqual(state['VisibleCount'], '3')
+
+ # select tracked deletion with RedlineSuccessorData in tree list
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "DOWN"}))
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['SelectEntryText'], 'Kelemen Gábor 2\t05/19/2021 12:35:00\t')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['Children'], '1')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['VisibleChildCount'], '0')
+
+ # open tree node with the tracked insertion: four visible children
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "RIGHT"}))
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['Children'], '4')
+ self.assertEqual(state['VisibleCount'], '4')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['Children'], '1')
+ self.assertEqual(get_state_as_dict(changesList.getChild(1))['VisibleChildCount'], '1')
+
+ # select tracked insertion in tree list
+ changesList.executeAction("TYPE", mkPropertyValues({"KEYCODE": "DOWN"}))
+ state = get_state_as_dict(changesList)
+ self.assertEqual(state['SelectEntryText'], 'First Person\t10/21/2012 23:45:00\t')
+
+ # reject all
+ xAccBtn = xTrackDlg.getChild("rejectall")
+ xAccBtn.executeAction("CLICK", tuple())
+ # FIXME why we need double rejectall (dialog-only)?
+ xAccBtn.executeAction("CLICK", tuple())
+ self.assertEqual(0, len(changesList.getChildren()))
+ # This was false, because of not rejected tracked deletion
+ # of the text "inserts": "Document text inserts document"...
+ self.assertTrue(document.getText().getString().startswith('Document text document text'))
+
+# vim: set shiftwidth=4 softtabstop=4 expandtab: