diff --git a/owrx/active/list/__init__.py b/owrx/active/list/__init__.py index 718ba78d..0301744c 100644 --- a/owrx/active/list/__init__.py +++ b/owrx/active/list/__init__.py @@ -39,6 +39,12 @@ class ActiveListIndexDeleted(ActiveListChange): self.oldValue = oldValue +class ActiveListIndexMoved(ActiveListChange): + def __init__(self, old_index: int, new_index: int): + self.old_index = old_index + self.new_index = new_index + + class ActiveListListener(ABC): @abstractmethod def onListChange(self, source: "ActiveList", changes: list[ActiveListChange]): @@ -105,6 +111,8 @@ class ActiveListTransformationListener(ActiveListListener): elif isinstance(change, ActiveListIndexDeleted): del self.target[change.index] self.transformation.unmonitor(change.oldValue) + elif isinstance(change, ActiveListIndexMoved): + self.target.move(change.old_index, change.new_index) def _onMonitor(self, value): idx = self.source.index(value) @@ -152,6 +160,9 @@ class ActiveListFilterListener(ActiveListListener): del self.keyMap[idx] for i in range(idx, len(self.keyMap)): self.keyMap[i] -= 1 + elif isinstance(change, ActiveListIndexMoved): + idx = self.keyMap.index(change.old_index) + #TODO update keymap, fire change event def _onMonitor(self, value): idx = self.source.index(value) @@ -199,6 +210,12 @@ class ActiveListFlattenListener(ActiveListListener): change.oldValue.removeListener(self) idx = self.getOffsetForIndex(change.index) del self.target[idx, idx + len(change.oldValue)] + elif isinstance(change, ActiveListIndexMoved): + old_index = self.getOffsetForIndex(change.old_index) + new_index = self.getOffsetForIndex(change.new_index) + moved_list = self.source[change.new_index] + for (idx, element) in enumerate(moved_list): + self.target.move(old_index + idx, new_index + idx) else: if isinstance(change, ActiveListIndexAdded): self.target.insert(self.getOffsetFor(source) + change.index, change.newValue) @@ -206,6 +223,9 @@ class ActiveListFlattenListener(ActiveListListener): self.target[self.getOffsetFor(source) + change.index] = change.newValue elif isinstance(change, ActiveListIndexDeleted): del self.target[self.getOffsetFor(source) + change.index] + elif isinstance(change, ActiveListIndexMoved): + offset = self.getOffsetFor(source) + self.target.move(offset + change.old_index, offset + change.new_index) class ActiveList: @@ -241,6 +261,10 @@ class ActiveList: self.delegate.insert(index, value) self.__fireChanges([ActiveListIndexInserted(index, value)]) + def move(self, old_index, new_index): + self.delegate.insert(new_index, self.delegate.pop(old_index)) + self.__fireChanges([ActiveListIndexMoved(old_index, new_index)]) + def index(self, value): return self.delegate.index(value) diff --git a/test/owrx/active/list/test_active_list.py b/test/owrx/active/list/test_active_list.py index bcbf2787..e82db745 100644 --- a/test/owrx/active/list/test_active_list.py +++ b/test/owrx/active/list/test_active_list.py @@ -1,4 +1,4 @@ -from owrx.active.list import ActiveList, ActiveListIndexUpdated, ActiveListIndexAppended, ActiveListIndexDeleted, ActiveListIndexInserted +from owrx.active.list import ActiveList, ActiveListIndexUpdated, ActiveListIndexAppended, ActiveListIndexDeleted, ActiveListIndexInserted, ActiveListIndexMoved from unittest import TestCase from unittest.mock import Mock @@ -333,3 +333,106 @@ class ActiveListTest(TestCase): self.assertEqual(len(filteredList), 2) # update should propagate self.assertEqual(filteredList[0], 42) + + def testListMove(self): + list = ActiveList([1, 2, 3, 4, 5]) + list.move(1, 4) + self.assertEqual(len(list), 5) + self.assertEqual(list[1], 3) + self.assertEqual(list[4], 2) + + list = ActiveList([1, 2, 3, 4, 5]) + list.move(4, 1) + self.assertEqual(len(list), 5) + self.assertEqual(list[4], 4) + self.assertEqual(list[1], 5) + + def testListMoveNotification(self): + list = ActiveList([1, 2, 3, 4, 5]) + listenerMock = Mock() + list.addListener(listenerMock) + list.move(1, 4) + listenerMock.onListChange.assert_called_once() + source, changes = listenerMock.onListChange.call_args.args + self.assertIs(source, list) + self.assertEqual(len(changes), 1) + self.assertIsInstance(changes[0], ActiveListIndexMoved) + self.assertEqual(changes[0].old_index, 1) + self.assertEqual(changes[0].new_index, 4) + + def testActiveTransformationMove(self): + list = ActiveList([1, 2, 3, 4, 5]) + transformedList = list.map(lambda x: x + 10) + list.move(1, 4) + self.assertEqual(len(transformedList), 5) + self.assertEqual(transformedList[1], 13) + self.assertEqual(transformedList[4], 12) + + def testActiveTransformationMoveNotification(self): + list = ActiveList([1, 2, 3, 4, 5]) + transformedList = list.map(lambda x: x + 10) + listenerMock = Mock() + transformedList.addListener(listenerMock) + list.move(1, 4) + listenerMock.onListChange.assert_called_once() + source, changes = listenerMock.onListChange.call_args.args + self.assertIs(source, transformedList) + self.assertEqual(len(changes), 1) + self.assertIsInstance(changes[0], ActiveListIndexMoved) + self.assertEqual(changes[0].old_index, 1) + self.assertEqual(changes[0].new_index, 4) + + #def testActiveFilterMove(self): + # list = ActiveList([1, 2, 3, 4, 5]) + # filteredList = list.filter(lambda x: x != 3) + # list.move(1, 4) + # self.assertEqual(len(filteredList), 4) + # self.assertEqual(filteredList[1], 4) + # self.assertEqual(filteredList[3], 2) + + def testActiveListFlattenMove(self): + firstMember = ActiveList([1, 2, 3, 4, 5]) + list = ActiveList([firstMember, ActiveList([6, 7, 8, 9, 10])]) + flattenedList = list.flatten() + firstMember.move(1, 4) + self.assertEqual(len(flattenedList), 10) + self.assertEqual(flattenedList[1], 3) + self.assertEqual(flattenedList[4], 2) + + def testActiveListFlattenMoveNotification(self): + firstMember = ActiveList([1, 2, 3, 4, 5]) + secondMember = ActiveList([6, 7, 8, 9, 10]) + list = ActiveList([firstMember, secondMember]) + flattenedList = list.flatten() + listenerMock = Mock() + flattenedList.addListener(listenerMock) + secondMember.move(1, 4) + listenerMock.onListChange.assert_called_once() + source, changes = listenerMock.onListChange.call_args.args + self.assertIs(source, flattenedList) + self.assertEqual(len(changes), 1) + self.assertIsInstance(changes[0], ActiveListIndexMoved) + self.assertEqual(changes[0].old_index, 6) + self.assertEqual(changes[0].new_index, 9) + + def testActiveListFlattenListMove(self): + list = ActiveList([ActiveList([1]), ActiveList([2]), ActiveList([3]), ActiveList([4]), ActiveList([5])]) + flattenedList = list.flatten() + list.move(1, 4) + self.assertEqual(len(flattenedList), 5) + self.assertEqual(flattenedList[1], 3) + self.assertEqual(flattenedList[4], 2) + + def testActiveListFlattenListMoveNotification(self): + list = ActiveList([ActiveList([1, 2, 3, 4, 5]), ActiveList([6, 7, 8, 9, 10])]) + flattenedList = list.flatten() + listenerMock = Mock() + flattenedList.addListener(listenerMock) + list.move(0, 1) + self.assertEqual(listenerMock.onListChange.call_count, 5) + for i in range(0, 5): + source, changes = listenerMock.onListChange.call_args_list[i].args + self.assertIs(source, flattenedList) + self.assertEqual(len(changes), 1) + self.assertEqual(changes[0].old_index, i) + self.assertEqual(changes[0].new_index, i + 5)