diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 4bba580b2..8b7e0afb4 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -107,6 +107,7 @@ class Settings(QtCore.QSettings): __default_settings__ = { 'advanced/add page break': False, 'advanced/alternate rows': not is_win(), + 'advanced/autoscrolling': {'dist': 1, 'pos': 0}, 'advanced/current media plugin': -1, 'advanced/data path': '', # 7 stands for now, 0 to 6 is Monday to Sunday. @@ -119,7 +120,6 @@ class Settings(QtCore.QSettings): 'advanced/double click live': False, 'advanced/enable exit confirmation': True, 'advanced/expand service item': False, - 'advanced/slide max height': 0, 'advanced/hide mouse': True, 'advanced/is portable': False, 'advanced/max recent files': 20, @@ -129,6 +129,7 @@ class Settings(QtCore.QSettings): 'advanced/recent file count': 4, 'advanced/save current plugin': False, 'advanced/slide limits': SlideLimits.End, + 'advanced/slide max height': 0, 'advanced/single click preview': False, 'advanced/single click service preview': False, 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT, diff --git a/openlp/core/ui/advancedtab.py b/openlp/core/ui/advancedtab.py index cf17dac4a..97e2d3617 100644 --- a/openlp/core/ui/advancedtab.py +++ b/openlp/core/ui/advancedtab.py @@ -47,6 +47,10 @@ class AdvancedTab(SettingsTab): """ self.data_exists = False self.icon_path = ':/system/system_settings.png' + self.autoscroll_map = [None, {'dist': -1, 'pos': 0}, {'dist': -1, 'pos': 1}, {'dist': -1, 'pos': 2}, + {'dist': 0, 'pos': 0}, {'dist': 0, 'pos': 1}, {'dist': 0, 'pos': 2}, + {'dist': 0, 'pos': 3}, {'dist': 1, 'pos': 0}, {'dist': 1, 'pos': 1}, + {'dist': 1, 'pos': 2}, {'dist': 1, 'pos': 3}] advanced_translated = translate('OpenLP.AdvancedTab', 'Advanced') super(AdvancedTab, self).__init__(parent, 'Advanced', advanced_translated) @@ -88,6 +92,13 @@ class AdvancedTab(SettingsTab): self.slide_max_height_spin_box.setRange(0, 1000) self.slide_max_height_spin_box.setSingleStep(20) self.ui_layout.addRow(self.slide_max_height_label, self.slide_max_height_spin_box) + self.autoscroll_label = QtWidgets.QLabel(self.ui_group_box) + self.autoscroll_label.setObjectName('autoscroll_label') + self.autoscroll_combo_box = QtWidgets.QComboBox(self.ui_group_box) + self.autoscroll_combo_box.addItems(['', '', '', '', '', '', '', '', '', '', '', '']) + self.autoscroll_combo_box.setObjectName('autoscroll_combo_box') + self.ui_layout.addRow(self.autoscroll_label) + self.ui_layout.addRow(self.autoscroll_combo_box) self.search_as_type_check_box = QtWidgets.QCheckBox(self.ui_group_box) self.search_as_type_check_box.setObjectName('SearchAsType_check_box') self.ui_layout.addRow(self.search_as_type_check_box) @@ -255,6 +266,31 @@ class AdvancedTab(SettingsTab): self.slide_max_height_label.setText(translate('OpenLP.AdvancedTab', 'Max height for non-text slides\nin slide controller:')) self.slide_max_height_spin_box.setSpecialValueText(translate('OpenLP.AdvancedTab', 'Disabled')) + self.autoscroll_label.setText(translate('OpenLP.AdvancedTab', + 'When changing slides:')) + self.autoscroll_combo_box.setItemText(0, translate('OpenLP.AdvancedTab', 'Do not auto-scroll')) + self.autoscroll_combo_box.setItemText(1, translate('OpenLP.AdvancedTab', + 'Auto-scroll the previous slide into view')) + self.autoscroll_combo_box.setItemText(2, translate('OpenLP.AdvancedTab', + 'Auto-scroll the previous slide to top')) + self.autoscroll_combo_box.setItemText(3, translate('OpenLP.AdvancedTab', + 'Auto-scroll the previous slide to middle')) + self.autoscroll_combo_box.setItemText(4, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide into view')) + self.autoscroll_combo_box.setItemText(5, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide to top')) + self.autoscroll_combo_box.setItemText(6, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide to middle')) + self.autoscroll_combo_box.setItemText(7, translate('OpenLP.AdvancedTab', + 'Auto-scroll the current slide to bottom')) + self.autoscroll_combo_box.setItemText(8, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide into view')) + self.autoscroll_combo_box.setItemText(9, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide to top')) + self.autoscroll_combo_box.setItemText(10, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide to middle')) + self.autoscroll_combo_box.setItemText(11, translate('OpenLP.AdvancedTab', + 'Auto-scroll the next slide to bottom')) self.enable_auto_close_check_box.setText(translate('OpenLP.AdvancedTab', 'Enable application exit confirmation')) self.service_name_group_box.setTitle(translate('OpenLP.AdvancedTab', 'Default Service Name')) @@ -320,6 +356,10 @@ class AdvancedTab(SettingsTab): self.single_click_service_preview_check_box.setChecked(settings.value('single click service preview')) self.expand_service_item_check_box.setChecked(settings.value('expand service item')) self.slide_max_height_spin_box.setValue(settings.value('slide max height')) + autoscroll_value = settings.value('autoscrolling') + for i in range(0, len(self.autoscroll_map)): + if self.autoscroll_map[i] == autoscroll_value: + self.autoscroll_combo_box.setCurrentIndex(i) self.enable_auto_close_check_box.setChecked(settings.value('enable exit confirmation')) self.hide_mouse_check_box.setChecked(settings.value('hide mouse')) self.service_name_day.setCurrentIndex(settings.value('default service day')) @@ -400,6 +440,7 @@ class AdvancedTab(SettingsTab): settings.setValue('single click service preview', self.single_click_service_preview_check_box.isChecked()) settings.setValue('expand service item', self.expand_service_item_check_box.isChecked()) settings.setValue('slide max height', self.slide_max_height_spin_box.value()) + settings.setValue('autoscrolling', self.autoscroll_map[self.autoscroll_combo_box.currentIndex()]) settings.setValue('enable exit confirmation', self.enable_auto_close_check_box.isChecked()) settings.setValue('hide mouse', self.hide_mouse_check_box.isChecked()) settings.setValue('alternate rows', self.alternate_rows_check_box.isChecked()) diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 68c983d42..88aef818a 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -87,7 +87,7 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): height = self.viewport().width() // self.screen_ratio max_img_row_height = Settings().value('advanced/slide max height') # Adjust for row height cap if in use. - if max_img_row_height > 0 and height > max_img_row_height: + if isinstance(max_img_row_height, int) and max_img_row_height > 0 and height > max_img_row_height: height = max_img_row_height # Apply new height to slides for frame_number in range(len(self.service_item.get_frames())): @@ -98,7 +98,8 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): Will scale non-image slides. """ # Only for non-text slides when row height cap in use - if self.service_item.is_text() or Settings().value('advanced/slide max height') <= 0: + max_img_row_height = Settings().value('advanced/slide max height') + if self.service_item.is_text() or not isinstance(max_img_row_height, int) or max_img_row_height <= 0: return # Get and validate label widget containing slide & adjust max width try: @@ -160,9 +161,9 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): pixmap.setDevicePixelRatio(label.devicePixelRatio()) label.setPixmap(pixmap) slide_height = width // self.screen_ratio - # Setup row height cap if in use. + # Setup and validate row height cap if in use. max_img_row_height = Settings().value('advanced/slide max height') - if max_img_row_height > 0: + if isinstance(max_img_row_height, int) and max_img_row_height > 0: if slide_height > max_img_row_height: slide_height = max_img_row_height label.setMaximumWidth(max_img_row_height * self.screen_ratio) @@ -194,11 +195,22 @@ class ListPreviewWidget(QtWidgets.QTableWidget, RegistryProperties): """ Switches to the given row. """ - if slide >= self.slide_count(): - slide = self.slide_count() - 1 - # Scroll to next item if possible. - if slide + 1 < self.slide_count(): - self.scrollToItem(self.item(slide + 1, 0)) + # Retrieve setting + autoscrolling = Settings().value('advanced/autoscrolling') + # Check if auto-scroll disabled (None) and validate value as dict containing 'dist' and 'pos' + # 'dist' represents the slide to scroll to relative to the new slide (-1 = previous, 0 = current, 1 = next) + # 'pos' represents the vert position of of the slide (0 = in view, 1 = top, 2 = middle, 3 = bottom) + if not (isinstance(autoscrolling, dict) and 'dist' in autoscrolling and 'pos' in autoscrolling and + isinstance(autoscrolling['dist'], int) and isinstance(autoscrolling['pos'], int)): + return + # prevent scrolling past list bounds + scroll_to_slide = slide + autoscrolling['dist'] + if scroll_to_slide < 0: + scroll_to_slide = 0 + if scroll_to_slide >= self.slide_count(): + scroll_to_slide = self.slide_count() - 1 + # Scroll to item if possible. + self.scrollToItem(self.item(scroll_to_slide, 0), autoscrolling['pos']) self.selectRow(slide) def current_slide_number(self): diff --git a/tests/functional/openlp_core_ui/test_listpreviewwidget.py b/tests/functional/openlp_core_ui/test_listpreviewwidget.py index a222189e6..e9f99a8eb 100644 --- a/tests/functional/openlp_core_ui/test_listpreviewwidget.py +++ b/tests/functional/openlp_core_ui/test_listpreviewwidget.py @@ -130,12 +130,14 @@ class TestListPreviewWidget(TestCase): # WHEN: __recalculate_layout() is called (via resizeEvent) list_preview_widget.resizeEvent(None) + self.mocked_Settings_obj.value.return_value = None + list_preview_widget.resizeEvent(None) # THEN: resizeRowsToContents() should not be called, while setRowHeight() should be called # twice for each slide. self.assertEquals(mocked_resizeRowsToContents.call_count, 0, 'Should not be called') - self.assertEquals(mocked_setRowHeight.call_count, 4, 'Should be called twice for each slide') - calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400)] + self.assertEquals(mocked_setRowHeight.call_count, 6, 'Should be called 3 times for each slide') + calls = [call(0, 200), call(1, 200), call(0, 400), call(1, 400), call(0, 400), call(1, 400)] mocked_setRowHeight.assert_has_calls(calls) @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.resizeRowsToContents') @@ -236,6 +238,8 @@ class TestListPreviewWidget(TestCase): # WHEN: row_resized() is called list_preview_widget.row_resized(0, 100, 150) + self.mocked_Settings_obj.value.return_value = None + list_preview_widget.row_resized(0, 100, 150) # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should not be called self.assertEquals(mocked_cellWidget_child.setMaximumWidth.call_count, 0, 'Should not be called') @@ -273,3 +277,101 @@ class TestListPreviewWidget(TestCase): # THEN: self.cellWidget(row, 0).children()[1].setMaximumWidth() should be called mocked_cellWidget_child.setMaximumWidth.assert_called_once_with(150) + + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.slide_count') + def autoscroll_test_setting_invalid(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): + """ + Test if 'advanced/autoscrolling' setting None or invalid, that no autoscrolling occurs on change_slide(). + """ + # GIVEN: A setting for autoscrolling and a ListPreviewWidget. + # Mock Settings().value('advanced/autoscrolling') + self.mocked_Settings_obj.value.return_value = None + # Mocked returns + mocked_slide_count.return_value = 1 + mocked_item.return_value = None + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + + # WHEN: change_slide() is called + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = 1 + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'fail': 1} + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'fail': 1} + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 'fail', 'pos': 1} + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'pos': 'fail'} + list_preview_widget.change_slide(0) + + # THEN: no further functions should be called + self.assertEquals(mocked_slide_count.call_count, 0, 'Should not be called') + self.assertEquals(mocked_scrollToItem.call_count, 0, 'Should not be called') + self.assertEquals(mocked_selectRow.call_count, 0, 'Should not be called') + self.assertEquals(mocked_item.call_count, 0, 'Should not be called') + + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.slide_count') + def autoscroll_test_dist_bounds(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): + """ + Test if 'advanced/autoscrolling' setting asks to scroll beyond list bounds, that it does not beyond. + """ + # GIVEN: A setting for autoscrolling and a ListPreviewWidget. + # Mock Settings().value('advanced/autoscrolling') + self.mocked_Settings_obj.value.return_value = {'dist': -1, 'pos': 1} + # Mocked returns + mocked_slide_count.return_value = 1 + mocked_item.return_value = None + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + + # WHEN: change_slide() is called + list_preview_widget.change_slide(0) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'pos': 1} + list_preview_widget.change_slide(0) + + # THEN: no further functions should be called + self.assertEquals(mocked_slide_count.call_count, 3, 'Should be called') + self.assertEquals(mocked_scrollToItem.call_count, 2, 'Should be called') + self.assertEquals(mocked_selectRow.call_count, 2, 'Should be called') + self.assertEquals(mocked_item.call_count, 2, 'Should be called') + calls = [call(0, 0), call(0, 0)] + mocked_item.assert_has_calls(calls) + + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.selectRow') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.scrollToItem') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.item') + @patch(u'openlp.core.ui.listpreviewwidget.ListPreviewWidget.slide_count') + def autoscroll_test_normal(self, mocked_slide_count, mocked_item, mocked_scrollToItem, mocked_selectRow): + """ + Test if 'advanced/autoscrolling' setting valid, autoscrolling called as expected. + """ + # GIVEN: A setting for autoscrolling and a ListPreviewWidget. + # Mock Settings().value('advanced/autoscrolling') + self.mocked_Settings_obj.value.return_value = {'dist': -1, 'pos': 1} + # Mocked returns + mocked_slide_count.return_value = 3 + mocked_item.return_value = None + # init ListPreviewWidget and load service item + list_preview_widget = ListPreviewWidget(None, 1) + + # WHEN: change_slide() is called + list_preview_widget.change_slide(1) + self.mocked_Settings_obj.value.return_value = {'dist': 0, 'pos': 1} + list_preview_widget.change_slide(1) + self.mocked_Settings_obj.value.return_value = {'dist': 1, 'pos': 1} + list_preview_widget.change_slide(1) + + # THEN: no further functions should be called + self.assertEquals(mocked_slide_count.call_count, 3, 'Should be called') + self.assertEquals(mocked_scrollToItem.call_count, 3, 'Should be called') + self.assertEquals(mocked_selectRow.call_count, 3, 'Should be called') + self.assertEquals(mocked_item.call_count, 3, 'Should be called') + calls = [call(0, 0), call(1, 0), call(2, 0)] + mocked_item.assert_has_calls(calls)