Fix up some stuff around the screen list

This commit is contained in:
Raoul Snyman 2017-12-01 17:35:21 -07:00
parent 64a245bef9
commit c7ea4c460e
10 changed files with 110 additions and 93 deletions

View File

@ -23,7 +23,6 @@
The :mod:`screen` module provides management functionality for a machines' The :mod:`screen` module provides management functionality for a machines'
displays. displays.
""" """
import json
import logging import logging
from PyQt5 import QtCore from PyQt5 import QtCore
@ -90,6 +89,30 @@ class ScreenList(object):
for screen in self.screens: for screen in self.screens:
yield screen yield screen
def __len__(self):
"""
Make sure we can call "len" on this object
"""
return len(self.screens)
@property
def current(self):
"""
Return the first "current" desktop
NOTE: This is a HACK to ease the upgrade process
"""
# Get the first display screen
for screen in self.screens:
if screen.is_display:
return screen
# If there's no display screen, get the first primary screen
for screen in self.screens:
if screen.is_primary:
return screen
# Otherwise just return the first screen
return self.screens[0]
@classmethod @classmethod
def create(cls, desktop): def create(cls, desktop):
""" """
@ -100,7 +123,7 @@ class ScreenList(object):
screen_list = cls() screen_list = cls()
screen_list.desktop = desktop screen_list.desktop = desktop
screen_list.screens = [] screen_list.screens = []
screen_list.screen_count_changed() screen_list.on_screen_count_changed()
screen_list.load_screen_settings() screen_list.load_screen_settings()
screen_list.desktop.resized.connect(screen_list.on_screen_resolution_changed) screen_list.desktop.resized.connect(screen_list.on_screen_resolution_changed)
screen_list.desktop.screenCountChanged.connect(screen_list.on_screen_count_changed) screen_list.desktop.screenCountChanged.connect(screen_list.on_screen_count_changed)
@ -138,7 +161,7 @@ class ScreenList(object):
# Add new screens. # Add new screens.
for number in range(self.desktop.screenCount()): for number in range(self.desktop.screenCount()):
if not self.has_screen(number): if not self.has_screen(number):
self.screens.append(Screen(number, self.desktop.getGeometry(number), self.screens.append(Screen(number, self.desktop.screenGeometry(number),
self.desktop.primaryScreen() == number)) self.desktop.primaryScreen() == number))
# We do not want to send this message at start up. # We do not want to send this message at start up.
if changed_screen is not None: if changed_screen is not None:
@ -201,7 +224,7 @@ class ScreenList(object):
'core/monitors': '{}' 'core/monitors': '{}'
} }
Settings.extend_default_settings(screen_settings) Settings.extend_default_settings(screen_settings)
monitors = json.loads(Settings().value('core/monitors')) monitors = Settings().value('core/monitors')
for screen in self.screens: for screen in self.screens:
monitor = monitors.get(screen.number) monitor = monitors.get(screen.number)
if monitor: if monitor:

View File

@ -176,8 +176,8 @@ class ImageManager(QtCore.QObject):
super(ImageManager, self).__init__() super(ImageManager, self).__init__()
Registry().register('image_manager', self) Registry().register('image_manager', self)
current_screen = ScreenList().current current_screen = ScreenList().current
self.width = current_screen['size'].width() self.width = current_screen.display_geometry.width()
self.height = current_screen['size'].height() self.height = current_screen.display_geometry.height()
self._cache = {} self._cache = {}
self.image_thread = ImageThread(self) self.image_thread = ImageThread(self)
self._conversion_queue = PriorityQueue() self._conversion_queue = PriorityQueue()
@ -190,8 +190,8 @@ class ImageManager(QtCore.QObject):
""" """
log.debug('update_display') log.debug('update_display')
current_screen = ScreenList().current current_screen = ScreenList().current
self.width = current_screen['size'].width() self.width = current_screen.display_geometry.width()
self.height = current_screen['size'].height() self.height = current_screen.display_geometry.height()
# Mark the images as dirty for a rebuild by setting the image and byte stream to None. # Mark the images as dirty for a rebuild by setting the image and byte stream to None.
for image in list(self._cache.values()): for image in list(self._cache.values()):
self._reset_image(image) self._reset_image(image)

View File

@ -48,7 +48,7 @@ class OpenLPDockWidget(QtWidgets.QDockWidget):
self.setWindowIcon(build_icon(icon)) self.setWindowIcon(build_icon(icon))
# Sort out the minimum width. # Sort out the minimum width.
screens = ScreenList() screens = ScreenList()
main_window_docbars = screens.current['size'].width() // 5 main_window_docbars = screens.current.display_geometry.width() // 5
if main_window_docbars > 300: if main_window_docbars > 300:
self.setMinimumWidth(300) self.setMinimumWidth(300)
else: else:

View File

@ -249,7 +249,7 @@ class PdfDocument(PresentationDocument):
self.image_files.append(image_path) self.image_files.append(image_path)
self.num_pages = len(self.image_files) self.num_pages = len(self.image_files)
return True return True
size = ScreenList().current['size'] size = ScreenList().current.display_geometry
# Generate images from PDF that will fit the frame. # Generate images from PDF that will fit the frame.
runlog = '' runlog = ''
try: try:

View File

@ -62,19 +62,26 @@ class TestScreenList(TestCase):
del self.screens del self.screens
del self.application del self.application
def test_add_desktop(self): def test_create_screen_list(self):
""" """
Test the ScreenList.screen_count_changed method to check if new monitors are detected by OpenLP. Create the screen list
""" """
# GIVEN: The screen list at its current size # GIVEN: Mocked desktop
old_screen_count = len(self.screens.screen_list) mocked_desktop = MagicMock()
mocked_desktop.screenCount.return_value = 2
mocked_desktop.screenGeometry.side_effect = [
QtCore.QRect(0, 0, 1024, 768),
QtCore.QRect(1024, 0, 1024, 768)
]
mocked_desktop.primaryScreen.return_value = 0
# WHEN: We add a new screen # WHEN: create() is called
self.desktop.screenCount.return_value = SCREEN['number'] + 1 screen_list = ScreenList.create(mocked_desktop)
self.screens.screen_count_changed(old_screen_count)
# THEN: The screen should have been added and the screens should be identical # THEN: The correct screens have been set up
new_screen_count = len(self.screens.screen_list) assert screen_list.screens[0].number == 0
self.assertEqual(old_screen_count + 1, new_screen_count, 'The new_screens list should be bigger') assert screen_list.screens[0].geometry == QtCore.QRect(0, 0, 1024, 768)
self.assertEqual(SCREEN, self.screens.screen_list.pop(), assert screen_list.screens[0].is_primary is True
'The 2nd screen should be identical to the first screen') assert screen_list.screens[1].number == 1
assert screen_list.screens[1].geometry == QtCore.QRect(1024, 0, 1024, 768)
assert screen_list.screens[1].is_primary is False

View File

@ -24,7 +24,7 @@ Package to test the openlp.core.ui.exeptionform package.
""" """
import os import os
import tempfile import tempfile
from collections import OrderedDict
from unittest import TestCase from unittest import TestCase
from unittest.mock import call, patch from unittest.mock import call, patch
@ -45,40 +45,44 @@ exceptionform.VLC_VERSION = 'VLC Test'
MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of the Exception. ---\n\n' MAIL_ITEM_TEXT = ('**OpenLP Bug Report**\nVersion: Trunk Test\n\n--- Details of the Exception. ---\n\n'
'Description Test\n\n --- Exception Traceback ---\nopenlp: Traceback Test\n' 'Description Test\n\n --- Exception Traceback ---\nopenlp: Traceback Test\n'
'--- System information ---\nPlatform: Nose Test\n\n--- Library Versions ---\n' '--- System information ---\nPlatform: Nose Test\n\n--- Library Versions ---\n'
'Python: Python Test\nQt5: Qt5 test\nPyQt5: PyQt5 Test\nQtWebkit: Webkit Test\n' 'Python: Python Test\nQt5: Qt5 Test\nPyQt5: PyQt5 Test\n'
'SQLAlchemy: SqlAlchemy Test\nSQLAlchemy Migrate: Migrate Test\nBeautifulSoup: BeautifulSoup Test\n' 'SQLAlchemy: SQLAlchemy Test\nAlembic: Alembic Test\nBeautifulSoup: BeautifulSoup Test\n'
'lxml: ETree Test\nChardet: CHARDET Test\nPyEnchant: Enchant Test\nMako: Mako Test\n' 'lxml: ETree Test\nChardet: Chardet Test\nPyEnchant: PyEnchant Test\nMako: Mako Test\n'
'pyICU: ICU Test\npyUNO bridge: UNO Bridge Test\nVLC: VLC Test\n\n') 'pyICU: pyICU Test\nVLC: VLC Test\nPyUNO: UNO Bridge Test\n')
LIBRARY_VERSIONS = OrderedDict([
('Python', 'Python Test'),
('Qt5', 'Qt5 Test'),
('PyQt5', 'PyQt5 Test'),
('SQLAlchemy', 'SQLAlchemy Test'),
('Alembic', 'Alembic Test'),
('BeautifulSoup', 'BeautifulSoup Test'),
('lxml', 'ETree Test'),
('Chardet', 'Chardet Test'),
('PyEnchant', 'PyEnchant Test'),
('Mako', 'Mako Test'),
('pyICU', 'pyICU Test'),
('VLC', 'VLC Test')
])
@patch("openlp.core.ui.exceptionform.Qt.qVersion") @patch('openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl')
@patch("openlp.core.ui.exceptionform.QtGui.QDesktopServices.openUrl") @patch('openlp.core.ui.exceptionform.get_version')
@patch("openlp.core.ui.exceptionform.get_version") @patch('openlp.core.ui.exceptionform.get_library_versions')
@patch("openlp.core.ui.exceptionform.sqlalchemy") @patch('openlp.core.ui.exceptionform.is_linux')
@patch("openlp.core.ui.exceptionform.bs4") @patch('openlp.core.ui.exceptionform.platform.platform')
@patch("openlp.core.ui.exceptionform.etree")
@patch("openlp.core.ui.exceptionform.is_linux")
@patch("openlp.core.ui.exceptionform.platform.platform")
@patch("openlp.core.ui.exceptionform.platform.python_version")
class TestExceptionForm(TestMixin, TestCase): class TestExceptionForm(TestMixin, TestCase):
""" """
Test functionality of exception form functions Test functionality of exception form functions
""" """
def __method_template_for_class_patches(self, __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__, def __method_template_for_class_patches(self, __PLACEHOLDER_FOR_LOCAL_METHOD_PATCH_DECORATORS_GO_HERE__,
mocked_python_version, mocked_platform, mocked_is_linux, mocked_platform, mocked_is_linux, mocked_get_library_versions,
mocked_etree, mocked_bs4, mocked_sqlalchemy, mocked_get_version, mocked_get_version, mocked_openlurl):
mocked_openlurl, mocked_qversion):
""" """
Template so you don't have to remember the layout of class mock options for methods Template so you don't have to remember the layout of class mock options for methods
""" """
mocked_etree.__version__ = 'ETree Test'
mocked_bs4.__version__ = 'BeautifulSoup Test'
mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
mocked_python_version.return_value = 'Python Test'
mocked_platform.return_value = 'Nose Test'
mocked_qversion.return_value = 'Qt5 test'
mocked_is_linux.return_value = False mocked_is_linux.return_value = False
mocked_get_version.return_value = 'Trunk Test' mocked_get_version.return_value = 'Trunk Test'
mocked_get_library_versions.return_value = LIBRARY_VERSIONS
def setUp(self): def setUp(self):
self.setup_application() self.setup_application()
@ -98,31 +102,21 @@ class TestExceptionForm(TestMixin, TestCase):
@patch("openlp.core.ui.exceptionform.FileDialog") @patch("openlp.core.ui.exceptionform.FileDialog")
@patch("openlp.core.ui.exceptionform.QtCore.QUrl") @patch("openlp.core.ui.exceptionform.QtCore.QUrl")
@patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem") @patch("openlp.core.ui.exceptionform.QtCore.QUrlQuery.addQueryItem")
@patch("openlp.core.ui.exceptionform.Qt") def test_on_send_report_button_clicked(self, mocked_add_query_item, mocked_qurl, mocked_file_dialog,
def test_on_send_report_button_clicked(self, mocked_qt, mocked_add_query_item, mocked_qurl, mocked_file_dialog, mocked_ui_exception_dialog, mocked_platform, mocked_is_linux,
mocked_ui_exception_dialog, mocked_python_version, mocked_platform, mocked_get_library_versions, mocked_get_version, mocked_openlurl):
mocked_is_linux, mocked_etree, mocked_bs4, mocked_sqlalchemy,
mocked_get_version, mocked_openlurl, mocked_qversion):
""" """
Test send report creates the proper system information text Test send report creates the proper system information text
""" """
# GIVEN: Test environment # GIVEN: Test environment
mocked_etree.__version__ = 'ETree Test'
mocked_bs4.__version__ = 'BeautifulSoup Test'
mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
mocked_python_version.return_value = 'Python Test'
mocked_platform.return_value = 'Nose Test' mocked_platform.return_value = 'Nose Test'
mocked_qversion.return_value = 'Qt5 test'
mocked_is_linux.return_value = False mocked_is_linux.return_value = False
mocked_get_version.return_value = 'Trunk Test' mocked_get_version.return_value = 'Trunk Test'
mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test' mocked_get_library_versions.return_value = LIBRARY_VERSIONS
mocked_is_linux.return_value = False
mocked_get_version.return_value = 'Trunk Test'
test_form = exceptionform.ExceptionForm() test_form = exceptionform.ExceptionForm()
test_form.file_attachment = None test_form.file_attachment = None
with patch.object(test_form, '_pyuno_import') as mock_pyuno, \ with patch.object(test_form, '_get_pyuno_version') as mock_pyuno, \
patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \ patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
mock_pyuno.return_value = 'UNO Bridge Test' mock_pyuno.return_value = 'UNO Bridge Test'
@ -136,24 +130,15 @@ class TestExceptionForm(TestMixin, TestCase):
mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT) mocked_add_query_item.assert_called_with('body', MAIL_ITEM_TEXT)
@patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName") @patch("openlp.core.ui.exceptionform.FileDialog.getSaveFileName")
@patch("openlp.core.ui.exceptionform.Qt") def test_on_save_report_button_clicked(self, mocked_save_filename, mocked_platform, mocked_is_linux,
def test_on_save_report_button_clicked(self, mocked_qt, mocked_save_filename, mocked_python_version, mocked_get_library_versions, mocked_get_version, mocked_openlurl):
mocked_platform, mocked_is_linux, mocked_etree, mocked_bs4,
mocked_sqlalchemy, mocked_get_version, mocked_openlurl,
mocked_qversion):
""" """
Test save report saves the correct information to a file Test save report saves the correct information to a file
""" """
mocked_etree.__version__ = 'ETree Test'
mocked_bs4.__version__ = 'BeautifulSoup Test'
mocked_sqlalchemy.__version__ = 'SqlAlchemy Test'
mocked_python_version.return_value = 'Python Test'
mocked_platform.return_value = 'Nose Test' mocked_platform.return_value = 'Nose Test'
mocked_qversion.return_value = 'Qt5 test'
mocked_qt.PYQT_VERSION_STR = 'PyQt5 Test'
mocked_is_linux.return_value = False mocked_is_linux.return_value = False
mocked_get_version.return_value = 'Trunk Test' mocked_get_version.return_value = 'Trunk Test'
mocked_get_library_versions.return_value = LIBRARY_VERSIONS
with patch.object(Path, 'open') as mocked_path_open: with patch.object(Path, 'open') as mocked_path_open:
test_path = Path('testfile.txt') test_path = Path('testfile.txt')
mocked_save_filename.return_value = test_path, 'ext' mocked_save_filename.return_value = test_path, 'ext'
@ -161,7 +146,7 @@ class TestExceptionForm(TestMixin, TestCase):
test_form = exceptionform.ExceptionForm() test_form = exceptionform.ExceptionForm()
test_form.file_attachment = None test_form.file_attachment = None
with patch.object(test_form, '_pyuno_import') as mock_pyuno, \ with patch.object(test_form, '_get_pyuno_version') as mock_pyuno, \
patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \ patch.object(test_form.exception_text_edit, 'toPlainText') as mock_traceback, \
patch.object(test_form.description_text_edit, 'toPlainText') as mock_description: patch.object(test_form.description_text_edit, 'toPlainText') as mock_description:
mock_pyuno.return_value = 'UNO Bridge Test' mock_pyuno.return_value = 'UNO Bridge Test'

View File

@ -67,8 +67,11 @@ class TestMainWindow(TestCase, TestMixin):
self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action') self.add_toolbar_action_patcher = patch('openlp.core.ui.mainwindow.create_action')
self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start() self.mocked_add_toolbar_action = self.add_toolbar_action_patcher.start()
self.mocked_add_toolbar_action.side_effect = self._create_mock_action self.mocked_add_toolbar_action.side_effect = self._create_mock_action
with patch('openlp.core.display.screens.ScreenList.__instance__', spec=ScreenList) as mocked_screen_list: mocked_desktop = MagicMock()
mocked_screen_list.current = {'number': 0, 'size': QtCore.QSize(600, 800), 'primary': True} mocked_desktop.screenCount.return_value = 1
mocked_desktop.screenGeometry.return_value = QtCore.QRect(0, 0, 1024, 768)
mocked_desktop.primaryScreen.return_value = 1
ScreenList.create(mocked_desktop)
self.main_window = MainWindow() self.main_window = MainWindow()
def tearDown(self): def tearDown(self):

View File

@ -23,7 +23,6 @@
This module contains tests for the PdfController This module contains tests for the PdfController
""" """
import os import os
import shutil
from tempfile import mkdtemp from tempfile import mkdtemp
from unittest import TestCase, SkipTest from unittest import TestCase, SkipTest
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch

View File

@ -108,7 +108,7 @@ class EasyWorshipSongImportLogger(EasyWorshipSongImport):
self._title_assignment_list.append(title) self._title_assignment_list.append(title)
class TestFieldDesc: class FakeFieldDesc:
def __init__(self, name, field_type, size): def __init__(self, name, field_type, size):
self.name = name self.name = name
self.field_type = field_type self.field_type = field_type
@ -120,11 +120,11 @@ CODE_PAGE_MAPPINGS = [
(852, 'cp1250'), (737, 'cp1253'), (775, 'cp1257'), (855, 'cp1251'), (857, 'cp1254'), (852, 'cp1250'), (737, 'cp1253'), (775, 'cp1257'), (855, 'cp1251'), (857, 'cp1254'),
(866, 'cp1251'), (869, 'cp1253'), (862, 'cp1255'), (874, 'cp874')] (866, 'cp1251'), (869, 'cp1253'), (862, 'cp1255'), (874, 'cp874')]
TEST_FIELD_DESCS = [ TEST_FIELD_DESCS = [
TestFieldDesc('Title', FieldType.String, 50), FakeFieldDesc('Title', FieldType.String, 50),
TestFieldDesc('Text Percentage Bottom', FieldType.Int16, 2), TestFieldDesc('RecID', FieldType.Int32, 4), FakeFieldDesc('Text Percentage Bottom', FieldType.Int16, 2), FakeFieldDesc('RecID', FieldType.Int32, 4),
TestFieldDesc('Default Background', FieldType.Logical, 1), TestFieldDesc('Words', FieldType.Memo, 250), FakeFieldDesc('Default Background', FieldType.Logical, 1), FakeFieldDesc('Words', FieldType.Memo, 250),
TestFieldDesc('Words', FieldType.Memo, 250), TestFieldDesc('BK Bitmap', FieldType.Blob, 10), FakeFieldDesc('Words', FieldType.Memo, 250), FakeFieldDesc('BK Bitmap', FieldType.Blob, 10),
TestFieldDesc('Last Modified', FieldType.Timestamp, 10)] FakeFieldDesc('Last Modified', FieldType.Timestamp, 10)]
TEST_FIELDS = [ TEST_FIELDS = [
b'A Heart Like Thine\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', 32868, 2147483750, b'A Heart Like Thine\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0', 32868, 2147483750,
129, b'{\\rtf1\\ansi\\deff0\\deftab254{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}{\\f1\\fnil\\fcharset0 Verdana;}}' 129, b'{\\rtf1\\ansi\\deff0\\deftab254{\\fonttbl{\\f0\\fnil\\fcharset0 Arial;}{\\f1\\fnil\\fcharset0 Verdana;}}'

View File

@ -34,9 +34,9 @@ except ImportError:
CAN_RUN_TESTS = False CAN_RUN_TESTS = False
class TestRecord(object): class FakeRecord(object):
""" """
Microsoft Access Driver is not available on non Microsoft Systems for this reason the :class:`TestRecord` is used Microsoft Access Driver is not available on non Microsoft Systems for this reason the :class:`FakeRecord` is used
to simulate a recordset that would be returned by pyobdc. to simulate a recordset that would be returned by pyobdc.
""" """
def __init__(self, id_, field, value): def __init__(self, id_, field, value):
@ -66,12 +66,12 @@ if CAN_RUN_TESTS:
self._title_assignment_list.append(title) self._title_assignment_list.append(title)
RECORDSET_TEST_DATA = [TestRecord(1, 'TITLE', 'Amazing Grace'), RECORDSET_TEST_DATA = [FakeRecord(1, 'TITLE', 'Amazing Grace'),
TestRecord(1, 'AUTHOR', 'John Newton'), FakeRecord(1, 'AUTHOR', 'John Newton'),
TestRecord(1, 'CCLISONGID', '12345'), FakeRecord(1, 'CCLISONGID', '12345'),
TestRecord(1, 'COMMENTS', 'The original version'), FakeRecord(1, 'COMMENTS', 'The original version'),
TestRecord(1, 'COPY', 'Public Domain'), FakeRecord(1, 'COPY', 'Public Domain'),
TestRecord( FakeRecord(
1, 'LYRICS', 1, 'LYRICS',
'Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;' 'Amazing grace! How&crlf;sweet the sound&crlf;That saved a wretch like me!&crlf;'
'I once was lost,&crlf;but now am found;&crlf;Was blind, but now I see.&crlf;&crlf;' 'I once was lost,&crlf;but now am found;&crlf;Was blind, but now I see.&crlf;&crlf;'
@ -88,8 +88,8 @@ RECORDSET_TEST_DATA = [TestRecord(1, 'TITLE', 'Amazing Grace'),
'Shall be forever mine.&crlf;&crlf;When we\'ve been there&crlf;ten thousand years,&crlf;' 'Shall be forever mine.&crlf;&crlf;When we\'ve been there&crlf;ten thousand years,&crlf;'
'Bright shining as the sun,&crlf;We\'ve no less days to&crlf;sing God\'s praise&crlf;' 'Bright shining as the sun,&crlf;We\'ve no less days to&crlf;sing God\'s praise&crlf;'
'Than when we\'d first begun.&crlf;&crlf;'), 'Than when we\'d first begun.&crlf;&crlf;'),
TestRecord(2, 'TITLE', 'Beautiful Garden Of Prayer, The'), FakeRecord(2, 'TITLE', 'Beautiful Garden Of Prayer, The'),
TestRecord( FakeRecord(
2, 'LYRICS', 2, 'LYRICS',
'There\'s a garden where&crlf;Jesus is waiting,&crlf;' 'There\'s a garden where&crlf;Jesus is waiting,&crlf;'
'There\'s a place that&crlf;is wondrously fair,&crlf;For it glows with the&crlf;' 'There\'s a place that&crlf;is wondrously fair,&crlf;For it glows with the&crlf;'