forked from openlp/openlp
Merge trunk
This commit is contained in:
commit
9f8433a18e
@ -26,7 +26,6 @@ import os
|
||||
import logging
|
||||
import json
|
||||
|
||||
from xml.dom.minidom import Document
|
||||
from lxml import etree, objectify
|
||||
from openlp.core.common import AppLocation, de_hump
|
||||
|
||||
@ -150,7 +149,7 @@ INTEGER_LIST = ['size', 'line_adjustment', 'x', 'height', 'y', 'width', 'shadow_
|
||||
'horizontal_align', 'vertical_align', 'wrap_style']
|
||||
|
||||
|
||||
class ThemeXML(object):
|
||||
class Theme(object):
|
||||
"""
|
||||
A class to encapsulate the Theme XML.
|
||||
"""
|
||||
@ -195,184 +194,6 @@ class ThemeXML(object):
|
||||
self.background_filename = self.background_filename.strip()
|
||||
self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
|
||||
|
||||
def _new_document(self, name):
|
||||
"""
|
||||
Create a new theme XML document.
|
||||
"""
|
||||
self.theme_xml = Document()
|
||||
self.theme = self.theme_xml.createElement('theme')
|
||||
self.theme_xml.appendChild(self.theme)
|
||||
self.theme.setAttribute('version', '2.0')
|
||||
self.name = self.theme_xml.createElement('name')
|
||||
text_node = self.theme_xml.createTextNode(name)
|
||||
self.name.appendChild(text_node)
|
||||
self.theme.appendChild(self.name)
|
||||
|
||||
def add_background_transparent(self):
|
||||
"""
|
||||
Add a transparent background.
|
||||
"""
|
||||
background = self.theme_xml.createElement('background')
|
||||
background.setAttribute('type', 'transparent')
|
||||
self.theme.appendChild(background)
|
||||
|
||||
def add_background_solid(self, bkcolor):
|
||||
"""
|
||||
Add a Solid background.
|
||||
|
||||
:param bkcolor: The color of the background.
|
||||
"""
|
||||
background = self.theme_xml.createElement('background')
|
||||
background.setAttribute('type', 'solid')
|
||||
self.theme.appendChild(background)
|
||||
self.child_element(background, 'color', str(bkcolor))
|
||||
|
||||
def add_background_gradient(self, startcolor, endcolor, direction):
|
||||
"""
|
||||
Add a gradient background.
|
||||
|
||||
:param startcolor: The gradient's starting colour.
|
||||
:param endcolor: The gradient's ending colour.
|
||||
:param direction: The direction of the gradient.
|
||||
"""
|
||||
background = self.theme_xml.createElement('background')
|
||||
background.setAttribute('type', 'gradient')
|
||||
self.theme.appendChild(background)
|
||||
# Create startColor element
|
||||
self.child_element(background, 'startColor', str(startcolor))
|
||||
# Create endColor element
|
||||
self.child_element(background, 'endColor', str(endcolor))
|
||||
# Create direction element
|
||||
self.child_element(background, 'direction', str(direction))
|
||||
|
||||
def add_background_image(self, filename, border_color):
|
||||
"""
|
||||
Add a image background.
|
||||
|
||||
:param filename: The file name of the image.
|
||||
:param border_color:
|
||||
"""
|
||||
background = self.theme_xml.createElement('background')
|
||||
background.setAttribute('type', 'image')
|
||||
self.theme.appendChild(background)
|
||||
# Create Filename element
|
||||
self.child_element(background, 'filename', filename)
|
||||
# Create endColor element
|
||||
self.child_element(background, 'borderColor', str(border_color))
|
||||
|
||||
def add_background_video(self, filename, border_color):
|
||||
"""
|
||||
Add a video background.
|
||||
|
||||
:param filename: The file name of the video.
|
||||
:param border_color:
|
||||
"""
|
||||
background = self.theme_xml.createElement('background')
|
||||
background.setAttribute('type', 'video')
|
||||
self.theme.appendChild(background)
|
||||
# Create Filename element
|
||||
self.child_element(background, 'filename', filename)
|
||||
# Create endColor element
|
||||
self.child_element(background, 'borderColor', str(border_color))
|
||||
|
||||
def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
|
||||
line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
|
||||
outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
|
||||
"""
|
||||
Add a Font.
|
||||
|
||||
:param name: The name of the font.
|
||||
:param color: The colour of the font.
|
||||
:param size: The size of the font.
|
||||
:param override: Whether or not to override the default positioning of the theme.
|
||||
:param fonttype: The type of font, ``main`` or ``footer``. Defaults to ``main``.
|
||||
:param bold:
|
||||
:param italics: The weight of then font Defaults to 50 Normal
|
||||
:param line_adjustment: Does the font render to italics Defaults to 0 Normal
|
||||
:param xpos: The X position of the text block.
|
||||
:param ypos: The Y position of the text block.
|
||||
:param width: The width of the text block.
|
||||
:param height: The height of the text block.
|
||||
:param outline: Whether or not to show an outline.
|
||||
:param outline_color: The colour of the outline.
|
||||
:param outline_pixel: How big the Shadow is
|
||||
:param shadow: Whether or not to show a shadow.
|
||||
:param shadow_color: The colour of the shadow.
|
||||
:param shadow_pixel: How big the Shadow is
|
||||
"""
|
||||
background = self.theme_xml.createElement('font')
|
||||
background.setAttribute('type', fonttype)
|
||||
self.theme.appendChild(background)
|
||||
# Create Font name element
|
||||
self.child_element(background, 'name', name)
|
||||
# Create Font color element
|
||||
self.child_element(background, 'color', str(color))
|
||||
# Create Proportion name element
|
||||
self.child_element(background, 'size', str(size))
|
||||
# Create weight name element
|
||||
self.child_element(background, 'bold', str(bold))
|
||||
# Create italics name element
|
||||
self.child_element(background, 'italics', str(italics))
|
||||
# Create indentation name element
|
||||
self.child_element(background, 'line_adjustment', str(line_adjustment))
|
||||
# Create Location element
|
||||
element = self.theme_xml.createElement('location')
|
||||
element.setAttribute('override', str(override))
|
||||
element.setAttribute('x', str(xpos))
|
||||
element.setAttribute('y', str(ypos))
|
||||
element.setAttribute('width', str(width))
|
||||
element.setAttribute('height', str(height))
|
||||
background.appendChild(element)
|
||||
# Shadow
|
||||
element = self.theme_xml.createElement('shadow')
|
||||
element.setAttribute('shadowColor', str(shadow_color))
|
||||
element.setAttribute('shadowSize', str(shadow_pixel))
|
||||
value = self.theme_xml.createTextNode(str(shadow))
|
||||
element.appendChild(value)
|
||||
background.appendChild(element)
|
||||
# Outline
|
||||
element = self.theme_xml.createElement('outline')
|
||||
element.setAttribute('outlineColor', str(outline_color))
|
||||
element.setAttribute('outlineSize', str(outline_pixel))
|
||||
value = self.theme_xml.createTextNode(str(outline))
|
||||
element.appendChild(value)
|
||||
background.appendChild(element)
|
||||
|
||||
def add_display(self, horizontal, vertical, transition):
|
||||
"""
|
||||
Add a Display options.
|
||||
|
||||
:param horizontal: The horizontal alignment of the text.
|
||||
:param vertical: The vertical alignment of the text.
|
||||
:param transition: Whether the slide transition is active.
|
||||
"""
|
||||
background = self.theme_xml.createElement('display')
|
||||
self.theme.appendChild(background)
|
||||
# Horizontal alignment
|
||||
element = self.theme_xml.createElement('horizontalAlign')
|
||||
value = self.theme_xml.createTextNode(str(horizontal))
|
||||
element.appendChild(value)
|
||||
background.appendChild(element)
|
||||
# Vertical alignment
|
||||
element = self.theme_xml.createElement('verticalAlign')
|
||||
value = self.theme_xml.createTextNode(str(vertical))
|
||||
element.appendChild(value)
|
||||
background.appendChild(element)
|
||||
# Slide Transition
|
||||
element = self.theme_xml.createElement('slideTransition')
|
||||
value = self.theme_xml.createTextNode(str(transition))
|
||||
element.appendChild(value)
|
||||
background.appendChild(element)
|
||||
|
||||
def child_element(self, element, tag, value):
|
||||
"""
|
||||
Generic child element creator.
|
||||
"""
|
||||
child = self.theme_xml.createElement(tag)
|
||||
child.appendChild(self.theme_xml.createTextNode(value))
|
||||
element.appendChild(child)
|
||||
return child
|
||||
|
||||
def set_default_header_footer(self):
|
||||
"""
|
||||
Set the header and footer size into the current primary screen.
|
||||
@ -386,25 +207,24 @@ class ThemeXML(object):
|
||||
self.font_footer_y = current_screen['size'].height() * 9 / 10
|
||||
self.font_footer_height = current_screen['size'].height() / 10
|
||||
|
||||
def dump_xml(self):
|
||||
def load_theme(self, theme):
|
||||
"""
|
||||
Dump the XML to file used for debugging
|
||||
"""
|
||||
return self.theme_xml.toprettyxml(indent=' ')
|
||||
Convert the JSON file and expand it.
|
||||
|
||||
def extract_xml(self):
|
||||
:param theme: the theme string
|
||||
"""
|
||||
Print out the XML string.
|
||||
"""
|
||||
self._build_xml_from_attrs()
|
||||
return self.theme_xml.toxml('utf-8').decode('utf-8')
|
||||
jsn = json.loads(theme)
|
||||
self.expand_json(jsn)
|
||||
|
||||
def extract_formatted_xml(self):
|
||||
def export_theme(self):
|
||||
"""
|
||||
Pull out the XML string formatted for human consumption
|
||||
Loop through the fields and build a dictionary of them
|
||||
|
||||
"""
|
||||
self._build_xml_from_attrs()
|
||||
return self.theme_xml.toprettyxml(indent=' ', newl='\n', encoding='utf-8')
|
||||
theme_data = {}
|
||||
for attr, value in self.__dict__.items():
|
||||
theme_data["{attr}".format(attr=attr)] = value
|
||||
return json.dumps(theme_data)
|
||||
|
||||
def parse(self, xml):
|
||||
"""
|
||||
@ -461,7 +281,8 @@ class ThemeXML(object):
|
||||
if element.tag == 'name':
|
||||
self._create_attr('theme', element.tag, element.text)
|
||||
|
||||
def _translate_tags(self, master, element, value):
|
||||
@staticmethod
|
||||
def _translate_tags(master, element, value):
|
||||
"""
|
||||
Clean up XML removing and redefining tags
|
||||
"""
|
||||
@ -514,71 +335,5 @@ class ThemeXML(object):
|
||||
theme_strings = []
|
||||
for key in dir(self):
|
||||
if key[0:1] != '_':
|
||||
# TODO: Due to bound methods returned, I don't know how to write a proper test
|
||||
theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
|
||||
return '\n'.join(theme_strings)
|
||||
|
||||
def _build_xml_from_attrs(self):
|
||||
"""
|
||||
Build the XML from the varables in the object
|
||||
"""
|
||||
self._new_document(self.theme_name)
|
||||
if self.background_type == BackgroundType.to_string(BackgroundType.Solid):
|
||||
self.add_background_solid(self.background_color)
|
||||
elif self.background_type == BackgroundType.to_string(BackgroundType.Gradient):
|
||||
self.add_background_gradient(
|
||||
self.background_start_color,
|
||||
self.background_end_color,
|
||||
self.background_direction
|
||||
)
|
||||
elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
|
||||
filename = os.path.split(self.background_filename)[1]
|
||||
self.add_background_image(filename, self.background_border_color)
|
||||
elif self.background_type == BackgroundType.to_string(BackgroundType.Video):
|
||||
filename = os.path.split(self.background_filename)[1]
|
||||
self.add_background_video(filename, self.background_border_color)
|
||||
elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
|
||||
self.add_background_transparent()
|
||||
self.add_font(
|
||||
self.font_main_name,
|
||||
self.font_main_color,
|
||||
self.font_main_size,
|
||||
self.font_main_override, 'main',
|
||||
self.font_main_bold,
|
||||
self.font_main_italics,
|
||||
self.font_main_line_adjustment,
|
||||
self.font_main_x,
|
||||
self.font_main_y,
|
||||
self.font_main_width,
|
||||
self.font_main_height,
|
||||
self.font_main_outline,
|
||||
self.font_main_outline_color,
|
||||
self.font_main_outline_size,
|
||||
self.font_main_shadow,
|
||||
self.font_main_shadow_color,
|
||||
self.font_main_shadow_size
|
||||
)
|
||||
self.add_font(
|
||||
self.font_footer_name,
|
||||
self.font_footer_color,
|
||||
self.font_footer_size,
|
||||
self.font_footer_override, 'footer',
|
||||
self.font_footer_bold,
|
||||
self.font_footer_italics,
|
||||
0, # line adjustment
|
||||
self.font_footer_x,
|
||||
self.font_footer_y,
|
||||
self.font_footer_width,
|
||||
self.font_footer_height,
|
||||
self.font_footer_outline,
|
||||
self.font_footer_outline_color,
|
||||
self.font_footer_outline_size,
|
||||
self.font_footer_shadow,
|
||||
self.font_footer_shadow_color,
|
||||
self.font_footer_shadow_size
|
||||
)
|
||||
self.add_display(
|
||||
self.display_horizontal_align,
|
||||
self.display_vertical_align,
|
||||
self.display_slide_transition
|
||||
)
|
||||
|
14
openlp/core/ui/lib/listwidgetwithdnd.py
Normal file → Executable file
14
openlp/core/ui/lib/listwidgetwithdnd.py
Normal file → Executable file
@ -44,7 +44,6 @@ class ListWidgetWithDnD(QtWidgets.QListWidget):
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.locked = False
|
||||
|
||||
def activateDnD(self):
|
||||
"""
|
||||
@ -54,15 +53,13 @@ class ListWidgetWithDnD(QtWidgets.QListWidget):
|
||||
self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
|
||||
Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
|
||||
|
||||
def clear(self, search_while_typing=False, override_lock=False):
|
||||
def clear(self, search_while_typing=False):
|
||||
"""
|
||||
Re-implement clear, so that we can customise feedback when using 'Search as you type'
|
||||
|
||||
:param search_while_typing: True if we want to display the customised message
|
||||
:return: None
|
||||
"""
|
||||
if self.locked and not override_lock:
|
||||
return
|
||||
if search_while_typing:
|
||||
self.no_results_text = UiStrings().ShortResults
|
||||
else:
|
||||
@ -128,6 +125,15 @@ class ListWidgetWithDnD(QtWidgets.QListWidget):
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def allItems(self):
|
||||
"""
|
||||
An generator to list all the items in the widget
|
||||
|
||||
:return: a generator
|
||||
"""
|
||||
for row in range(self.count()):
|
||||
yield self.item(row)
|
||||
|
||||
def paintEvent(self, event):
|
||||
"""
|
||||
Re-implement paintEvent so that we can add 'No Results' text when the listWidget is empty.
|
||||
|
@ -698,7 +698,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ServiceMa
|
||||
translate('OpenLP.ServiceManager',
|
||||
'OpenLP Service Files (*.osz);; OpenLP Service Files - lite (*.oszl)'))
|
||||
else:
|
||||
file_name, filter_uesd = QtWidgets.QFileDialog.getSaveFileName(
|
||||
file_name, filter_used = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self.main_window, UiStrings().SaveService, path,
|
||||
translate('OpenLP.ServiceManager', 'OpenLP Service Files (*.osz);;'))
|
||||
if not file_name:
|
||||
|
@ -22,6 +22,7 @@
|
||||
"""
|
||||
The Theme Manager manages adding, deleteing and modifying of themes.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
import shutil
|
||||
@ -33,7 +34,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, Settin
|
||||
check_directory_exists, UiStrings, translate, is_win, get_filesystem_encoding, delete_file
|
||||
from openlp.core.lib import FileDialog, ImageSource, ValidationError, get_text_file_string, build_icon, \
|
||||
check_item_selected, create_thumb, validate_thumb
|
||||
from openlp.core.lib.theme import ThemeXML, BackgroundType
|
||||
from openlp.core.lib.theme import Theme, BackgroundType
|
||||
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
|
||||
from openlp.core.ui import FileRenameForm, ThemeForm
|
||||
from openlp.core.ui.lib import OpenLPToolbar
|
||||
@ -245,7 +246,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
their customisations.
|
||||
:param field:
|
||||
"""
|
||||
theme = ThemeXML()
|
||||
theme = Theme()
|
||||
theme.set_default_header_footer()
|
||||
self.theme_form.theme = theme
|
||||
self.theme_form.exec()
|
||||
@ -378,11 +379,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
critical_error_message_box(message=translate('OpenLP.ThemeManager', 'You have not selected a theme.'))
|
||||
return
|
||||
theme = item.data(QtCore.Qt.UserRole)
|
||||
path = QtWidgets.QFileDialog.getExistingDirectory(self,
|
||||
translate('OpenLP.ThemeManager',
|
||||
'Save Theme - ({name})').format(name=theme),
|
||||
Settings().value(self.settings_section +
|
||||
'/last directory export'))
|
||||
path, filter_used = \
|
||||
QtWidgets.QFileDialog.getSaveFileName(self.main_window,
|
||||
translate('OpenLP.ThemeManager', 'Save Theme - ({name})').
|
||||
format(name=theme),
|
||||
Settings().value(self.settings_section + '/last directory export'),
|
||||
translate('OpenLP.ThemeManager', 'OpenLP Themes (*.otz)'))
|
||||
self.application.set_busy_cursor()
|
||||
if path:
|
||||
Settings().setValue(self.settings_section + '/last directory export', path)
|
||||
@ -393,13 +395,12 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
'Your theme has been successfully exported.'))
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
def _export_theme(self, path, theme):
|
||||
def _export_theme(self, theme_path, theme):
|
||||
"""
|
||||
Create the zipfile with the theme contents.
|
||||
:param path: Location where the zip file will be placed
|
||||
:param theme_path: Location where the zip file will be placed
|
||||
:param theme: The name of the theme to be exported
|
||||
"""
|
||||
theme_path = os.path.join(path, theme + '.otz')
|
||||
theme_zip = None
|
||||
try:
|
||||
theme_zip = zipfile.ZipFile(theme_path, 'w')
|
||||
@ -452,7 +453,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
files = AppLocation.get_files(self.settings_section, '.png')
|
||||
# No themes have been found so create one
|
||||
if not files:
|
||||
theme = ThemeXML()
|
||||
theme = Theme()
|
||||
theme.theme_name = UiStrings().Default
|
||||
self._write_theme(theme, None, None)
|
||||
Settings().setValue(self.settings_section + '/global theme', theme.theme_name)
|
||||
@ -505,19 +506,27 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
|
||||
def get_theme_data(self, theme_name):
|
||||
"""
|
||||
Returns a theme object from an XML file
|
||||
Returns a theme object from an XML or JSON file
|
||||
|
||||
:param theme_name: Name of the theme to load from file
|
||||
:return: The theme object.
|
||||
"""
|
||||
self.log_debug('get theme data for theme {name}'.format(name=theme_name))
|
||||
xml_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
|
||||
xml = get_text_file_string(xml_file)
|
||||
if not xml:
|
||||
theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.json')
|
||||
theme_data = get_text_file_string(theme_file)
|
||||
jsn = True
|
||||
if not theme_data:
|
||||
theme_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
|
||||
theme_data = get_text_file_string(theme_file)
|
||||
jsn = False
|
||||
if not theme_data:
|
||||
self.log_debug('No theme data - using default theme')
|
||||
return ThemeXML()
|
||||
return Theme()
|
||||
else:
|
||||
return self._create_theme_from_xml(xml, self.path)
|
||||
if jsn:
|
||||
return self._create_theme_from_json(theme_data, self.path)
|
||||
else:
|
||||
return self._create_theme_from_xml(theme_data, self.path)
|
||||
|
||||
def over_write_message_box(self, theme_name):
|
||||
"""
|
||||
@ -547,18 +556,28 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
out_file = None
|
||||
file_xml = None
|
||||
abort_import = True
|
||||
json_theme = False
|
||||
theme_name = ""
|
||||
try:
|
||||
theme_zip = zipfile.ZipFile(file_name)
|
||||
xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
|
||||
if len(xml_file) != 1:
|
||||
self.log_error('Theme contains "{val:d}" XML files'.format(val=len(xml_file)))
|
||||
raise ValidationError
|
||||
xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
|
||||
theme_version = xml_tree.get('version', default=None)
|
||||
if not theme_version or float(theme_version) < 2.0:
|
||||
self.log_error('Theme version is less than 2.0')
|
||||
raise ValidationError
|
||||
theme_name = xml_tree.find('name').text.strip()
|
||||
json_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.json']
|
||||
if len(json_file) != 1:
|
||||
# TODO: remove XML handling at some point but would need a auto conversion to run first.
|
||||
xml_file = [name for name in theme_zip.namelist() if os.path.splitext(name)[1].lower() == '.xml']
|
||||
if len(xml_file) != 1:
|
||||
self.log_error('Theme contains "{val:d}" theme files'.format(val=len(xml_file)))
|
||||
raise ValidationError
|
||||
xml_tree = ElementTree(element=XML(theme_zip.read(xml_file[0]))).getroot()
|
||||
theme_version = xml_tree.get('version', default=None)
|
||||
if not theme_version or float(theme_version) < 2.0:
|
||||
self.log_error('Theme version is less than 2.0')
|
||||
raise ValidationError
|
||||
theme_name = xml_tree.find('name').text.strip()
|
||||
else:
|
||||
new_theme = Theme()
|
||||
new_theme.load_theme(theme_zip.read(json_file[0]).decode("utf-8"))
|
||||
theme_name = new_theme.theme_name
|
||||
json_theme = True
|
||||
theme_folder = os.path.join(directory, theme_name)
|
||||
theme_exists = os.path.exists(theme_folder)
|
||||
if theme_exists and not self.over_write_message_box(theme_name):
|
||||
@ -574,7 +593,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
continue
|
||||
full_name = os.path.join(directory, out_name)
|
||||
check_directory_exists(os.path.dirname(full_name))
|
||||
if os.path.splitext(name)[1].lower() == '.xml':
|
||||
if os.path.splitext(name)[1].lower() == '.xml' or os.path.splitext(name)[1].lower() == '.json':
|
||||
file_xml = str(theme_zip.read(name), 'utf-8')
|
||||
out_file = open(full_name, 'w', encoding='utf-8')
|
||||
out_file.write(file_xml)
|
||||
@ -597,7 +616,10 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
if not abort_import:
|
||||
# As all files are closed, we can create the Theme.
|
||||
if file_xml:
|
||||
theme = self._create_theme_from_xml(file_xml, self.path)
|
||||
if json_theme:
|
||||
theme = self._create_theme_from_json(file_xml, self.path)
|
||||
else:
|
||||
theme = self._create_theme_from_xml(file_xml, self.path)
|
||||
self.generate_and_save_image(theme_name, theme)
|
||||
# Only show the error message, when IOError was not raised (in
|
||||
# this case the error message has already been shown).
|
||||
@ -646,16 +668,16 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
:param image_to: Where the Theme Image is to be saved to
|
||||
"""
|
||||
name = theme.theme_name
|
||||
theme_pretty_xml = theme.extract_formatted_xml()
|
||||
theme_pretty = theme.export_theme()
|
||||
theme_dir = os.path.join(self.path, name)
|
||||
check_directory_exists(theme_dir)
|
||||
theme_file = os.path.join(theme_dir, name + '.xml')
|
||||
theme_file = os.path.join(theme_dir, name + '.json')
|
||||
if self.old_background_image and image_to != self.old_background_image:
|
||||
delete_file(self.old_background_image)
|
||||
out_file = None
|
||||
try:
|
||||
out_file = open(theme_file, 'w', encoding='utf-8')
|
||||
out_file.write(theme_pretty_xml.decode('utf-8'))
|
||||
out_file.write(theme_pretty)
|
||||
except IOError:
|
||||
self.log_exception('Saving theme to file failed')
|
||||
finally:
|
||||
@ -717,7 +739,8 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
"""
|
||||
return os.path.join(self.path, theme + '.png')
|
||||
|
||||
def _create_theme_from_xml(self, theme_xml, image_path):
|
||||
@staticmethod
|
||||
def _create_theme_from_xml(theme_xml, image_path):
|
||||
"""
|
||||
Return a theme object using information parsed from XML
|
||||
|
||||
@ -725,11 +748,25 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtWidgets.QWidget, Ui_ThemeManage
|
||||
:param image_path: Where the theme image is stored
|
||||
:return: Theme data.
|
||||
"""
|
||||
theme = ThemeXML()
|
||||
theme = Theme()
|
||||
theme.parse(theme_xml)
|
||||
theme.extend_image_filename(image_path)
|
||||
return theme
|
||||
|
||||
@staticmethod
|
||||
def _create_theme_from_json(theme_json, image_path):
|
||||
"""
|
||||
Return a theme object using information parsed from JSON
|
||||
|
||||
:param theme_json: The Theme data object.
|
||||
:param image_path: Where the theme image is stored
|
||||
:return: Theme data.
|
||||
"""
|
||||
theme = Theme()
|
||||
theme.load_theme(theme_json)
|
||||
theme.extend_image_filename(image_path)
|
||||
return theme
|
||||
|
||||
def _validate_theme_action(self, select_text, confirm_title, confirm_text, test_plugin=True, confirm=True):
|
||||
"""
|
||||
Check to see if theme has been selected and the destructive action is allowed.
|
||||
|
@ -341,10 +341,10 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
|
||||
if not book_ref_id:
|
||||
book_ref_id = bible.get_book_ref_id_by_localised_name(book, language_selection)
|
||||
elif not bible.get_book_by_book_ref_id(book_ref_id):
|
||||
return False
|
||||
return []
|
||||
# We have not found the book so do not continue
|
||||
if not book_ref_id:
|
||||
return False
|
||||
return []
|
||||
ranges = match.group('ranges')
|
||||
range_list = get_reference_match('range_separator').split(ranges)
|
||||
ref_list = []
|
||||
@ -403,7 +403,7 @@ def parse_reference(reference, bible, language_selection, book_ref_id=False):
|
||||
return ref_list
|
||||
else:
|
||||
log.debug('Invalid reference: {text}'.format(text=reference))
|
||||
return None
|
||||
return []
|
||||
|
||||
|
||||
class SearchResults(object):
|
||||
|
@ -158,6 +158,7 @@ class BibleDB(Manager):
|
||||
self.get_name()
|
||||
if 'path' in kwargs:
|
||||
self.path = kwargs['path']
|
||||
self._is_web_bible = None
|
||||
|
||||
def get_name(self):
|
||||
"""
|
||||
@ -426,6 +427,18 @@ class BibleDB(Manager):
|
||||
return 0
|
||||
return count
|
||||
|
||||
@property
|
||||
def is_web_bible(self):
|
||||
"""
|
||||
A read only property indicating if the bible is a 'web bible'
|
||||
|
||||
:return: If the bible is a web bible.
|
||||
:rtype: bool
|
||||
"""
|
||||
if self._is_web_bible is None:
|
||||
self._is_web_bible = bool(self.get_object(BibleMeta, 'download_source'))
|
||||
return self._is_web_bible
|
||||
|
||||
def dump_bible(self):
|
||||
"""
|
||||
Utility debugging method to dump the contents of a bible.
|
||||
|
@ -142,8 +142,8 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
log.debug('Bible Name: "{name}"'.format(name=name))
|
||||
self.db_cache[name] = bible
|
||||
# Look to see if lazy load bible exists and get create getter.
|
||||
source = self.db_cache[name].get_object(BibleMeta, 'download_source')
|
||||
if source:
|
||||
if self.db_cache[name].is_web_bible:
|
||||
source = self.db_cache[name].get_object(BibleMeta, 'download_source')
|
||||
download_name = self.db_cache[name].get_object(BibleMeta, 'download_name').value
|
||||
meta_proxy = self.db_cache[name].get_object(BibleMeta, 'proxy_server')
|
||||
web_bible = HTTPBible(self.parent, path=self.path, file=filename, download_source=source.value,
|
||||
@ -278,7 +278,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
:param show_error:
|
||||
"""
|
||||
if not bible or not ref_list:
|
||||
return None
|
||||
return []
|
||||
return self.db_cache[bible].get_verses(ref_list, show_error)
|
||||
|
||||
def get_language_selection(self, bible):
|
||||
@ -305,11 +305,17 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
"""
|
||||
Does a verse search for the given bible and text.
|
||||
|
||||
:param bible: The bible to search in (unicode).
|
||||
:param second_bible: The second bible (unicode). We do not search in this bible.
|
||||
:param text: The text to search for (unicode).
|
||||
:param bible: The bible to search
|
||||
:type bible: str
|
||||
:param text: The text to search for
|
||||
:type text: str
|
||||
|
||||
:return: The search results if valid, or None if the search is invalid.
|
||||
:rtype: None, list
|
||||
"""
|
||||
log.debug('BibleManager.verse_search("{bible}", "{text}")'.format(bible=bible, text=text))
|
||||
if not text:
|
||||
return None
|
||||
# If no bibles are installed, message is given.
|
||||
if not bible:
|
||||
self.main_window.information_message(
|
||||
@ -317,8 +323,7 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
UiStrings().BibleNoBibles)
|
||||
return None
|
||||
# Check if the bible or second_bible is a web bible.
|
||||
web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source')
|
||||
if web_bible:
|
||||
if self.db_cache[bible].is_web_bible:
|
||||
# If either Bible is Web, cursor is reset to normal and message is given.
|
||||
self.application.set_normal_cursor()
|
||||
self.main_window.information_message(
|
||||
@ -328,41 +333,8 @@ class BibleManager(OpenLPMixin, RegistryProperties):
|
||||
'This means that the currently selected Bible is a Web Bible.')
|
||||
)
|
||||
return None
|
||||
# Shorter than 3 char searches break OpenLP with very long search times, thus they are blocked.
|
||||
if len(text) - text.count(' ') < 3:
|
||||
return None
|
||||
# Fetch the results from db. If no results are found, return None, no message is given for this.
|
||||
elif text:
|
||||
return self.db_cache[bible].verse_search(text)
|
||||
else:
|
||||
return None
|
||||
|
||||
def verse_search_while_typing(self, bible, second_bible, text):
|
||||
"""
|
||||
Does a verse search for the given bible and text.
|
||||
This is used during "Search while typing"
|
||||
It's the same thing as the normal text search, but it does not show the web Bible error.
|
||||
(It would result in the error popping every time a char is entered or removed)
|
||||
It also does not have a minimum text len, this is set in mediaitem.py
|
||||
|
||||
:param bible: The bible to search in (unicode).
|
||||
:param second_bible: The second bible (unicode). We do not search in this bible.
|
||||
:param text: The text to search for (unicode).
|
||||
"""
|
||||
# If no bibles are installed, message is given.
|
||||
if not bible:
|
||||
return None
|
||||
# Check if the bible or second_bible is a web bible.
|
||||
web_bible = self.db_cache[bible].get_object(BibleMeta, 'download_source')
|
||||
second_web_bible = ''
|
||||
if second_bible:
|
||||
second_web_bible = self.db_cache[second_bible].get_object(BibleMeta, 'download_source')
|
||||
if web_bible or second_web_bible:
|
||||
# If either Bible is Web, cursor is reset to normal and search ends w/o any message.
|
||||
self.application.set_normal_cursor()
|
||||
return None
|
||||
# Fetch the results from db. If no results are found, return None, no message is given for this.
|
||||
elif text:
|
||||
if text:
|
||||
return self.db_cache[bible].verse_search(text)
|
||||
else:
|
||||
return None
|
||||
|
241
openlp/plugins/bibles/lib/mediaitem.py
Normal file → Executable file
241
openlp/plugins/bibles/lib/mediaitem.py
Normal file → Executable file
@ -22,6 +22,7 @@
|
||||
|
||||
import logging
|
||||
import re
|
||||
from enum import IntEnum, unique
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
|
||||
@ -48,15 +49,45 @@ def get_reference_separators():
|
||||
'list': get_reference_separator('sep_l_display')}
|
||||
|
||||
|
||||
class BibleSearch(object):
|
||||
@unique
|
||||
class BibleSearch(IntEnum):
|
||||
"""
|
||||
Enumeration class for the different search methods for the "Search" tab.
|
||||
Enumeration class for the different search types for the "Search" tab.
|
||||
"""
|
||||
Reference = 1
|
||||
Text = 2
|
||||
Combined = 3
|
||||
|
||||
|
||||
@unique
|
||||
class ResultsTab(IntEnum):
|
||||
"""
|
||||
Enumeration class for the different tabs for the results list.
|
||||
"""
|
||||
Saved = 0
|
||||
Search = 1
|
||||
|
||||
|
||||
@unique
|
||||
class SearchStatus(IntEnum):
|
||||
"""
|
||||
Enumeration class for the different search methods.
|
||||
"""
|
||||
SearchButton = 0
|
||||
SearchAsYouType = 1
|
||||
NotEnoughText = 2
|
||||
|
||||
|
||||
@unique
|
||||
class SearchTabs(IntEnum):
|
||||
"""
|
||||
Enumeration class for the tabs on the media item.
|
||||
"""
|
||||
Search = 0
|
||||
Select = 1
|
||||
Options = 2
|
||||
|
||||
|
||||
class BibleMediaItem(MediaManagerItem):
|
||||
"""
|
||||
This is the custom media manager item for Bibles.
|
||||
@ -73,11 +104,13 @@ class BibleMediaItem(MediaManagerItem):
|
||||
:param kwargs: Keyword arguments to pass to the super method. (dict)
|
||||
"""
|
||||
self.clear_icon = build_icon(':/bibles/bibles_search_clear.png')
|
||||
self.lock_icon = build_icon(':/bibles/bibles_search_lock.png')
|
||||
self.unlock_icon = build_icon(':/bibles/bibles_search_unlock.png')
|
||||
self.save_results_icon = build_icon(':/bibles/bibles_save_results.png')
|
||||
self.sort_icon = build_icon(':/bibles/bibles_book_sort.png')
|
||||
self.bible = None
|
||||
self.second_bible = None
|
||||
self.saved_results = []
|
||||
self.current_results = []
|
||||
self.search_status = SearchStatus.SearchButton
|
||||
# TODO: Make more central and clean up after!
|
||||
self.search_timer = QtCore.QTimer()
|
||||
self.search_timer.setInterval(200)
|
||||
@ -162,8 +195,10 @@ class BibleMediaItem(MediaManagerItem):
|
||||
self.select_tab.setVisible(False)
|
||||
self.page_layout.addWidget(self.select_tab)
|
||||
# General Search Opions
|
||||
self.options_widget = QtWidgets.QGroupBox(translate('BiblesPlugin.MediaItem', 'Options'), self)
|
||||
self.general_bible_layout = QtWidgets.QFormLayout(self.options_widget)
|
||||
self.options_tab = QtWidgets.QWidget()
|
||||
self.options_tab.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Minimum)
|
||||
self.search_tab_bar.addTab(translate('BiblesPlugin.MediaItem', 'Options'))
|
||||
self.general_bible_layout = QtWidgets.QFormLayout(self.options_tab)
|
||||
self.version_combo_box = create_horizontal_adjusting_combo_box(self, 'version_combo_box')
|
||||
self.general_bible_layout.addRow('{version}:'.format(version=UiStrings().Version), self.version_combo_box)
|
||||
self.second_combo_box = create_horizontal_adjusting_combo_box(self, 'second_combo_box')
|
||||
@ -171,20 +206,28 @@ class BibleMediaItem(MediaManagerItem):
|
||||
self.style_combo_box = create_horizontal_adjusting_combo_box(self, 'style_combo_box')
|
||||
self.style_combo_box.addItems(['', '', ''])
|
||||
self.general_bible_layout.addRow(UiStrings().LayoutStyle, self.style_combo_box)
|
||||
self.search_button_layout = QtWidgets.QHBoxLayout()
|
||||
self.options_tab.setVisible(False)
|
||||
self.page_layout.addWidget(self.options_tab)
|
||||
# This widget is the easier way to reset the spacing of search_button_layout. (Because page_layout has had its
|
||||
# spacing set to 0)
|
||||
self.search_button_widget = QtWidgets.QWidget()
|
||||
self.search_button_layout = QtWidgets.QHBoxLayout(self.search_button_widget)
|
||||
self.search_button_layout.addStretch()
|
||||
# Note: If we use QPushButton instead of the QToolButton, the icon will be larger than the Lock icon.
|
||||
self.clear_button = QtWidgets.QToolButton(self)
|
||||
self.clear_button = QtWidgets.QPushButton()
|
||||
self.clear_button.setIcon(self.clear_icon)
|
||||
self.lock_button = QtWidgets.QToolButton(self)
|
||||
self.lock_button.setIcon(self.unlock_icon)
|
||||
self.lock_button.setCheckable(True)
|
||||
self.save_results_button = QtWidgets.QPushButton()
|
||||
self.save_results_button.setIcon(self.save_results_icon)
|
||||
self.search_button_layout.addWidget(self.clear_button)
|
||||
self.search_button_layout.addWidget(self.lock_button)
|
||||
self.search_button_layout.addWidget(self.save_results_button)
|
||||
self.search_button = QtWidgets.QPushButton(self)
|
||||
self.search_button_layout.addWidget(self.search_button)
|
||||
self.general_bible_layout.addRow(self.search_button_layout)
|
||||
self.page_layout.addWidget(self.options_widget)
|
||||
self.page_layout.addWidget(self.search_button_widget)
|
||||
self.results_view_tab = QtWidgets.QTabBar(self)
|
||||
self.results_view_tab.addTab('')
|
||||
self.results_view_tab.addTab('')
|
||||
self.results_view_tab.setCurrentIndex(ResultsTab.Search)
|
||||
self.page_layout.addWidget(self.results_view_tab)
|
||||
|
||||
def setupUi(self):
|
||||
super().setupUi()
|
||||
@ -211,12 +254,15 @@ class BibleMediaItem(MediaManagerItem):
|
||||
# Buttons
|
||||
self.book_order_button.toggled.connect(self.on_book_order_button_toggled)
|
||||
self.clear_button.clicked.connect(self.on_clear_button_clicked)
|
||||
self.lock_button.toggled.connect(self.on_lock_button_toggled)
|
||||
self.save_results_button.clicked.connect(self.on_save_results_button_clicked)
|
||||
self.search_button.clicked.connect(self.on_search_button_clicked)
|
||||
# Other stuff
|
||||
self.search_edit.returnPressed.connect(self.on_search_button_clicked)
|
||||
self.search_tab_bar.currentChanged.connect(self.on_search_tab_bar_current_changed)
|
||||
self.results_view_tab.currentChanged.connect(self.on_results_view_tab_current_changed)
|
||||
self.search_edit.textChanged.connect(self.on_search_edit_text_changed)
|
||||
self.on_results_view_tab_total_update(ResultsTab.Saved)
|
||||
self.on_results_view_tab_total_update(ResultsTab.Search)
|
||||
|
||||
def retranslateUi(self):
|
||||
log.debug('retranslateUi')
|
||||
@ -225,9 +271,9 @@ class BibleMediaItem(MediaManagerItem):
|
||||
self.style_combo_box.setItemText(LayoutStyle.VersePerSlide, UiStrings().VersePerSlide)
|
||||
self.style_combo_box.setItemText(LayoutStyle.VersePerLine, UiStrings().VersePerLine)
|
||||
self.style_combo_box.setItemText(LayoutStyle.Continuous, UiStrings().Continuous)
|
||||
self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the search results.'))
|
||||
self.lock_button.setToolTip(
|
||||
translate('BiblesPlugin.MediaItem', 'Toggle to keep or clear the previous results.'))
|
||||
self.clear_button.setToolTip(translate('BiblesPlugin.MediaItem', 'Clear the results on the current tab.'))
|
||||
self.save_results_button.setToolTip(
|
||||
translate('BiblesPlugin.MediaItem', 'Add the search results to the saved list.'))
|
||||
self.search_button.setText(UiStrings().Search)
|
||||
|
||||
def on_focus(self):
|
||||
@ -241,8 +287,10 @@ class BibleMediaItem(MediaManagerItem):
|
||||
if self.search_tab.isVisible():
|
||||
self.search_edit.setFocus()
|
||||
self.search_edit.selectAll()
|
||||
else:
|
||||
if self.select_tab.isVisible():
|
||||
self.select_book_combo_box.setFocus()
|
||||
if self.options_tab.isVisible():
|
||||
self.version_combo_box.setFocus()
|
||||
|
||||
def config_update(self):
|
||||
"""
|
||||
@ -415,14 +463,48 @@ class BibleMediaItem(MediaManagerItem):
|
||||
"""
|
||||
Show the selected tab and set focus to it
|
||||
|
||||
:param index: The tab selected (int)
|
||||
:param index: The tab selected
|
||||
:type index: int
|
||||
:return: None
|
||||
"""
|
||||
search_tab = index == 0
|
||||
self.search_tab.setVisible(search_tab)
|
||||
self.select_tab.setVisible(not search_tab)
|
||||
if index == SearchTabs.Search or index == SearchTabs.Select:
|
||||
self.search_button.setEnabled(True)
|
||||
else:
|
||||
self.search_button.setEnabled(False)
|
||||
self.search_tab.setVisible(index == SearchTabs.Search)
|
||||
self.select_tab.setVisible(index == SearchTabs.Select)
|
||||
self.options_tab.setVisible(index == SearchTabs.Options)
|
||||
self.on_focus()
|
||||
|
||||
def on_results_view_tab_current_changed(self, index):
|
||||
"""
|
||||
Update list_widget with the contents of the selected list
|
||||
|
||||
:param index: The index of the tab that has been changed to. (int)
|
||||
:return: None
|
||||
"""
|
||||
if index == ResultsTab.Saved:
|
||||
self.add_built_results_to_list_widget(self.saved_results)
|
||||
elif index == ResultsTab.Search:
|
||||
self.add_built_results_to_list_widget(self.current_results)
|
||||
|
||||
def on_results_view_tab_total_update(self, index):
|
||||
"""
|
||||
Update the result total count on the tab with the given index.
|
||||
|
||||
:param index: Index of the tab to update (int)
|
||||
:return: None
|
||||
"""
|
||||
string = ''
|
||||
count = 0
|
||||
if index == ResultsTab.Saved:
|
||||
string = translate('BiblesPlugin.MediaItem', 'Saved ({result_count})')
|
||||
count = len(self.saved_results)
|
||||
elif index == ResultsTab.Search:
|
||||
string = translate('BiblesPlugin.MediaItem', 'Results ({result_count})')
|
||||
count = len(self.current_results)
|
||||
self.results_view_tab.setTabText(index, string.format(result_count=count))
|
||||
|
||||
def on_book_order_button_toggled(self, checked):
|
||||
"""
|
||||
Change the sort order of the book names
|
||||
@ -442,22 +524,25 @@ class BibleMediaItem(MediaManagerItem):
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.list_view.clear()
|
||||
self.search_edit.clear()
|
||||
self.on_focus()
|
||||
current_index = self.results_view_tab.currentIndex()
|
||||
for item in self.list_view.selectedItems():
|
||||
self.list_view.takeItem(self.list_view.row(item))
|
||||
results = [item.data(QtCore.Qt.UserRole) for item in self.list_view.allItems()]
|
||||
if current_index == ResultsTab.Saved:
|
||||
self.saved_results = results
|
||||
elif current_index == ResultsTab.Search:
|
||||
self.current_results = results
|
||||
self.on_results_view_tab_total_update(current_index)
|
||||
|
||||
def on_lock_button_toggled(self, checked):
|
||||
def on_save_results_button_clicked(self):
|
||||
"""
|
||||
Toggle the lock button, if Search tab is used, set focus to search field.
|
||||
Add the selected verses to the saved_results list.
|
||||
|
||||
:param checked: The state of the toggle button. (bool)
|
||||
:return: None
|
||||
"""
|
||||
self.list_view.locked = checked
|
||||
if checked:
|
||||
self.sender().setIcon(self.lock_icon)
|
||||
else:
|
||||
self.sender().setIcon(self.unlock_icon)
|
||||
for verse in self.list_view.selectedItems():
|
||||
self.saved_results.append(verse.data(QtCore.Qt.UserRole))
|
||||
self.on_results_view_tab_total_update(ResultsTab.Saved)
|
||||
|
||||
def on_style_combo_box_index_changed(self, index):
|
||||
"""
|
||||
@ -490,16 +575,17 @@ class BibleMediaItem(MediaManagerItem):
|
||||
:return: None
|
||||
"""
|
||||
new_selection = self.second_combo_box.currentData()
|
||||
if self.list_view.count():
|
||||
if self.saved_results:
|
||||
# Exclusive or (^) the new and previous selections to detect if the user has switched between single and
|
||||
# dual bible mode
|
||||
if (new_selection is None) ^ (self.second_bible is None):
|
||||
if critical_error_message_box(
|
||||
message=translate('BiblesPlugin.MediaItem',
|
||||
'OpenLP cannot combine single and dual Bible verse search results. '
|
||||
'Do you want to clear your search results and start a new search?'),
|
||||
'Do you want to clear your saved results?'),
|
||||
parent=self, question=True) == QtWidgets.QMessageBox.Yes:
|
||||
self.list_view.clear(override_lock=True)
|
||||
self.saved_results = []
|
||||
self.on_results_view_tab_total_update(ResultsTab.Saved)
|
||||
else:
|
||||
self.second_combo_box.setCurrentIndex(self.second_combo_box.findData(self.second_bible))
|
||||
return
|
||||
@ -525,7 +611,8 @@ class BibleMediaItem(MediaManagerItem):
|
||||
log.warning('Not enough chapters in %s', book_ref_id)
|
||||
critical_error_message_box(message=translate('BiblesPlugin.MediaItem', 'Bible not fully loaded.'))
|
||||
else:
|
||||
self.search_button.setEnabled(True)
|
||||
if self.select_tab.isVisible():
|
||||
self.search_button.setEnabled(True)
|
||||
self.adjust_combo_box(1, self.chapter_count, self.from_chapter)
|
||||
self.adjust_combo_box(1, self.chapter_count, self.to_chapter)
|
||||
self.adjust_combo_box(1, verse_count, self.from_verse)
|
||||
@ -602,6 +689,8 @@ class BibleMediaItem(MediaManagerItem):
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.search_timer.stop()
|
||||
self.search_status = SearchStatus.SearchButton
|
||||
if not self.bible:
|
||||
self.main_window.information_message(UiStrings().BibleNoBiblesTitle, UiStrings().BibleNoBibles)
|
||||
return
|
||||
@ -613,6 +702,7 @@ class BibleMediaItem(MediaManagerItem):
|
||||
elif self.select_tab.isVisible():
|
||||
self.select_search()
|
||||
self.search_button.setEnabled(True)
|
||||
self.results_view_tab.setCurrentIndex(ResultsTab.Search)
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
def select_search(self):
|
||||
@ -636,18 +726,21 @@ class BibleMediaItem(MediaManagerItem):
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.search_results = []
|
||||
verse_refs = self.plugin.manager.parse_ref(self.bible.name, search_text)
|
||||
self.search_results = self.plugin.manager.get_verses(self.bible.name, verse_refs, True)
|
||||
if self.second_bible and self.search_results:
|
||||
self.search_results = self.plugin.manager.get_verses(self.second_bible.name, verse_refs, True)
|
||||
self.display_results()
|
||||
|
||||
def on_text_search(self, text, search_while_type=False):
|
||||
def on_text_search(self, text):
|
||||
"""
|
||||
We are doing a 'Text Search'.
|
||||
This search is called on def text_search by 'Search' Text and Combined Searches.
|
||||
"""
|
||||
self.search_results = self.plugin.manager.verse_search(self.bible.name, text)
|
||||
if self.search_results is None:
|
||||
return
|
||||
if self.second_bible and self.search_results:
|
||||
filtered_search_results = []
|
||||
not_found_count = 0
|
||||
@ -663,7 +756,7 @@ class BibleMediaItem(MediaManagerItem):
|
||||
verse=verse.verse, bible_name=self.second_bible.name))
|
||||
not_found_count += 1
|
||||
self.search_results = filtered_search_results
|
||||
if not_found_count != 0 and not search_while_type:
|
||||
if not_found_count != 0 and self.search_status == SearchStatus.SearchButton:
|
||||
self.main_window.information_message(
|
||||
translate('BiblesPlugin.MediaItem', 'Verses not found'),
|
||||
translate('BiblesPlugin.MediaItem',
|
||||
@ -673,22 +766,23 @@ class BibleMediaItem(MediaManagerItem):
|
||||
).format(second_name=self.second_bible.name, name=self.bible.name, count=not_found_count))
|
||||
self.display_results()
|
||||
|
||||
def text_search(self, search_while_type=False):
|
||||
def text_search(self):
|
||||
"""
|
||||
This triggers the proper 'Search' search based on which search type is used.
|
||||
"Eg. "Reference Search", "Text Search" or "Combined search".
|
||||
"""
|
||||
self.search_results = []
|
||||
log.debug('text_search called')
|
||||
text = self.search_edit.text()
|
||||
if text == '':
|
||||
self.list_view.clear()
|
||||
self.display_results()
|
||||
return
|
||||
self.list_view.clear(search_while_typing=search_while_type)
|
||||
self.on_results_view_tab_total_update(ResultsTab.Search)
|
||||
if self.search_edit.current_search_type() == BibleSearch.Reference:
|
||||
if get_reference_match('full').match(text):
|
||||
# Valid reference found. Do reference search.
|
||||
self.text_reference_search(text)
|
||||
elif not search_while_type:
|
||||
elif self.search_status == SearchStatus.SearchButton:
|
||||
self.main_window.information_message(
|
||||
translate('BiblesPlugin.BibleManager', 'Scripture Reference Error'),
|
||||
translate('BiblesPlugin.BibleManager',
|
||||
@ -700,10 +794,12 @@ class BibleMediaItem(MediaManagerItem):
|
||||
self.text_reference_search(text)
|
||||
else:
|
||||
# It can only be a 'Combined' search without a valid reference, or a 'Text' search
|
||||
if search_while_type:
|
||||
if len(text) > 8 and VALID_TEXT_SEARCH.search(text):
|
||||
self.on_text_search(text, True)
|
||||
elif VALID_TEXT_SEARCH.search(text):
|
||||
if self.search_status == SearchStatus.SearchAsYouType:
|
||||
if len(text) <= 8:
|
||||
self.search_status = SearchStatus.NotEnoughText
|
||||
self.display_results()
|
||||
return
|
||||
if VALID_TEXT_SEARCH.search(text):
|
||||
self.on_text_search(text)
|
||||
|
||||
def on_search_edit_text_changed(self):
|
||||
@ -713,9 +809,12 @@ class BibleMediaItem(MediaManagerItem):
|
||||
|
||||
:return: None
|
||||
"""
|
||||
if Settings().value('bibles/is search while typing enabled'):
|
||||
if not self.search_timer.isActive():
|
||||
self.search_timer.start()
|
||||
if not Settings().value('bibles/is search while typing enabled') or \
|
||||
not self.bible or self.bible.is_web_bible or \
|
||||
(self.second_bible and self.bible.is_web_bible):
|
||||
return
|
||||
if not self.search_timer.isActive():
|
||||
self.search_timer.start()
|
||||
|
||||
def on_search_timer_timeout(self):
|
||||
"""
|
||||
@ -724,7 +823,9 @@ class BibleMediaItem(MediaManagerItem):
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.text_search(True)
|
||||
self.search_status = SearchStatus.SearchAsYouType
|
||||
self.text_search()
|
||||
self.results_view_tab.setCurrentIndex(ResultsTab.Search)
|
||||
|
||||
def display_results(self):
|
||||
"""
|
||||
@ -732,14 +833,16 @@ class BibleMediaItem(MediaManagerItem):
|
||||
|
||||
:return: None
|
||||
"""
|
||||
self.list_view.clear()
|
||||
if self.search_results:
|
||||
items = self.build_display_results(self.bible, self.second_bible, self.search_results)
|
||||
for item in items:
|
||||
self.list_view.addItem(item)
|
||||
self.list_view.selectAll()
|
||||
self.current_results = self.build_display_results(self.bible, self.second_bible, self.search_results)
|
||||
self.search_results = []
|
||||
self.second_search_results = []
|
||||
self.add_built_results_to_list_widget(self.current_results)
|
||||
|
||||
def add_built_results_to_list_widget(self, results):
|
||||
self.list_view.clear(self.search_status == SearchStatus.NotEnoughText)
|
||||
for item in self.build_list_widget_items(results):
|
||||
self.list_view.addItem(item)
|
||||
self.list_view.selectAll()
|
||||
self.on_results_view_tab_total_update(ResultsTab.Search)
|
||||
|
||||
def build_display_results(self, bible, second_bible, search_results):
|
||||
"""
|
||||
@ -789,11 +892,18 @@ class BibleMediaItem(MediaManagerItem):
|
||||
bible_text = '{book} {chapter:d}{sep}{verse:d} ({version}, {second_version})'
|
||||
else:
|
||||
bible_text = '{book} {chapter:d}{sep}{verse:d} ({version})'
|
||||
bible_verse = QtWidgets.QListWidgetItem(bible_text.format(sep=verse_separator, **data))
|
||||
bible_verse.setData(QtCore.Qt.UserRole, data)
|
||||
items.append(bible_verse)
|
||||
data['item_title'] = bible_text.format(sep=verse_separator, **data)
|
||||
items.append(data)
|
||||
return items
|
||||
|
||||
def build_list_widget_items(self, items):
|
||||
list_widget_items = []
|
||||
for data in items:
|
||||
bible_verse = QtWidgets.QListWidgetItem(data['item_title'])
|
||||
bible_verse.setData(QtCore.Qt.UserRole, data)
|
||||
list_widget_items.append(bible_verse)
|
||||
return list_widget_items
|
||||
|
||||
def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
|
||||
context=ServiceItemContext.Service):
|
||||
"""
|
||||
@ -897,6 +1007,8 @@ class BibleMediaItem(MediaManagerItem):
|
||||
"""
|
||||
Search for some Bible verses (by reference).
|
||||
"""
|
||||
if self.bible is None:
|
||||
return []
|
||||
reference = self.plugin.manager.parse_ref(self.bible.name, string)
|
||||
search_results = self.plugin.manager.get_verses(self.bible.name, reference, showError)
|
||||
if search_results:
|
||||
@ -908,6 +1020,9 @@ class BibleMediaItem(MediaManagerItem):
|
||||
"""
|
||||
Create a media item from an item id.
|
||||
"""
|
||||
if self.bible is None:
|
||||
return []
|
||||
reference = self.plugin.manager.parse_ref(self.bible.name, item_id)
|
||||
search_results = self.plugin.manager.get_verses(self.bible.name, reference, False)
|
||||
return self.build_display_results(self.bible, None, search_results)
|
||||
items = self.build_display_results(self.bible, None, search_results)
|
||||
return self.build_list_widget_items(items)
|
||||
|
@ -23,7 +23,6 @@
|
||||
The :mod:`songshowplus` module provides the functionality for importing SongShow Plus songs into the OpenLP
|
||||
database.
|
||||
"""
|
||||
import chardet
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
@ -226,6 +225,7 @@ class SongShowPlusImport(SongImport):
|
||||
|
||||
def decode(self, data):
|
||||
try:
|
||||
return str(data, chardet.detect(data)['encoding'])
|
||||
# Don't question this, it works...
|
||||
return data.decode('utf-8').encode('cp1251').decode('cp1251')
|
||||
except:
|
||||
return str(data, retrieve_windows_encoding())
|
||||
return data.decode(retrieve_windows_encoding())
|
||||
|
BIN
resources/images/bibles_save_results.png
Normal file
BIN
resources/images/bibles_save_results.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 735 B |
Binary file not shown.
Before Width: | Height: | Size: 452 B |
Binary file not shown.
Before Width: | Height: | Size: 440 B |
Binary file not shown.
Before Width: | Height: | Size: 577 B |
@ -34,8 +34,7 @@
|
||||
<file>bibles_search_text.png</file>
|
||||
<file>bibles_search_reference.png</file>
|
||||
<file>bibles_search_clear.png</file>
|
||||
<file>bibles_search_unlock.png</file>
|
||||
<file>bibles_search_lock.png</file>
|
||||
<file>bibles_save_results.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="plugins">
|
||||
<file>plugin_alerts.png</file>
|
||||
@ -144,7 +143,6 @@
|
||||
</qresource>
|
||||
<qresource prefix="remote">
|
||||
<file>network_server.png</file>
|
||||
<file>network_ssl.png</file>
|
||||
<file>network_auth.png</file>
|
||||
</qresource>
|
||||
<qresource prefix="songusage">
|
||||
@ -188,4 +186,4 @@
|
||||
<file>android_app_qr.png</file>
|
||||
<file>ios_app_qr.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
</RCC>
|
||||
|
@ -30,7 +30,7 @@ from PyQt5 import QtCore
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib import Renderer, ScreenList, ServiceItem, FormattingTags
|
||||
from openlp.core.lib.renderer import words_split, get_start_tags
|
||||
from openlp.core.lib.theme import ThemeXML
|
||||
from openlp.core.lib.theme import Theme
|
||||
|
||||
|
||||
SCREEN = {
|
||||
@ -189,7 +189,7 @@ class TestRenderer(TestCase):
|
||||
# GIVEN: test object and data
|
||||
mock_lyrics_css.return_value = ' FORMAT CSS; '
|
||||
mock_outline_css.return_value = ' OUTLINE CSS; '
|
||||
theme_data = ThemeXML()
|
||||
theme_data = Theme()
|
||||
theme_data.font_main_name = 'Arial'
|
||||
theme_data.font_main_size = 20
|
||||
theme_data.font_main_color = '#FFFFFF'
|
||||
|
@ -25,36 +25,30 @@ Package to test the openlp.core.lib.theme package.
|
||||
from unittest import TestCase
|
||||
import os
|
||||
|
||||
from openlp.core.lib.theme import ThemeXML
|
||||
from openlp.core.lib.theme import Theme
|
||||
|
||||
|
||||
class TestThemeXML(TestCase):
|
||||
class TestTheme(TestCase):
|
||||
"""
|
||||
Test the ThemeXML class
|
||||
Test the Theme class
|
||||
"""
|
||||
def test_new_theme(self):
|
||||
"""
|
||||
Test the ThemeXML constructor
|
||||
Test the Theme constructor
|
||||
"""
|
||||
# GIVEN: The ThemeXML class
|
||||
# GIVEN: The Theme class
|
||||
# WHEN: A theme object is created
|
||||
default_theme = ThemeXML()
|
||||
default_theme = Theme()
|
||||
|
||||
# THEN: The default values should be correct
|
||||
self.assertEqual('#000000', default_theme.background_border_color,
|
||||
'background_border_color should be "#000000"')
|
||||
self.assertEqual('solid', default_theme.background_type, 'background_type should be "solid"')
|
||||
self.assertEqual(0, default_theme.display_vertical_align, 'display_vertical_align should be 0')
|
||||
self.assertEqual('Arial', default_theme.font_footer_name, 'font_footer_name should be "Arial"')
|
||||
self.assertFalse(default_theme.font_main_bold, 'font_main_bold should be False')
|
||||
self.assertEqual(47, len(default_theme.__dict__), 'The theme should have 47 attributes')
|
||||
self.check_theme(default_theme)
|
||||
|
||||
def test_expand_json(self):
|
||||
"""
|
||||
Test the expand_json method
|
||||
"""
|
||||
# GIVEN: A ThemeXML object and some JSON to "expand"
|
||||
theme = ThemeXML()
|
||||
# GIVEN: A Theme object and some JSON to "expand"
|
||||
theme = Theme()
|
||||
theme_json = {
|
||||
'background': {
|
||||
'border_color': '#000000',
|
||||
@ -73,31 +67,48 @@ class TestThemeXML(TestCase):
|
||||
}
|
||||
}
|
||||
|
||||
# WHEN: ThemeXML.expand_json() is run
|
||||
# WHEN: Theme.expand_json() is run
|
||||
theme.expand_json(theme_json)
|
||||
|
||||
# THEN: The attributes should be set on the object
|
||||
self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
|
||||
self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
|
||||
self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
|
||||
self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
|
||||
self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
|
||||
self.check_theme(theme)
|
||||
|
||||
def test_extend_image_filename(self):
|
||||
"""
|
||||
Test the extend_image_filename method
|
||||
"""
|
||||
# GIVEN: A theme object
|
||||
theme = ThemeXML()
|
||||
theme = Theme()
|
||||
theme.theme_name = 'MyBeautifulTheme '
|
||||
theme.background_filename = ' video.mp4'
|
||||
theme.background_type = 'video'
|
||||
path = os.path.expanduser('~')
|
||||
|
||||
# WHEN: ThemeXML.extend_image_filename is run
|
||||
# WHEN: Theme.extend_image_filename is run
|
||||
theme.extend_image_filename(path)
|
||||
|
||||
# THEN: The filename of the background should be correct
|
||||
expected_filename = os.path.join(path, 'MyBeautifulTheme', 'video.mp4')
|
||||
self.assertEqual(expected_filename, theme.background_filename)
|
||||
self.assertEqual('MyBeautifulTheme', theme.theme_name)
|
||||
|
||||
def test_save_retrieve(self):
|
||||
"""
|
||||
Load a dummy theme, save it and reload it
|
||||
"""
|
||||
# GIVEN: The default Theme class
|
||||
# WHEN: A theme object is created
|
||||
default_theme = Theme()
|
||||
# THEN: The default values should be correct
|
||||
save_theme_json = default_theme.export_theme()
|
||||
lt = Theme()
|
||||
lt.load_theme(save_theme_json)
|
||||
self.check_theme(lt)
|
||||
|
||||
def check_theme(self, theme):
|
||||
self.assertEqual('#000000', theme.background_border_color, 'background_border_color should be "#000000"')
|
||||
self.assertEqual('solid', theme.background_type, 'background_type should be "solid"')
|
||||
self.assertEqual(0, theme.display_vertical_align, 'display_vertical_align should be 0')
|
||||
self.assertFalse(theme.font_footer_bold, 'font_footer_bold should be False')
|
||||
self.assertEqual('Arial', theme.font_main_name, 'font_main_name should be "Arial"')
|
||||
self.assertEqual(47, len(theme.__dict__), 'The theme should have 47 attributes')
|
||||
|
@ -63,7 +63,7 @@ class TestThemeManager(TestCase):
|
||||
mocked_zipfile_init.return_value = None
|
||||
|
||||
# WHEN: The theme is exported
|
||||
theme_manager._export_theme(os.path.join('some', 'path'), 'Default')
|
||||
theme_manager._export_theme(os.path.join('some', 'path', 'Default.otz'), 'Default')
|
||||
|
||||
# THEN: The zipfile should be created at the given path
|
||||
mocked_zipfile_init.assert_called_with(os.path.join('some', 'path', 'Default.otz'), 'w')
|
||||
@ -126,8 +126,9 @@ class TestThemeManager(TestCase):
|
||||
theme_manager.path = ''
|
||||
mocked_theme = MagicMock()
|
||||
mocked_theme.theme_name = 'themename'
|
||||
mocked_theme.extract_formatted_xml = MagicMock()
|
||||
mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
|
||||
mocked_theme.filename = "filename"
|
||||
# mocked_theme.extract_formatted_xml = MagicMock()
|
||||
# mocked_theme.extract_formatted_xml.return_value = 'fake_theme_xml'.encode()
|
||||
|
||||
# WHEN: Calling _write_theme with path to different images
|
||||
file_name1 = os.path.join(TEST_RESOURCES_PATH, 'church.jpg')
|
||||
@ -148,14 +149,13 @@ class TestThemeManager(TestCase):
|
||||
theme_manager.path = self.temp_folder
|
||||
mocked_theme = MagicMock()
|
||||
mocked_theme.theme_name = 'theme 愛 name'
|
||||
mocked_theme.extract_formatted_xml = MagicMock()
|
||||
mocked_theme.extract_formatted_xml.return_value = 'fake theme 愛 XML'.encode()
|
||||
mocked_theme.export_theme.return_value = "{}"
|
||||
|
||||
# WHEN: Calling _write_theme with a theme with a name with special characters in it
|
||||
theme_manager._write_theme(mocked_theme, None, None)
|
||||
|
||||
# THEN: It should have been created
|
||||
self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.xml')),
|
||||
self.assertTrue(os.path.exists(os.path.join(self.temp_folder, 'theme 愛 name', 'theme 愛 name.json')),
|
||||
'Theme with special characters should have been created!')
|
||||
|
||||
def test_over_write_message_box_yes(self):
|
||||
|
64
tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py
Normal file → Executable file
64
tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py
Normal file → Executable file
@ -23,6 +23,7 @@
|
||||
This module contains tests for the openlp.core.lib.listwidgetwithdnd module
|
||||
"""
|
||||
from unittest import TestCase
|
||||
from types import GeneratorType
|
||||
|
||||
from openlp.core.common.uistrings import UiStrings
|
||||
from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
|
||||
@ -33,37 +34,6 @@ class TestListWidgetWithDnD(TestCase):
|
||||
"""
|
||||
Test the :class:`~openlp.core.lib.listwidgetwithdnd.ListWidgetWithDnD` class
|
||||
"""
|
||||
def test_clear_locked(self):
|
||||
"""
|
||||
Test the clear method the list is 'locked'
|
||||
"""
|
||||
with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
|
||||
# GIVEN: An instance of ListWidgetWithDnD
|
||||
widget = ListWidgetWithDnD()
|
||||
|
||||
# WHEN: The list is 'locked' and clear has been called
|
||||
widget.locked = True
|
||||
widget.clear()
|
||||
|
||||
# THEN: The super method should not have been called (i.e. The list not cleared)
|
||||
self.assertFalse(mocked_clear_super_method.called)
|
||||
|
||||
def test_clear_overide_locked(self):
|
||||
"""
|
||||
Test the clear method the list is 'locked', but clear is called with 'override_lock' set to True
|
||||
"""
|
||||
with patch('openlp.core.ui.lib.listwidgetwithdnd.QtWidgets.QListWidget.clear') as mocked_clear_super_method:
|
||||
# GIVEN: An instance of ListWidgetWithDnD
|
||||
widget = ListWidgetWithDnD()
|
||||
|
||||
# WHEN: The list is 'locked' and clear has been called with override_lock se to True
|
||||
widget.locked = True
|
||||
widget.clear(override_lock=True)
|
||||
|
||||
# THEN: The super method should have been called (i.e. The list is cleared regardless whether it is locked
|
||||
# or not)
|
||||
mocked_clear_super_method.assert_called_once_with()
|
||||
|
||||
def test_clear(self):
|
||||
"""
|
||||
Test the clear method when called without any arguments.
|
||||
@ -90,6 +60,38 @@ class TestListWidgetWithDnD(TestCase):
|
||||
# THEN: The results text should be the 'short results' text.
|
||||
self.assertEqual(widget.no_results_text, UiStrings().ShortResults)
|
||||
|
||||
def test_all_items_no_list_items(self):
|
||||
"""
|
||||
Test allItems when there are no items in the list widget
|
||||
"""
|
||||
# GIVEN: An instance of ListWidgetWithDnD
|
||||
widget = ListWidgetWithDnD()
|
||||
with patch.object(widget, 'count', return_value=0), \
|
||||
patch.object(widget, 'item', side_effect=lambda x: [][x]):
|
||||
|
||||
# WHEN: Calling allItems
|
||||
result = widget.allItems()
|
||||
|
||||
# THEN: An instance of a Generator object should be returned. The generator should not yeild any results
|
||||
self.assertIsInstance(result, GeneratorType)
|
||||
self.assertEqual(list(result), [])
|
||||
|
||||
def test_all_items_list_items(self):
|
||||
"""
|
||||
Test allItems when the list widget contains some items.
|
||||
"""
|
||||
# GIVEN: An instance of ListWidgetWithDnD
|
||||
widget = ListWidgetWithDnD()
|
||||
with patch.object(widget, 'count', return_value=2), \
|
||||
patch.object(widget, 'item', side_effect=lambda x: [5, 3][x]):
|
||||
|
||||
# WHEN: Calling allItems
|
||||
result = widget.allItems()
|
||||
|
||||
# THEN: An instance of a Generator object should be returned. The generator should not yeild any results
|
||||
self.assertIsInstance(result, GeneratorType)
|
||||
self.assertEqual(list(result), [5, 3])
|
||||
|
||||
def test_paint_event(self):
|
||||
"""
|
||||
Test the paintEvent method when the list is not empty
|
||||
|
98
tests/functional/openlp_plugins/bibles/test_mediaitem.py
Normal file → Executable file
98
tests/functional/openlp_plugins/bibles/test_mediaitem.py
Normal file → Executable file
@ -31,7 +31,8 @@ from tests.helpers.testmixin import TestMixin
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.lib import MediaManagerItem
|
||||
from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, get_reference_separators, VALID_TEXT_SEARCH
|
||||
from openlp.plugins.bibles.lib.mediaitem import BibleMediaItem, BibleSearch, ResultsTab, SearchStatus, \
|
||||
get_reference_separators, VALID_TEXT_SEARCH
|
||||
|
||||
|
||||
class TestBibleMediaItemModulefunctions(TestCase):
|
||||
@ -143,6 +144,7 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.media_item = BibleMediaItem(None, self.mocked_plugin)
|
||||
|
||||
self.media_item.settings_section = 'bibles'
|
||||
self.media_item.results_view_tab = MagicMock()
|
||||
|
||||
self.mocked_book_1 = MagicMock(**{'get_name.return_value': 'Book 1', 'book_reference_id': 1})
|
||||
self.mocked_book_2 = MagicMock(**{'get_name.return_value': 'Book 2', 'book_reference_id': 2})
|
||||
@ -658,56 +660,65 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
# THEN: The select_book_combo_box model sort should have been reset
|
||||
self.media_item.select_book_combo_box.model().sort.assert_called_once_with(-1)
|
||||
|
||||
def test_on_clear_button_clicked(self):
|
||||
def test_on_clear_button_clicked_saved_tab(self):
|
||||
"""
|
||||
Test on_clear_button_clicked
|
||||
Test on_clear_button_clicked when the saved tab is selected
|
||||
"""
|
||||
# GIVEN: An instance of :class:`MediaManagerItem` and mocked out saved_tab and select_tab and a mocked out
|
||||
# list_view and search_edit
|
||||
self.media_item.list_view = MagicMock()
|
||||
self.media_item.search_edit = MagicMock()
|
||||
self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Saved})
|
||||
self.media_item.saved_results = ['Some', 'Results']
|
||||
with patch.object(self.media_item, 'on_focus'):
|
||||
|
||||
# WHEN: Calling on_clear_button_clicked
|
||||
self.media_item.on_clear_button_clicked()
|
||||
|
||||
# THEN: The list_view should be cleared
|
||||
self.assertEqual(self.media_item.saved_results, [])
|
||||
self.media_item.list_view.clear.assert_called_once_with()
|
||||
|
||||
def test_on_clear_button_clicked_search_tab(self):
|
||||
"""
|
||||
Test on_clear_button_clicked when the search tab is selected
|
||||
"""
|
||||
# GIVEN: An instance of :class:`MediaManagerItem` and mocked out search_tab and select_tab and a mocked out
|
||||
# list_view and search_edit
|
||||
self.media_item.list_view = MagicMock()
|
||||
self.media_item.search_edit = MagicMock()
|
||||
self.media_item.results_view_tab = MagicMock(**{'currentIndex.return_value': ResultsTab.Search})
|
||||
self.media_item.current_results = ['Some', 'Results']
|
||||
with patch.object(self.media_item, 'on_focus'):
|
||||
|
||||
# WHEN: Calling on_clear_button_clicked
|
||||
self.media_item.on_clear_button_clicked()
|
||||
|
||||
# THEN: The list_view and the search_edit should be cleared
|
||||
self.assertEqual(self.media_item.current_results, [])
|
||||
self.media_item.list_view.clear.assert_called_once_with()
|
||||
self.media_item.search_edit.clear.assert_called_once_with()
|
||||
|
||||
def test_on_lock_button_toggled_search_tab_lock_icon(self):
|
||||
def test_on_save_results_button_clicked(self):
|
||||
"""
|
||||
Test that "on_lock_button_toggled" toggles the lock properly.
|
||||
Test that "on_save_results_button_clicked" saves the results.
|
||||
"""
|
||||
# GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view
|
||||
self.media_item.list_view = MagicMock()
|
||||
self.media_item.lock_icon = 'lock icon'
|
||||
mocked_sender_instance = MagicMock()
|
||||
with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance):
|
||||
# GIVEN: An instance of :class:`MediaManagerItem` and a mocked list_view
|
||||
result_1 = MagicMock(**{'data.return_value': 'R1'})
|
||||
result_2 = MagicMock(**{'data.return_value': 'R2'})
|
||||
result_3 = MagicMock(**{'data.return_value': 'R3'})
|
||||
self.media_item.list_view = MagicMock(**{'selectedItems.return_value': [result_1, result_2, result_3]})
|
||||
|
||||
# WHEN: When the lock_button is checked
|
||||
self.media_item.on_lock_button_toggled(True)
|
||||
with patch.object(self.media_item, 'on_results_view_tab_total_update') as \
|
||||
mocked_on_results_view_tab_total_update:
|
||||
|
||||
# THEN: list_view should be 'locked' and the lock icon set
|
||||
self.assertTrue(self.media_item.list_view.locked)
|
||||
mocked_sender_instance.setIcon.assert_called_once_with('lock icon')
|
||||
# WHEN: When the save_results_button is clicked
|
||||
self.media_item.on_save_results_button_clicked()
|
||||
|
||||
def test_on_lock_button_toggled_unlock_icon(self):
|
||||
"""
|
||||
Test that "on_lock_button_toggled" toggles the lock properly.
|
||||
"""
|
||||
# GIVEN: An instance of :class:`MediaManagerItem` a mocked sender and list_view
|
||||
self.media_item.list_view = MagicMock()
|
||||
self.media_item.unlock_icon = 'unlock icon'
|
||||
mocked_sender_instance = MagicMock()
|
||||
with patch.object(self.media_item, 'sender', return_value=mocked_sender_instance):
|
||||
|
||||
# WHEN: When the lock_button is unchecked
|
||||
self.media_item.on_lock_button_toggled(False)
|
||||
|
||||
# THEN: list_view should be 'unlocked' and the unlock icon set
|
||||
self.assertFalse(self.media_item.list_view.locked)
|
||||
mocked_sender_instance.setIcon.assert_called_once_with('unlock icon')
|
||||
# THEN: The selected results in the list_view should be added to the 'saved_results' list. And the saved_tab
|
||||
# total should be updated.
|
||||
self.assertEqual(self.media_item.saved_results, ['R1', 'R2', 'R3'])
|
||||
mocked_on_results_view_tab_total_update.assert_called_once_with(ResultsTab.Saved)
|
||||
|
||||
def test_on_style_combo_box_changed(self):
|
||||
"""
|
||||
@ -815,7 +826,9 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.media_item.list_view = MagicMock(**{'count.return_value': 5})
|
||||
self.media_item.style_combo_box = MagicMock()
|
||||
self.media_item.select_book_combo_box = MagicMock()
|
||||
self.media_item.search_results = ['list', 'of', 'results']
|
||||
with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
|
||||
patch.object(self.media_item, 'display_results'), \
|
||||
patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
|
||||
return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
|
||||
|
||||
@ -825,9 +838,8 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': self.mocked_bible_1})
|
||||
self.media_item.on_second_combo_box_index_changed(5)
|
||||
|
||||
# THEN: The list_view should be cleared and the selected bible should be set as the current bible
|
||||
# THEN: The selected bible should be set as the current bible
|
||||
self.assertTrue(mocked_critical_error_message_box.called)
|
||||
self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
|
||||
self.media_item.style_combo_box.setEnabled.assert_called_once_with(False)
|
||||
self.assertTrue(mocked_initialise_advanced_bible.called)
|
||||
self.assertEqual(self.media_item.second_bible, self.mocked_bible_1)
|
||||
@ -841,7 +853,9 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.media_item.list_view = MagicMock(**{'count.return_value': 5})
|
||||
self.media_item.style_combo_box = MagicMock()
|
||||
self.media_item.select_book_combo_box = MagicMock()
|
||||
self.media_item.search_results = ['list', 'of', 'results']
|
||||
with patch.object(self.media_item, 'initialise_advanced_bible') as mocked_initialise_advanced_bible, \
|
||||
patch.object(self.media_item, 'display_results'), \
|
||||
patch('openlp.plugins.bibles.lib.mediaitem.critical_error_message_box',
|
||||
return_value=QtWidgets.QMessageBox.Yes) as mocked_critical_error_message_box:
|
||||
# WHEN: The previously is a bible new selection is None and the user selects yes
|
||||
@ -850,9 +864,8 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.media_item.second_combo_box = MagicMock(**{'currentData.return_value': None})
|
||||
self.media_item.on_second_combo_box_index_changed(0)
|
||||
|
||||
# THEN: The list_view should be cleared and the selected bible should be set as the current bible
|
||||
# THEN: The selected bible should be set as the current bible
|
||||
self.assertTrue(mocked_critical_error_message_box.called)
|
||||
self.media_item.list_view.clear.assert_called_once_with(override_lock=True)
|
||||
self.media_item.style_combo_box.setEnabled.assert_called_once_with(True)
|
||||
self.assertFalse(mocked_initialise_advanced_bible.called)
|
||||
self.assertEqual(self.media_item.second_bible, None)
|
||||
@ -1388,8 +1401,9 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
# WHEN: Calling on_search_timer_timeout
|
||||
self.media_item.on_search_timer_timeout()
|
||||
|
||||
# THEN: The text_search method should have been called with True
|
||||
mocked_text_search.assert_called_once_with(True)
|
||||
# THEN: The search_status should be set to SearchAsYouType and text_search should have been called
|
||||
self.assertEqual(self.media_item.search_status, SearchStatus().SearchAsYouType)
|
||||
mocked_text_search.assert_called_once_with()
|
||||
|
||||
def test_display_results_no_results(self):
|
||||
"""
|
||||
@ -1407,7 +1421,6 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.media_item.display_results()
|
||||
|
||||
# THEN: No items should be added to the list
|
||||
self.media_item.list_view.clear.assert_called_once_with()
|
||||
self.assertFalse(self.media_item.list_view.addItem.called)
|
||||
|
||||
def test_display_results_results(self):
|
||||
@ -1415,7 +1428,10 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
Test the display_results method when there are items to display
|
||||
"""
|
||||
# GIVEN: An instance of BibleMediaItem and a mocked build_display_results which returns a list of results
|
||||
with patch.object(self.media_item, 'build_display_results', return_value=['list', 'items']):
|
||||
with patch.object(self.media_item, 'build_display_results', return_value=[
|
||||
{'item_title': 'Title 1'}, {'item_title': 'Title 2'}]), \
|
||||
patch.object(self.media_item, 'add_built_results_to_list_widget') as \
|
||||
mocked_add_built_results_to_list_widget:
|
||||
self.media_item.search_results = ['results']
|
||||
self.media_item.list_view = MagicMock()
|
||||
|
||||
@ -1423,5 +1439,5 @@ class TestMediaItem(TestCase, TestMixin):
|
||||
self.media_item.display_results()
|
||||
|
||||
# THEN: addItem should have been with the display items
|
||||
self.media_item.list_view.clear.assert_called_once_with()
|
||||
self.assertEqual(self.media_item.list_view.addItem.call_args_list, [call('list'), call('items')])
|
||||
mocked_add_built_results_to_list_widget.assert_called_once_with(
|
||||
[{'item_title': 'Title 1'}, {'item_title': 'Title 2'}])
|
||||
|
@ -27,10 +27,15 @@ from unittest.mock import MagicMock
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from openlp.core.common import Settings
|
||||
from openlp.plugins.songs.forms.editverseform import EditVerseForm
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
__default_settings__ = {
|
||||
'songs/enable chords': True,
|
||||
}
|
||||
|
||||
|
||||
class TestEditVerseForm(TestCase, TestMixin):
|
||||
"""
|
||||
@ -40,9 +45,10 @@ class TestEditVerseForm(TestCase, TestMixin):
|
||||
"""
|
||||
Set up the components need for all tests.
|
||||
"""
|
||||
self.edit_verse_form = EditVerseForm(None)
|
||||
self.setup_application()
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.edit_verse_form = EditVerseForm(None)
|
||||
QtCore.QLocale.setDefault(QtCore.QLocale('en_GB'))
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -108,7 +108,7 @@ class TestBibleManager(TestCase, TestMixin):
|
||||
# WHEN asking to parse the bible reference
|
||||
results = parse_reference('Raoul 1', self.manager.db_cache['tests'], MagicMock())
|
||||
# THEN a verse array should be returned
|
||||
self.assertEqual(False, results, "The bible Search should return False")
|
||||
self.assertEqual([], results, "The bible Search should return an empty list")
|
||||
|
||||
def test_parse_reference_five(self):
|
||||
"""
|
||||
|
@ -27,12 +27,16 @@ from unittest.mock import MagicMock
|
||||
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.core.common.uistrings import UiStrings
|
||||
from openlp.plugins.songs.forms.editsongform import EditSongForm
|
||||
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
__default_settings__ = {
|
||||
'songs/enable chords': True,
|
||||
}
|
||||
|
||||
|
||||
class TestEditSongForm(TestCase, TestMixin):
|
||||
"""
|
||||
@ -48,12 +52,15 @@ class TestEditSongForm(TestCase, TestMixin):
|
||||
self.main_window = QtWidgets.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
Registry().register('theme_manager', MagicMock())
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.form = EditSongForm(MagicMock(), self.main_window, MagicMock())
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
self.destroy_settings()
|
||||
del self.form
|
||||
del self.main_window
|
||||
|
||||
|
@ -26,10 +26,14 @@ from unittest import TestCase
|
||||
|
||||
from PyQt5 import QtCore, QtTest, QtWidgets
|
||||
|
||||
from openlp.core.common import Registry
|
||||
from openlp.core.common import Registry, Settings
|
||||
from openlp.plugins.songs.forms.editverseform import EditVerseForm
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
__default_settings__ = {
|
||||
'songs/enable chords': True,
|
||||
}
|
||||
|
||||
|
||||
class TestEditVerseForm(TestCase, TestMixin):
|
||||
"""
|
||||
@ -44,12 +48,15 @@ class TestEditVerseForm(TestCase, TestMixin):
|
||||
self.setup_application()
|
||||
self.main_window = QtWidgets.QMainWindow()
|
||||
Registry().register('main_window', self.main_window)
|
||||
self.build_settings()
|
||||
Settings().extend_default_settings(__default_settings__)
|
||||
self.form = EditVerseForm()
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Delete all the C++ objects at the end so that we don't have a segfault
|
||||
"""
|
||||
self.destroy_settings()
|
||||
del self.form
|
||||
del self.main_window
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
"v1"
|
||||
],
|
||||
[
|
||||
"Did we in our own strength confide, our striving would be losing;\r\nWere not the right Man on our side, the Man of Godâs own choosing:\r\nDost ask who that may be? Christ Jesus, it is He;\r\nLord Sabaoth, His Name, from age to age the same,\r\nAnd He must win the battle.\r\n",
|
||||
"Did we in our own strength confide, our striving would be losing;\r\nWere not the right Man on our side, the Man of God’s own choosing:\r\nDost ask who that may be? Christ Jesus, it is He;\r\nLord Sabaoth, His Name, from age to age the same,\r\nAnd He must win the battle.\r\n",
|
||||
"v2"
|
||||
],
|
||||
[
|
||||
@ -23,7 +23,7 @@
|
||||
"v3"
|
||||
],
|
||||
[
|
||||
"That word above all earthly powers, no thanks to them, abideth;\r\nThe Spirit and the gifts are ours through Him Who with us sideth:\r\nLet goods and kindred go, this mortal life also;\r\nThe body they may kill: Godâs truth abideth still,\r\nHis kingdom is forever.\r\n",
|
||||
"That word above all earthly powers, no thanks to them, abideth;\r\nThe Spirit and the gifts are ours through Him Who with us sideth:\r\nLet goods and kindred go, this mortal life also;\r\nThe body they may kill: God’s truth abideth still,\r\nHis kingdom is forever.\r\n",
|
||||
"v4"
|
||||
]
|
||||
]
|
||||
|
Loading…
Reference in New Issue
Block a user