forked from openlp/openlp
Merge trunk
This commit is contained in:
commit
0d4541b8dc
@ -1 +1 @@
|
||||
2.1.6
|
||||
2.2
|
||||
|
@ -30,7 +30,7 @@ logging and a plugin framework are contained within the openlp.core module.
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from optparse import OptionParser
|
||||
import argparse
|
||||
from traceback import format_exception
|
||||
import shutil
|
||||
import time
|
||||
@ -282,17 +282,18 @@ def parse_options(args):
|
||||
:return: a tuple of parsed options of type optparse.Value and a list of remaining argsZ
|
||||
"""
|
||||
# Set up command line options.
|
||||
usage = 'Usage: %prog [options] [qt-options]'
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option('-e', '--no-error-form', dest='no_error_form', action='store_true',
|
||||
help='Disable the error notification form.')
|
||||
parser.add_option('-l', '--log-level', dest='loglevel', default='warning', metavar='LEVEL',
|
||||
help='Set logging to LEVEL level. Valid values are "debug", "info", "warning".')
|
||||
parser.add_option('-p', '--portable', dest='portable', action='store_true',
|
||||
help='Specify if this should be run as a portable app, off a USB flash drive (not implemented).')
|
||||
parser.add_option('-d', '--dev-version', dest='dev_version', action='store_true',
|
||||
help='Ignore the version file and pull the version directly from Bazaar')
|
||||
parser.add_option('-s', '--style', dest='style', help='Set the Qt4 style (passed directly to Qt4).')
|
||||
parser = argparse.ArgumentParser(prog='openlp.py')
|
||||
parser.add_argument('-e', '--no-error-form', dest='no_error_form', action='store_true',
|
||||
help='Disable the error notification form.')
|
||||
parser.add_argument('-l', '--log-level', dest='loglevel', default='warning', metavar='LEVEL',
|
||||
help='Set logging to LEVEL level. Valid values are "debug", "info", "warning".')
|
||||
parser.add_argument('-p', '--portable', dest='portable', action='store_true',
|
||||
help='Specify if this should be run as a portable app, '
|
||||
'off a USB flash drive (not implemented).')
|
||||
parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
|
||||
help='Ignore the version file and pull the version directly from Bazaar')
|
||||
parser.add_argument('-s', '--style', dest='style', help='Set the Qt4 style (passed directly to Qt4).')
|
||||
parser.add_argument('rargs', nargs='?', default=[])
|
||||
# Parse command line options and deal with them. Use args supplied pragmatically if possible.
|
||||
return parser.parse_args(args) if args else parser.parse_args()
|
||||
|
||||
@ -318,18 +319,18 @@ def main(args=None):
|
||||
|
||||
:param args: Some args
|
||||
"""
|
||||
(options, args) = parse_options(args)
|
||||
args = parse_options(args)
|
||||
qt_args = []
|
||||
if options.loglevel.lower() in ['d', 'debug']:
|
||||
if args and args.loglevel.lower() in ['d', 'debug']:
|
||||
log.setLevel(logging.DEBUG)
|
||||
elif options.loglevel.lower() in ['w', 'warning']:
|
||||
elif args and args.loglevel.lower() in ['w', 'warning']:
|
||||
log.setLevel(logging.WARNING)
|
||||
else:
|
||||
log.setLevel(logging.INFO)
|
||||
if options.style:
|
||||
qt_args.extend(['-style', options.style])
|
||||
if args and args.style:
|
||||
qt_args.extend(['-style', args.style])
|
||||
# Throw the rest of the arguments at Qt, just in case.
|
||||
qt_args.extend(args)
|
||||
qt_args.extend(args.rargs)
|
||||
# Bug #1018855: Set the WM_CLASS property in X11
|
||||
if not is_win() and not is_macosx():
|
||||
qt_args.append('OpenLP')
|
||||
@ -339,7 +340,7 @@ def main(args=None):
|
||||
application = OpenLP(qt_args)
|
||||
application.setOrganizationName('OpenLP')
|
||||
application.setOrganizationDomain('openlp.org')
|
||||
if options.portable:
|
||||
if args and args.portable:
|
||||
application.setApplicationName('OpenLPPortable')
|
||||
Settings.setDefaultFormat(Settings.IniFormat)
|
||||
# Get location OpenLPPortable.ini
|
||||
@ -383,6 +384,6 @@ def main(args=None):
|
||||
application.installTranslator(default_translator)
|
||||
else:
|
||||
log.debug('Could not find default_translator.')
|
||||
if not options.no_error_form:
|
||||
if args and not args.no_error_form:
|
||||
sys.excepthook = application.hook_exception
|
||||
sys.exit(application.run(qt_args))
|
||||
|
@ -99,7 +99,7 @@ class AppLocation(object):
|
||||
Get a list of files from the data files path.
|
||||
|
||||
:param section: Defaults to *None*. The section of code getting the files - used to load from a section's
|
||||
data subdirectory.
|
||||
data subdirectory.
|
||||
:param extension:
|
||||
Defaults to *None*. The extension to search for. For example::
|
||||
|
||||
|
@ -65,9 +65,9 @@ class Settings(QtCore.QSettings):
|
||||
|
||||
* Exposes all the methods of QSettings.
|
||||
* Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to
|
||||
``IniFormat``, and the path to the Ini file is set using ``set_filename``,
|
||||
then the Settings constructor (without any arguments) will create a Settings
|
||||
object for accessing settings stored in that Ini file.
|
||||
``IniFormat``, and the path to the Ini file is set using ``set_filename``,
|
||||
then the Settings constructor (without any arguments) will create a Settings
|
||||
object for accessing settings stored in that Ini file.
|
||||
|
||||
``__default_settings__``
|
||||
This dict contains all core settings with their default values.
|
||||
@ -118,6 +118,7 @@ class Settings(QtCore.QSettings):
|
||||
'advanced/slide limits': SlideLimits.End,
|
||||
'advanced/single click preview': False,
|
||||
'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
|
||||
'advanced/search as type': True,
|
||||
'crashreport/last directory': '',
|
||||
'formattingTags/html_tags': '',
|
||||
'core/audio repeat list': False,
|
||||
@ -321,48 +322,10 @@ class Settings(QtCore.QSettings):
|
||||
}
|
||||
__file_path__ = ''
|
||||
__obsolete_settings__ = [
|
||||
# Changed during 1.9.x development.
|
||||
('bibles/bookname language', 'bibles/book name language', []),
|
||||
('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)]),
|
||||
('songs/ccli number', 'core/ccli number', []),
|
||||
('media/use phonon', '', []),
|
||||
# Changed during 2.1.x development.
|
||||
('advanced/stylesheet fix', '', []),
|
||||
('bibles/last directory 1', 'bibles/last directory import', []),
|
||||
('media/background color', 'players/background color', []),
|
||||
('themes/last directory', 'themes/last directory import', []),
|
||||
('themes/last directory 1', 'themes/last directory export', []),
|
||||
('songs/last directory 1', 'songs/last directory import', []),
|
||||
('songusage/last directory 1', 'songusage/last directory export', []),
|
||||
('user interface/mainwindow splitter geometry', 'user interface/main window splitter geometry', []),
|
||||
('shortcuts/makeLive', 'shortcuts/make_live', []),
|
||||
('general/audio repeat list', 'core/audio repeat list', []),
|
||||
('general/auto open', 'core/auto open', []),
|
||||
('general/auto preview', 'core/auto preview', []),
|
||||
('general/audio start paused', 'core/audio start paused', []),
|
||||
('general/auto unblank', 'core/auto unblank', []),
|
||||
('general/blank warning', 'core/blank warning', []),
|
||||
('general/ccli number', 'core/ccli number', []),
|
||||
('general/has run wizard', 'core/has run wizard', []),
|
||||
('general/language', 'core/language', []),
|
||||
('general/last version test', 'core/last version test', []),
|
||||
('general/loop delay', 'core/loop delay', []),
|
||||
('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
|
||||
('general/save prompt', 'core/save prompt', []),
|
||||
('general/screen blank', 'core/screen blank', []),
|
||||
('general/show splash', 'core/show splash', []),
|
||||
('general/songselect password', 'core/songselect password', []),
|
||||
('general/songselect username', 'core/songselect username', []),
|
||||
('general/update check', 'core/update check', []),
|
||||
('general/view mode', 'core/view mode', []),
|
||||
('general/display on monitor', 'core/display on monitor', []),
|
||||
('general/override position', 'core/override position', []),
|
||||
('general/x position', 'core/x position', []),
|
||||
('general/y position', 'core/y position', []),
|
||||
('general/monitor', 'core/monitor', []),
|
||||
('general/height', 'core/height', []),
|
||||
('general/monitor', 'core/monitor', []),
|
||||
('general/width', 'core/width', [])
|
||||
# Changed during 2.2.x development.
|
||||
# ('advanced/stylesheet fix', '', []),
|
||||
# ('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
|
||||
('songs/search as type', 'advanced/search as type', [])
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
|
@ -108,8 +108,9 @@ class UiStrings(object):
|
||||
self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural')
|
||||
self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular')
|
||||
self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural')
|
||||
self.OLPV2 = translate('OpenLP.Ui', 'OpenLP 2')
|
||||
self.OLPV2x = translate('OpenLP.Ui', 'OpenLP 2.2')
|
||||
self.OLP = translate('OpenLP.Ui', 'OpenLP')
|
||||
self.OLPV2 = "%s %s" % (self.OLP, "2")
|
||||
self.OLPV2x = "%s %s" % (self.OLP, "2.4")
|
||||
self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?')
|
||||
self.OpenService = translate('OpenLP.Ui', 'Open service.')
|
||||
self.PlaySlidesInLoop = translate('OpenLP.Ui', 'Play Slides in Loop')
|
||||
|
@ -83,7 +83,7 @@ def get_text_file_string(text_file):
|
||||
None.
|
||||
|
||||
:param text_file: The name of the file.
|
||||
:return The file as a single string
|
||||
:return: The file as a single string
|
||||
"""
|
||||
if not os.path.isfile(text_file):
|
||||
return False
|
||||
@ -108,7 +108,7 @@ def str_to_bool(string_value):
|
||||
Convert a string version of a boolean into a real boolean.
|
||||
|
||||
:param string_value: The string value to examine and convert to a boolean type.
|
||||
:return The correct boolean value
|
||||
:return: The correct boolean value
|
||||
"""
|
||||
if isinstance(string_value, bool):
|
||||
return string_value
|
||||
@ -123,7 +123,7 @@ def build_icon(icon):
|
||||
:param icon:
|
||||
The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file
|
||||
location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string.
|
||||
:return The build icon.
|
||||
:return: The build icon.
|
||||
"""
|
||||
button_icon = QtGui.QIcon()
|
||||
if isinstance(icon, QtGui.QIcon):
|
||||
@ -168,7 +168,7 @@ def create_thumb(image_path, thumb_path, return_icon=True, size=None):
|
||||
:param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
|
||||
:param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
|
||||
height of 88 is used.
|
||||
:return The final icon.
|
||||
:return: The final icon.
|
||||
"""
|
||||
ext = os.path.splitext(thumb_path)[1].lower()
|
||||
reader = QtGui.QImageReader(image_path)
|
||||
@ -194,7 +194,7 @@ def validate_thumb(file_path, thumb_path):
|
||||
|
||||
:param file_path: The path to the file. The file **must** exist!
|
||||
:param thumb_path: The path to the thumb.
|
||||
:return True, False if the image has changed since the thumb was created.
|
||||
:return: True, False if the image has changed since the thumb was created.
|
||||
"""
|
||||
if not os.path.exists(thumb_path):
|
||||
return False
|
||||
|
@ -164,29 +164,29 @@ class FormattingTags(object):
|
||||
Add a list of tags to the list.
|
||||
|
||||
:param tags: The list with tags to add.
|
||||
Each **tag** has to be a ``dict`` and should have the following keys:
|
||||
Each **tag** has to be a ``dict`` and should have the following keys:
|
||||
|
||||
* desc
|
||||
The formatting tag's description, e. g. **Red**
|
||||
* desc
|
||||
The formatting tag's description, e. g. **Red**
|
||||
|
||||
* start tag
|
||||
The start tag, e. g. ``{r}``
|
||||
* start tag
|
||||
The start tag, e. g. ``{r}``
|
||||
|
||||
* end tag
|
||||
The end tag, e. g. ``{/r}``
|
||||
* end tag
|
||||
The end tag, e. g. ``{/r}``
|
||||
|
||||
* start html
|
||||
The start html tag. For instance ``<span style="-webkit-text-fill-color:red">``
|
||||
* start html
|
||||
The start html tag. For instance ``<span style="-webkit-text-fill-color:red">``
|
||||
|
||||
* end html
|
||||
The end html tag. For example ``</span>``
|
||||
* end html
|
||||
The end html tag. For example ``</span>``
|
||||
|
||||
* protected
|
||||
A boolean stating whether this is a build-in tag or not. Should be ``True`` in most cases.
|
||||
* protected
|
||||
A boolean stating whether this is a build-in tag or not. Should be ``True`` in most cases.
|
||||
|
||||
* temporary
|
||||
A temporary tag will not be saved, but is also considered when displaying text containing the tag. It has
|
||||
to be a ``boolean``.
|
||||
* temporary
|
||||
A temporary tag will not be saved, but is also considered when displaying text containing the tag. It
|
||||
has to be a ``boolean``.
|
||||
"""
|
||||
FormattingTags.html_expands.extend(tags)
|
||||
|
||||
|
@ -288,13 +288,7 @@ class Plugin(QtCore.QObject, RegistryProperties):
|
||||
"""
|
||||
Perform tasks on application startup
|
||||
"""
|
||||
# FIXME: Remove after 2.2 release.
|
||||
# This is needed to load the list of media/presentation from the config saved before the settings rewrite.
|
||||
if self.media_item_class is not None and self.name != 'images':
|
||||
loaded_list = Settings().get_files_from_config(self)
|
||||
# Now save the list to the config using our Settings class.
|
||||
if loaded_list:
|
||||
Settings().setValue('%s/%s files' % (self.settings_section, self.name), loaded_list)
|
||||
pass
|
||||
|
||||
def uses_theme(self, theme):
|
||||
"""
|
||||
|
@ -25,7 +25,9 @@
|
||||
|
||||
See PJLink Class 1 Specifications for details.
|
||||
http://pjlink.jbmia.or.jp/english/dl.html
|
||||
|
||||
Section 5-1 PJLink Specifications
|
||||
|
||||
Section 5-5 Guidelines for Input Terminals
|
||||
|
||||
NOTE:
|
||||
|
@ -89,9 +89,9 @@ class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
|
||||
|
||||
:param theme_name: The current theme name.
|
||||
:param old_theme_name: The old theme name. Has only to be passed, when the theme has been renamed.
|
||||
Defaults to *None*.
|
||||
Defaults to *None*.
|
||||
:param only_delete: Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be
|
||||
used when a theme is permanently deleted.
|
||||
used when a theme is permanently deleted.
|
||||
"""
|
||||
if old_theme_name is not None and old_theme_name in self._theme_dimensions:
|
||||
del self._theme_dimensions[old_theme_name]
|
||||
|
@ -142,7 +142,9 @@ class ScreenList(object):
|
||||
"""
|
||||
Add a screen to the list of known screens.
|
||||
|
||||
:param screen: A dict with the screen properties::
|
||||
:param screen: A dict with the screen properties:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
'primary': True,
|
||||
|
@ -114,17 +114,17 @@ class SearchEdit(QtGui.QLineEdit):
|
||||
default.
|
||||
|
||||
:param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon
|
||||
instance or string) and a title for the item in the menu. In short, they should look like this::
|
||||
instance or string) and a title for the item in the menu. In short, they should look like this::
|
||||
|
||||
(<identifier>, <icon>, <title>, <place holder text>)
|
||||
(<identifier>, <icon>, <title>, <place holder text>)
|
||||
|
||||
For instance::
|
||||
For instance::
|
||||
|
||||
(1, <QIcon instance>, "Titles", "Search Song Titles...")
|
||||
(1, <QIcon instance>, "Titles", "Search Song Titles...")
|
||||
|
||||
Or::
|
||||
Or::
|
||||
|
||||
(2, ":/songs/authors.png", "Authors", "Search Authors...")
|
||||
(2, ":/songs/authors.png", "Authors", "Search Authors...")
|
||||
"""
|
||||
menu = QtGui.QMenu(self)
|
||||
first = None
|
||||
|
@ -129,7 +129,7 @@ class ItemCapabilities(object):
|
||||
OnLoadUpdate = 8
|
||||
AddIfNewItem = 9
|
||||
ProvidesOwnDisplay = 10
|
||||
HasDetailedTitleDisplay = 11
|
||||
# HasDetailedTitleDisplay = 11
|
||||
HasVariableStartTime = 12
|
||||
CanSoftBreak = 13
|
||||
CanWordSplit = 14
|
||||
@ -388,7 +388,7 @@ class ServiceItem(RegistryProperties):
|
||||
|
||||
:param service_item: The item to extract data from.
|
||||
:param path: Defaults to *None*. This is the service manager path for things which have their files saved
|
||||
with them or None when the saved service is lite and the original file paths need to be preserved.
|
||||
with them or None when the saved service is lite and the original file paths need to be preserved.
|
||||
"""
|
||||
log.debug('set_from_service called with path %s' % path)
|
||||
header = service_item['serviceitem']['header']
|
||||
@ -415,11 +415,6 @@ class ServiceItem(RegistryProperties):
|
||||
self.will_auto_start = header.get('will_auto_start', False)
|
||||
self.processor = header.get('processor', None)
|
||||
self.has_original_files = True
|
||||
# TODO: Remove me in 2,3 build phase
|
||||
if self.is_capable(ItemCapabilities.HasDetailedTitleDisplay):
|
||||
self.capabilities.remove(ItemCapabilities.HasDetailedTitleDisplay)
|
||||
self.processor = self.title
|
||||
self.title = None
|
||||
if 'background_audio' in header:
|
||||
self.background_audio = []
|
||||
for filename in header['background_audio']:
|
||||
|
@ -66,9 +66,9 @@ def create_button_box(dialog, name, standard_buttons, custom_buttons=None):
|
||||
:param dialog: The parent object. This has to be a ``QDialog`` descendant.
|
||||
:param name: A string which is set as object name.
|
||||
:param standard_buttons: A list of strings for the used buttons. It might contain: ``ok``, ``save``, ``cancel``,
|
||||
``close``, and ``defaults``.
|
||||
``close``, and ``defaults``.
|
||||
:param custom_buttons: A list of additional buttons. If an item is an instance of QtGui.QAbstractButton it is added
|
||||
with QDialogButtonBox.ActionRole. Otherwise the item has to be a tuple of a Button and a ButtonRole.
|
||||
with QDialogButtonBox.ActionRole. Otherwise the item has to be a tuple of a Button and a ButtonRole.
|
||||
"""
|
||||
if custom_buttons is None:
|
||||
custom_buttons = []
|
||||
|
@ -154,114 +154,147 @@ class UiAboutDialog(object):
|
||||
'zh_CN': [' "executor" ']
|
||||
}
|
||||
documentors = ['Wesley "wrst" Stout', 'John "jseagull1" Cegalis (lead)']
|
||||
project_lead = translate('OpenLP.AboutForm', 'Project Lead')
|
||||
devs = translate('OpenLP.AboutForm', 'Developers')
|
||||
cons = translate('OpenLP.AboutForm', 'Contributors')
|
||||
packs = translate('OpenLP.AboutForm', 'Packagers')
|
||||
tests = translate('OpenLP.AboutForm', 'Testers')
|
||||
laters = translate('OpenLP.AboutForm', 'Translators')
|
||||
af = translate('OpenLP.AboutForm', 'Afrikaans (af)')
|
||||
cs = translate('OpenLP.AboutForm', 'Czech (cs)')
|
||||
da = translate('OpenLP.AboutForm', 'Danish (da)')
|
||||
de = translate('OpenLP.AboutForm', 'German (de)')
|
||||
el = translate('OpenLP.AboutForm', 'Greek (el)')
|
||||
gb = translate('OpenLP.AboutForm', 'English, United Kingdom (en_GB)')
|
||||
enza = translate('OpenLP.AboutForm', 'English, South Africa (en_ZA)')
|
||||
es = translate('OpenLP.AboutForm', 'Spanish (es)')
|
||||
et = translate('OpenLP.AboutForm', 'Estonian (et)')
|
||||
fi = translate('OpenLP.AboutForm', 'Finnish (fi)')
|
||||
fr = translate('OpenLP.AboutForm', 'French (fr)')
|
||||
hu = translate('OpenLP.AboutForm', 'Hungarian (hu)')
|
||||
ind = translate('OpenLP.AboutForm', 'Indonesian (id)')
|
||||
ja = translate('OpenLP.AboutForm', 'Japanese (ja)')
|
||||
nb = translate('OpenLP.AboutForm', 'Norwegian Bokm\xe5l (nb)')
|
||||
nl = translate('OpenLP.AboutForm', 'Dutch (nl)')
|
||||
pl = translate('OpenLP.AboutForm', 'Polish (pl)')
|
||||
ptbr = translate('OpenLP.AboutForm', 'Portuguese, Brazil (pt_BR)')
|
||||
ru = translate('OpenLP.AboutForm', 'Russian (ru)')
|
||||
sv = translate('OpenLP.AboutForm', 'Swedish (sv)')
|
||||
talk = translate('OpenLP.AboutForm', 'Tamil(Sri-Lanka) (ta_LK)')
|
||||
zhcn = translate('OpenLP.AboutForm', 'Chinese(China) (zh_CN)')
|
||||
documentation = translate('OpenLP.AboutForm', 'Documentation')
|
||||
built_with = translate('OpenLP.AboutForm', 'Built With\n'
|
||||
' Python: http://www.python.org/\n'
|
||||
' Qt4: http://qt.io\n'
|
||||
' PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro\n'
|
||||
' Oxygen Icons: http://techbase.kde.org/Projects/Oxygen/\n'
|
||||
' MuPDF: http://www.mupdf.com/\n')
|
||||
final_credit = translate('OpenLP.AboutForm', 'Final Credit\n'
|
||||
' "For God so loved the world that He gave\n'
|
||||
' His one and only Son, so that whoever\n'
|
||||
' believes in Him will not perish but inherit\n'
|
||||
' eternal life." -- John 3:16\n\n'
|
||||
' And last but not least, final credit goes to\n'
|
||||
' God our Father, for sending His Son to die\n'
|
||||
' on the cross, setting us free from sin. We\n'
|
||||
' bring this software to you for free because\n'
|
||||
' He has set us free.')
|
||||
self.credits_text_edit.setPlainText(
|
||||
translate('OpenLP.AboutForm',
|
||||
'Project Lead\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'Developers\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'Contributors\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'Testers\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'Packagers\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'Translators\n'
|
||||
' Afrikaans (af)\n'
|
||||
' %s\n'
|
||||
' Czech (cs)\n'
|
||||
' %s\n'
|
||||
' Danish (da)\n'
|
||||
' %s\n'
|
||||
' German (de)\n'
|
||||
' %s\n'
|
||||
' Greek (el)\n'
|
||||
' %s\n'
|
||||
' English, United Kingdom (en_GB)\n'
|
||||
' %s\n'
|
||||
' English, South Africa (en_ZA)\n'
|
||||
' %s\n'
|
||||
' Spanish (es)\n'
|
||||
' %s\n'
|
||||
' Estonian (et)\n'
|
||||
' %s\n'
|
||||
' Finnish (fi)\n'
|
||||
' %s\n'
|
||||
' French (fr)\n'
|
||||
' %s\n'
|
||||
' Hungarian (hu)\n'
|
||||
' %s\n'
|
||||
' Indonesian (id)\n'
|
||||
' %s\n'
|
||||
' Japanese (ja)\n'
|
||||
' %s\n'
|
||||
' Norwegian Bokm\xe5l (nb)\n'
|
||||
' %s\n'
|
||||
' Dutch (nl)\n'
|
||||
' %s\n'
|
||||
' Polish (pl)\n'
|
||||
' %s\n'
|
||||
' Portuguese, Brazil (pt_BR)\n'
|
||||
' %s\n'
|
||||
' Russian (ru)\n'
|
||||
' %s\n'
|
||||
' Swedish (sv)\n'
|
||||
' %s\n'
|
||||
' Tamil(Sri-Lanka) (ta_LK)\n'
|
||||
' %s\n'
|
||||
' Chinese(China) (zh_CN)\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'Documentation\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'Built With\n'
|
||||
' Python: http://www.python.org/\n'
|
||||
' Qt4: http://qt.io\n'
|
||||
' PyQt4: http://www.riverbankcomputing.co.uk/software/pyqt/intro\n'
|
||||
' Oxygen Icons: http://techbase.kde.org/Projects/Oxygen/\n'
|
||||
' MuPDF: http://www.mupdf.com/\n'
|
||||
'\n'
|
||||
'Final Credit\n'
|
||||
' "For God so loved the world that He gave\n'
|
||||
' His one and only Son, so that whoever\n'
|
||||
' believes in Him will not perish but inherit\n'
|
||||
' eternal life." -- John 3:16\n\n'
|
||||
' And last but not least, final credit goes to\n'
|
||||
' God our Father, for sending His Son to die\n'
|
||||
' on the cross, setting us free from sin. We\n'
|
||||
' bring this software to you for free because\n'
|
||||
' He has set us free.') %
|
||||
(lead, '\n '.join(developers),
|
||||
'\n '.join(contributors), '\n '.join(testers),
|
||||
'\n '.join(packagers), '\n '.join(translators['af']),
|
||||
'\n '.join(translators['cs']),
|
||||
'\n '.join(translators['da']),
|
||||
'\n '.join(translators['de']),
|
||||
'\n '.join(translators['el']),
|
||||
'\n '.join(translators['en_GB']),
|
||||
'\n '.join(translators['en_ZA']),
|
||||
'\n '.join(translators['es']),
|
||||
'\n '.join(translators['et']),
|
||||
'\n '.join(translators['fi']),
|
||||
'\n '.join(translators['fr']),
|
||||
'\n '.join(translators['hu']),
|
||||
'\n '.join(translators['id']),
|
||||
'\n '.join(translators['ja']),
|
||||
'\n '.join(translators['nb']),
|
||||
'\n '.join(translators['nl']),
|
||||
'\n '.join(translators['pl']),
|
||||
'\n '.join(translators['pt_BR']),
|
||||
'\n '.join(translators['ru']),
|
||||
'\n '.join(translators['sv']),
|
||||
'\n '.join(translators['ta_LK']),
|
||||
'\n '.join(translators['zh_CN']),
|
||||
'\n '.join(documentors)))
|
||||
'%s\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'%s\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'%s\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'%s\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'%s\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'%s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'%s\n'
|
||||
' %s\n'
|
||||
'\n'
|
||||
'%s\n%s' %
|
||||
(project_lead, lead,
|
||||
devs, '\n '.join(developers),
|
||||
cons, '\n '.join(contributors),
|
||||
tests, '\n '.join(testers),
|
||||
packs, '\n '.join(packagers),
|
||||
laters,
|
||||
af, '\n '.join(translators['af']),
|
||||
cs, '\n '.join(translators['cs']),
|
||||
da, '\n '.join(translators['da']),
|
||||
de, '\n '.join(translators['de']),
|
||||
el, '\n '.join(translators['el']),
|
||||
gb, '\n '.join(translators['en_GB']),
|
||||
enza, '\n '.join(translators['en_ZA']),
|
||||
es, '\n '.join(translators['es']),
|
||||
et, '\n '.join(translators['et']),
|
||||
fi, '\n '.join(translators['fi']),
|
||||
fr, '\n '.join(translators['fr']),
|
||||
hu, '\n '.join(translators['hu']),
|
||||
ind, '\n '.join(translators['id']),
|
||||
ja, '\n '.join(translators['ja']),
|
||||
nb, '\n '.join(translators['nb']),
|
||||
nl, '\n '.join(translators['nl']),
|
||||
pl, '\n '.join(translators['pl']),
|
||||
ptbr, '\n '.join(translators['pt_BR']),
|
||||
ru, '\n '.join(translators['ru']),
|
||||
sv, '\n '.join(translators['sv']),
|
||||
talk, '\n '.join(translators['ta_LK']),
|
||||
zhcn, '\n '.join(translators['zh_CN']),
|
||||
documentation, '\n '.join(documentors),
|
||||
built_with, final_credit))
|
||||
self.about_notebook.setTabText(self.about_notebook.indexOf(self.credits_tab),
|
||||
translate('OpenLP.AboutForm', 'Credits'))
|
||||
copyright_note = translate('OpenLP.AboutForm',
|
||||
|
@ -80,6 +80,9 @@ class AdvancedTab(SettingsTab):
|
||||
self.expand_service_item_check_box = QtGui.QCheckBox(self.ui_group_box)
|
||||
self.expand_service_item_check_box.setObjectName('expand_service_item_check_box')
|
||||
self.ui_layout.addRow(self.expand_service_item_check_box)
|
||||
self.search_as_type_check_box = QtGui.QCheckBox(self.ui_group_box)
|
||||
self.search_as_type_check_box.setObjectName('SearchAsType_check_box')
|
||||
self.ui_layout.addRow(self.search_as_type_check_box)
|
||||
self.enable_auto_close_check_box = QtGui.QCheckBox(self.ui_group_box)
|
||||
self.enable_auto_close_check_box.setObjectName('enable_auto_close_check_box')
|
||||
self.ui_layout.addRow(self.enable_auto_close_check_box)
|
||||
@ -251,6 +254,7 @@ class AdvancedTab(SettingsTab):
|
||||
self.end_slide_radio_button.clicked.connect(self.on_end_slide_button_clicked)
|
||||
self.wrap_slide_radio_button.clicked.connect(self.on_wrap_slide_button_clicked)
|
||||
self.next_item_radio_button.clicked.connect(self.on_next_item_button_clicked)
|
||||
self.search_as_type_check_box.stateChanged.connect(self.on_search_as_type_check_box_changed)
|
||||
|
||||
def retranslateUi(self):
|
||||
"""
|
||||
@ -319,6 +323,7 @@ class AdvancedTab(SettingsTab):
|
||||
self.end_slide_radio_button.setText(translate('OpenLP.GeneralTab', '&Remain on Slide'))
|
||||
self.wrap_slide_radio_button.setText(translate('OpenLP.GeneralTab', '&Wrap around'))
|
||||
self.next_item_radio_button.setText(translate('OpenLP.GeneralTab', '&Move to next/previous service item'))
|
||||
self.search_as_type_check_box.setText(translate('SongsPlugin.GeneralTab', 'Enable search as you type'))
|
||||
|
||||
def load(self):
|
||||
"""
|
||||
@ -349,6 +354,8 @@ class AdvancedTab(SettingsTab):
|
||||
self.default_color = settings.value('default color')
|
||||
self.default_file_edit.setText(settings.value('default image'))
|
||||
self.slide_limits = settings.value('slide limits')
|
||||
self.is_search_as_you_type_enabled = settings.value('search as type')
|
||||
self.search_as_type_check_box.setChecked(self.is_search_as_you_type_enabled)
|
||||
# Prevent the dialog displayed by the alternate_rows_check_box to display.
|
||||
self.alternate_rows_check_box.blockSignals(True)
|
||||
self.alternate_rows_check_box.setChecked(settings.value('alternate rows'))
|
||||
@ -424,8 +431,14 @@ class AdvancedTab(SettingsTab):
|
||||
settings.setValue('x11 bypass wm', self.x11_bypass_check_box.isChecked())
|
||||
self.settings_form.register_post_process('config_screen_changed')
|
||||
self.settings_form.register_post_process('slidecontroller_update_slide_limits')
|
||||
settings.setValue('search as type', self.is_search_as_you_type_enabled)
|
||||
settings.endGroup()
|
||||
|
||||
def on_search_as_type_check_box_changed(self, check_state):
|
||||
self.is_search_as_you_type_enabled = (check_state == QtCore.Qt.Checked)
|
||||
self.settings_form.register_post_process('songs_config_updated')
|
||||
self.settings_form.register_post_process('custom_config_updated')
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
Dialogue was cancelled, remove any pending data path change.
|
||||
|
@ -173,6 +173,19 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
Registry().register_function('live_display_hide', self.hide_display)
|
||||
Registry().register_function('live_display_show', self.show_display)
|
||||
Registry().register_function('update_display_css', self.css_changed)
|
||||
self.close_display = False
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""
|
||||
Catch the close event, and check that the close event is triggered by OpenLP closing the display.
|
||||
On Windows this event can be triggered by pressing ALT+F4, which we want to ignore.
|
||||
|
||||
:param event: The triggered event
|
||||
"""
|
||||
if self.close_display:
|
||||
super().closeEvent(event)
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
@ -182,6 +195,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
Registry().remove_function('live_display_hide', self.hide_display)
|
||||
Registry().remove_function('live_display_show', self.show_display)
|
||||
Registry().remove_function('update_display_css', self.css_changed)
|
||||
self.close_display = True
|
||||
super().close()
|
||||
|
||||
def set_transparency(self, enabled):
|
||||
@ -320,7 +334,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties):
|
||||
cache.
|
||||
|
||||
:param path: The path to the image to be displayed. **Note**, the path is only passed to identify the image.
|
||||
If the image has changed it has to be re-added to the image manager.
|
||||
If the image has changed it has to be re-added to the image manager.
|
||||
"""
|
||||
image = self.image_manager.get_image_bytes(path, ImageSource.ImagePlugin)
|
||||
self.controller.media_controller.media_reset(self.controller)
|
||||
|
@ -312,6 +312,13 @@ class Ui_MainWindow(object):
|
||||
icon=':/system/system_help_contents.png',
|
||||
can_shortcuts=True,
|
||||
category=UiStrings().Help, triggers=self.on_offline_help_clicked)
|
||||
elif is_macosx():
|
||||
self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir),
|
||||
'..', 'Resources', 'OpenLP.help')
|
||||
self.offline_help_item = create_action(main_window, 'offlineHelpItem',
|
||||
icon=':/system/system_help_contents.png',
|
||||
can_shortcuts=True,
|
||||
category=UiStrings().Help, triggers=self.on_offline_help_clicked)
|
||||
self.on_line_help_item = create_action(main_window, 'onlineHelpItem',
|
||||
icon=':/system/system_online_help.png',
|
||||
can_shortcuts=True,
|
||||
@ -354,7 +361,7 @@ class Ui_MainWindow(object):
|
||||
add_actions(self.tools_menu, (self.tools_open_data_folder, None))
|
||||
add_actions(self.tools_menu, (self.tools_first_time_wizard, None))
|
||||
add_actions(self.tools_menu, [self.update_theme_images])
|
||||
if is_win():
|
||||
if (is_win() or is_macosx()) and (hasattr(sys, 'frozen') and sys.frozen == 1):
|
||||
add_actions(self.help_menu, (self.offline_help_item, self.on_line_help_item, None, self.web_site_item,
|
||||
self.about_item))
|
||||
else:
|
||||
@ -382,7 +389,7 @@ class Ui_MainWindow(object):
|
||||
self.file_menu.setTitle(translate('OpenLP.MainWindow', '&File'))
|
||||
self.file_import_menu.setTitle(translate('OpenLP.MainWindow', '&Import'))
|
||||
self.file_export_menu.setTitle(translate('OpenLP.MainWindow', '&Export'))
|
||||
self.recent_files_menu.setTitle(translate('OpenLP.MainWindow', '&Recent Files'))
|
||||
self.recent_files_menu.setTitle(translate('OpenLP.MainWindow', '&Recent Services'))
|
||||
self.view_menu.setTitle(translate('OpenLP.MainWindow', '&View'))
|
||||
self.view_mode_menu.setTitle(translate('OpenLP.MainWindow', 'M&ode'))
|
||||
self.tools_menu.setTitle(translate('OpenLP.MainWindow', '&Tools'))
|
||||
@ -393,16 +400,16 @@ class Ui_MainWindow(object):
|
||||
self.service_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Service Manager'))
|
||||
self.theme_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Theme Manager'))
|
||||
self.projector_manager_dock.setWindowTitle(translate('OpenLP.MainWindow', 'Projector Manager'))
|
||||
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New'))
|
||||
self.file_new_item.setText(translate('OpenLP.MainWindow', '&New Service'))
|
||||
self.file_new_item.setToolTip(UiStrings().NewService)
|
||||
self.file_new_item.setStatusTip(UiStrings().CreateService)
|
||||
self.file_open_item.setText(translate('OpenLP.MainWindow', '&Open'))
|
||||
self.file_open_item.setText(translate('OpenLP.MainWindow', '&Open Service'))
|
||||
self.file_open_item.setToolTip(UiStrings().OpenService)
|
||||
self.file_open_item.setStatusTip(translate('OpenLP.MainWindow', 'Open an existing service.'))
|
||||
self.file_save_item.setText(translate('OpenLP.MainWindow', '&Save'))
|
||||
self.file_save_item.setText(translate('OpenLP.MainWindow', '&Save Service'))
|
||||
self.file_save_item.setToolTip(UiStrings().SaveService)
|
||||
self.file_save_item.setStatusTip(translate('OpenLP.MainWindow', 'Save the current service to disk.'))
|
||||
self.file_save_as_item.setText(translate('OpenLP.MainWindow', 'Save &As...'))
|
||||
self.file_save_as_item.setText(translate('OpenLP.MainWindow', 'Save Service &As...'))
|
||||
self.file_save_as_item.setToolTip(translate('OpenLP.MainWindow', 'Save Service As'))
|
||||
self.file_save_as_item.setStatusTip(translate('OpenLP.MainWindow',
|
||||
'Save the current service under a new name.'))
|
||||
@ -449,11 +456,11 @@ class Ui_MainWindow(object):
|
||||
self.lock_panel.setText(translate('OpenLP.MainWindow', 'L&ock Panels'))
|
||||
self.lock_panel.setStatusTip(translate('OpenLP.MainWindow', 'Prevent the panels being moved.'))
|
||||
self.view_live_panel.setStatusTip(translate('OpenLP.MainWindow', 'Toggle the visibility of the live panel.'))
|
||||
self.settings_plugin_list_item.setText(translate('OpenLP.MainWindow', '&Plugin List'))
|
||||
self.settings_plugin_list_item.setText(translate('OpenLP.MainWindow', '&Manage Plugins'))
|
||||
self.settings_plugin_list_item.setStatusTip(translate('OpenLP.MainWindow', 'List the Plugins'))
|
||||
self.about_item.setText(translate('OpenLP.MainWindow', '&About'))
|
||||
self.about_item.setStatusTip(translate('OpenLP.MainWindow', 'More information about OpenLP'))
|
||||
if is_win():
|
||||
if is_win() or is_macosx():
|
||||
self.offline_help_item.setText(translate('OpenLP.MainWindow', '&User Guide'))
|
||||
self.on_line_help_item.setText(translate('OpenLP.MainWindow', '&Online Help'))
|
||||
self.search_shortcut_action.setText(UiStrings().Search)
|
||||
@ -498,7 +505,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
super(MainWindow, self).__init__()
|
||||
Registry().register('main_window', self)
|
||||
self.clipboard = self.application.clipboard()
|
||||
self.arguments = self.application.args
|
||||
self.arguments = ''.join(self.application.args)
|
||||
# Set up settings sections for the main application (not for use by plugins).
|
||||
self.ui_settings_section = 'user interface'
|
||||
self.general_settings_section = 'core'
|
||||
@ -627,7 +634,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
self.live_controller.display.setFocus()
|
||||
self.activateWindow()
|
||||
if self.arguments:
|
||||
self.open_cmd_line_files()
|
||||
self.open_cmd_line_files(self.arguments)
|
||||
elif Settings().value(self.general_settings_section + '/auto open'):
|
||||
self.service_manager_contents.load_last_file()
|
||||
view_mode = Settings().value('%s/view mode' % self.general_settings_section)
|
||||
@ -762,7 +769,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
"""
|
||||
Load the local OpenLP help file
|
||||
"""
|
||||
os.startfile(self.local_help_file)
|
||||
QtGui.QDesktopServices.openUrl(QtCore.QUrl("file:///" + self.local_help_file))
|
||||
|
||||
def on_online_help_clicked(self):
|
||||
"""
|
||||
@ -1409,15 +1416,11 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties):
|
||||
settings.remove('advanced/data path')
|
||||
self.application.set_normal_cursor()
|
||||
|
||||
def open_cmd_line_files(self):
|
||||
def open_cmd_line_files(self, filename):
|
||||
"""
|
||||
Open files passed in through command line arguments
|
||||
"""
|
||||
args = []
|
||||
for a in self.arguments:
|
||||
args.extend([a])
|
||||
for filename in args:
|
||||
if not isinstance(filename, str):
|
||||
filename = str(filename, sys.getfilesystemencoding())
|
||||
if filename.endswith(('.osz', '.oszl')):
|
||||
self.service_manager_contents.load_file(filename)
|
||||
if not isinstance(filename, str):
|
||||
filename = str(filename, sys.getfilesystemencoding())
|
||||
if filename.endswith(('.osz', '.oszl')):
|
||||
self.service_manager_contents.load_file(filename)
|
||||
|
@ -514,9 +514,14 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties):
|
||||
:param display: Which display to use
|
||||
:param service_item: The ServiceItem containing the details to be played.
|
||||
"""
|
||||
used_players = get_media_players()[0]
|
||||
used_players = get_media_players()
|
||||
default_player = used_players[0]
|
||||
if service_item.processor and service_item.processor != UiStrings().Automatic:
|
||||
used_players = [service_item.processor.lower()]
|
||||
# check to see if the player is usable else use the default one.
|
||||
if not service_item.processor.lower() in used_players:
|
||||
used_players = default_player
|
||||
else:
|
||||
used_players = [service_item.processor.lower()]
|
||||
# If no player, we can't play
|
||||
if not used_players:
|
||||
return False
|
||||
|
2
openlp/core/ui/media/vendor/vlc.py
vendored
2
openlp/core/ui/media/vendor/vlc.py
vendored
@ -60,7 +60,7 @@ if sys.version_info[0] > 2:
|
||||
"""Translate string or bytes to bytes.
|
||||
"""
|
||||
if isinstance(s, str):
|
||||
return bytes(s, sys.getfilesystemencoding())
|
||||
return s.encode()
|
||||
else:
|
||||
return s
|
||||
|
||||
|
@ -80,10 +80,8 @@ def get_vlc():
|
||||
if is_win():
|
||||
if not isinstance(e, WindowsError) and e.winerror != 126:
|
||||
raise
|
||||
elif is_macosx():
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
pass
|
||||
if is_vlc_available:
|
||||
try:
|
||||
VERSION = vlc.libvlc_get_version().decode('UTF-8')
|
||||
@ -103,10 +101,14 @@ def get_vlc():
|
||||
# On linux we need to initialise X threads, but not when running tests.
|
||||
# This needs to happen on module load and not in get_vlc(), otherwise it can cause crashes on some DE on some setups
|
||||
# (reported on Gnome3, Unity, Cinnamon, all GTK+ based) when using native filedialogs...
|
||||
if get_vlc() and is_linux() and 'nose' not in sys.argv[0]:
|
||||
if is_linux() and 'nose' not in sys.argv[0] and get_vlc():
|
||||
import ctypes
|
||||
try:
|
||||
x11 = ctypes.cdll.LoadLibrary('libX11.so')
|
||||
try:
|
||||
x11 = ctypes.cdll.LoadLibrary('libX11.so.6')
|
||||
except OSError:
|
||||
# If libx11.so.6 was not found, fallback to more generic libx11.so
|
||||
x11 = ctypes.cdll.LoadLibrary('libX11.so')
|
||||
x11.XInitThreads()
|
||||
except:
|
||||
log.exception('Failed to run XInitThreads(), VLC might not work properly!')
|
||||
|
@ -33,21 +33,21 @@ class Ui_PluginViewDialog(object):
|
||||
"""
|
||||
The UI of the plugin view dialog
|
||||
"""
|
||||
def setupUi(self, pluginViewDialog):
|
||||
def setupUi(self, plugin_view_dialog):
|
||||
"""
|
||||
Set up the UI
|
||||
"""
|
||||
pluginViewDialog.setObjectName('pluginViewDialog')
|
||||
pluginViewDialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
|
||||
pluginViewDialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
self.plugin_layout = QtGui.QVBoxLayout(pluginViewDialog)
|
||||
plugin_view_dialog.setObjectName('plugin_view_dialog')
|
||||
plugin_view_dialog.setWindowIcon(build_icon(u':/icon/openlp-logo.svg'))
|
||||
plugin_view_dialog.setWindowModality(QtCore.Qt.ApplicationModal)
|
||||
self.plugin_layout = QtGui.QVBoxLayout(plugin_view_dialog)
|
||||
self.plugin_layout.setObjectName('plugin_layout')
|
||||
self.list_layout = QtGui.QHBoxLayout()
|
||||
self.list_layout.setObjectName('list_layout')
|
||||
self.plugin_list_widget = QtGui.QListWidget(pluginViewDialog)
|
||||
self.plugin_list_widget = QtGui.QListWidget(plugin_view_dialog)
|
||||
self.plugin_list_widget.setObjectName('plugin_list_widget')
|
||||
self.list_layout.addWidget(self.plugin_list_widget)
|
||||
self.plugin_info_group_box = QtGui.QGroupBox(pluginViewDialog)
|
||||
self.plugin_info_group_box = QtGui.QGroupBox(plugin_view_dialog)
|
||||
self.plugin_info_group_box.setObjectName('plugin_info_group_box')
|
||||
self.plugin_info_layout = QtGui.QFormLayout(self.plugin_info_group_box)
|
||||
self.plugin_info_layout.setObjectName('plugin_info_layout')
|
||||
@ -70,15 +70,15 @@ class Ui_PluginViewDialog(object):
|
||||
self.plugin_info_layout.addRow(self.about_label, self.about_text_browser)
|
||||
self.list_layout.addWidget(self.plugin_info_group_box)
|
||||
self.plugin_layout.addLayout(self.list_layout)
|
||||
self.button_box = create_button_box(pluginViewDialog, 'button_box', ['ok'])
|
||||
self.button_box = create_button_box(plugin_view_dialog, 'button_box', ['ok'])
|
||||
self.plugin_layout.addWidget(self.button_box)
|
||||
self.retranslateUi(pluginViewDialog)
|
||||
self.retranslateUi(plugin_view_dialog)
|
||||
|
||||
def retranslateUi(self, pluginViewDialog):
|
||||
def retranslateUi(self, plugin_view_dialog):
|
||||
"""
|
||||
Translate the UI on the fly
|
||||
"""
|
||||
pluginViewDialog.setWindowTitle(translate('OpenLP.PluginForm', 'Plugin List'))
|
||||
plugin_view_dialog.setWindowTitle(translate('OpenLP.PluginForm', 'Manage Plugins'))
|
||||
self.plugin_info_group_box.setTitle(translate('OpenLP.PluginForm', 'Plugin Details'))
|
||||
self.version_label.setText('%s:' % UiStrings().Version)
|
||||
self.about_label.setText('%s:' % UiStrings().About)
|
||||
|
@ -162,7 +162,7 @@ class PrintServiceForm(QtGui.QDialog, Ui_PrintServiceDialog, RegistryProperties)
|
||||
html_data = self._add_element('html')
|
||||
self._add_element('head', parent=html_data)
|
||||
self._add_element('title', self.title_line_edit.text(), html_data.head)
|
||||
css_path = os.path.join(AppLocation.get_data_path(), 'service_print.css')
|
||||
css_path = os.path.join(AppLocation.get_data_path(), 'serviceprint', 'service_print.css')
|
||||
custom_css = get_text_file_string(css_path)
|
||||
if not custom_css:
|
||||
custom_css = DEFAULT_CSS
|
||||
|
@ -473,8 +473,9 @@ class ProjectorManager(OpenLPMixin, RegistryMixin, QWidget, Ui_ProjectorManager,
|
||||
return
|
||||
projector = list_item.data(QtCore.Qt.UserRole)
|
||||
msg = QtGui.QMessageBox()
|
||||
msg.setText('Delete projector (%s) %s?' % (projector.link.ip, projector.link.name))
|
||||
msg.setInformativeText('Are you sure you want to delete this projector?')
|
||||
msg.setText(translate('OpenLP.ProjectorManager', 'Delete projector (%s) %s?') % (projector.link.ip,
|
||||
projector.link.name))
|
||||
msg.setInformativeText(translate('OpenLP.ProjectorManager', 'Are you sure you want to delete this projector?'))
|
||||
msg.setStandardButtons(msg.Cancel | msg.Ok)
|
||||
msg.setDefaultButton(msg.Cancel)
|
||||
ans = msg.exec_()
|
||||
|
@ -44,17 +44,19 @@ def source_group(inputs, source_text):
|
||||
Return a dictionary where key is source[0] and values are inputs
|
||||
grouped by source[0].
|
||||
|
||||
source_text = dict{"key1": "key1-text",
|
||||
"key2": "key2-text",
|
||||
...}
|
||||
return:
|
||||
dict{ key1[0]: { "key11": "key11-text",
|
||||
"key12": "key12-text",
|
||||
"key13": "key13-text",
|
||||
... }
|
||||
key2[0]: {"key21": "key21-text",
|
||||
"key22": "key22-text",
|
||||
... }
|
||||
::
|
||||
|
||||
source_text = dict{"key1": "key1-text",
|
||||
"key2": "key2-text",
|
||||
...}
|
||||
return:
|
||||
dict{key1[0]: {"key11": "key11-text",
|
||||
"key12": "key12-text",
|
||||
"key13": "key13-text",
|
||||
...}
|
||||
key2[0]: {"key21": "key21-text",
|
||||
"key22": "key22-text",
|
||||
...}
|
||||
|
||||
:param inputs: List of inputs
|
||||
:param source_text: Dictionary of {code: text} values to display
|
||||
@ -81,16 +83,18 @@ def Build_Tab(group, source_key, default, projector, projectordb, edit=False):
|
||||
Create the radio button page for a tab.
|
||||
Dictionary will be a 1-key entry where key=tab to setup, val=list of inputs.
|
||||
|
||||
source_key: {"groupkey1": {"key11": "key11-text",
|
||||
"key12": "key12-text",
|
||||
...
|
||||
},
|
||||
"groupkey2": {"key21": "key21-text",
|
||||
"key22": "key22-text",
|
||||
....
|
||||
},
|
||||
...
|
||||
}
|
||||
::
|
||||
|
||||
source_key: {"groupkey1": {"key11": "key11-text",
|
||||
"key12": "key12-text",
|
||||
...
|
||||
},
|
||||
"groupkey2": {"key21": "key21-text",
|
||||
"key22": "key22-text",
|
||||
...
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
:param group: Button group widget to add buttons to
|
||||
:param source_key: Dictionary of sources for radio buttons
|
||||
|
@ -480,7 +480,7 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage
|
||||
"""
|
||||
Create the initial service array with the base items to be saved.
|
||||
|
||||
:return service array
|
||||
:return: service array
|
||||
"""
|
||||
service = []
|
||||
core = {'lite-service': self._save_lite,
|
||||
|
@ -902,7 +902,8 @@ class SlideController(DisplayController, RegistryProperties):
|
||||
# This avoids the service theme/desktop flashing on screen
|
||||
# However opening a new item of the same type will automatically
|
||||
# close the previous, so make sure we don't close the new one.
|
||||
if old_item.is_command() and not service_item.is_command():
|
||||
if old_item.is_command() and not service_item.is_command() or \
|
||||
old_item.is_command() and not old_item.is_media() and service_item.is_media():
|
||||
Registry().execute('%s_stop' % old_item.name.lower(), [old_item, self.is_live])
|
||||
if old_item.is_media() and not service_item.is_media():
|
||||
self.on_media_close()
|
||||
|
@ -503,7 +503,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
|
||||
Returns a theme object from an XML file
|
||||
|
||||
:param theme_name: Name of the theme to load from file
|
||||
:return The theme object.
|
||||
:return: The theme object.
|
||||
"""
|
||||
self.log_debug('get theme data for theme %s' % theme_name)
|
||||
xml_file = os.path.join(self.path, str(theme_name), str(theme_name) + '.xml')
|
||||
@ -519,7 +519,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
|
||||
Display a warning box to the user that a theme already exists
|
||||
|
||||
:param theme_name: Name of the theme.
|
||||
:return Confirm if the theme is to be overwritten.
|
||||
:return: Confirm if the theme is to be overwritten.
|
||||
"""
|
||||
ret = QtGui.QMessageBox.question(self, translate('OpenLP.ThemeManager', 'Theme Already Exists'),
|
||||
translate('OpenLP.ThemeManager',
|
||||
@ -607,7 +607,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
|
||||
Check if theme already exists and displays error message
|
||||
|
||||
:param theme_name: Name of the Theme to test
|
||||
:return True or False if theme exists
|
||||
:return: True or False if theme exists
|
||||
"""
|
||||
theme_dir = os.path.join(self.path, theme_name)
|
||||
if os.path.exists(theme_dir):
|
||||
@ -718,7 +718,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
|
||||
|
||||
:param theme_xml: The Theme data object.
|
||||
:param image_path: Where the theme image is stored
|
||||
:return Theme data.
|
||||
:return: Theme data.
|
||||
"""
|
||||
theme = ThemeXML()
|
||||
theme.parse(theme_xml)
|
||||
@ -734,7 +734,7 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
|
||||
:param confirm_text: Confirm message text to be displayed.
|
||||
:param test_plugin: Do we check the plugins for theme usage.
|
||||
:param confirm: Do we display a confirm box before run checks.
|
||||
:return True or False depending on the validity.
|
||||
:return: True or False depending on the validity.
|
||||
"""
|
||||
self.global_theme = Settings().value(self.settings_section + '/global theme')
|
||||
if check_item_selected(self.theme_list_widget, select_text):
|
||||
@ -755,12 +755,19 @@ class ThemeManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ThemeManager, R
|
||||
return False
|
||||
# check for use in the system else where.
|
||||
if test_plugin:
|
||||
plugin_usage = ""
|
||||
for plugin in self.plugin_manager.plugins:
|
||||
if plugin.uses_theme(theme):
|
||||
critical_error_message_box(translate('OpenLP.ThemeManager', 'Validation Error'),
|
||||
translate('OpenLP.ThemeManager',
|
||||
'Theme %s is used in the %s plugin.')
|
||||
% (theme, plugin.name))
|
||||
return False
|
||||
used_count = plugin.uses_theme(theme)
|
||||
if used_count:
|
||||
plugin_usage = "%s%s" % (plugin_usage, (translate('OpenLP.ThemeManager',
|
||||
'%s time(s) by %s') %
|
||||
(used_count, plugin.name)))
|
||||
plugin_usage = "%s\n" % plugin_usage
|
||||
if plugin_usage:
|
||||
critical_error_message_box(translate('OpenLP.ThemeManager', 'Unable to delete theme'),
|
||||
translate('OpenLP.ThemeManager', 'Theme is currently used \n\n%s') %
|
||||
plugin_usage)
|
||||
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
@ -404,6 +404,7 @@ def get_web_page(url, header=None, update_openlp=False):
|
||||
try:
|
||||
page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT)
|
||||
log.debug('Downloaded page {}'.format(page.geturl()))
|
||||
break
|
||||
except urllib.error.URLError as err:
|
||||
log.exception('URLError on {}'.format(url))
|
||||
log.exception('URLError: {}'.format(err.reason))
|
||||
|
@ -238,10 +238,10 @@ class ActionList(object):
|
||||
|
||||
:param action: The action to add (QAction). **Note**, the action must not have an empty ``objectName``.
|
||||
:param category: The category this action belongs to. The category has to be a python string. . **Note**,
|
||||
if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
|
||||
if they are added, it is possible to avoid assigning shortcuts twice, which is important.
|
||||
if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
|
||||
if they are added, it is possible to avoid assigning shortcuts twice, which is important.
|
||||
:param weight: The weight specifies how important a category is. However, this only has an impact on the order
|
||||
the categories are displayed.
|
||||
the categories are displayed.
|
||||
"""
|
||||
if category not in self.categories:
|
||||
self.categories.append(category)
|
||||
|
@ -48,7 +48,11 @@ class AlertsManager(OpenLPMixin, RegistryMixin, QtCore.QObject, RegistryProperti
|
||||
:param message: The message text to be displayed
|
||||
"""
|
||||
if message:
|
||||
self.display_alert(message[0])
|
||||
text = message[0]
|
||||
# remove line breaks as these crash javascript code on display
|
||||
while '\n' in text:
|
||||
text = text.replace('\n', ' ')
|
||||
self.display_alert(text)
|
||||
|
||||
def display_alert(self, text=''):
|
||||
"""
|
||||
|
@ -178,12 +178,14 @@ class BiblePlugin(Plugin):
|
||||
|
||||
def uses_theme(self, theme):
|
||||
"""
|
||||
Called to find out if the bible plugin is currently using a theme. Returns ``True`` if the theme is being used,
|
||||
otherwise returns ``False``.
|
||||
Called to find out if the bible plugin is currently using a theme. Returns ``1`` if the theme is being used,
|
||||
otherwise returns ``0``.
|
||||
|
||||
:param theme: The theme
|
||||
"""
|
||||
return str(self.settings_tab.bible_theme) == theme
|
||||
if str(self.settings_tab.bible_theme) == theme:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
def rename_theme(self, old_theme, new_theme):
|
||||
"""
|
||||
|
@ -517,17 +517,19 @@ class BibleImportForm(OpenLPWizard):
|
||||
critical_error_message_box(translate('BiblesPlugin.ImportWizardForm', 'Error during download'),
|
||||
translate('BiblesPlugin.ImportWizardForm',
|
||||
'An error occurred while downloading the list of bibles from %s.'))
|
||||
self.web_bible_list[download_type] = {}
|
||||
for (bible_name, bible_key, language_code) in bibles:
|
||||
self.web_bible_list[download_type][bible_name] = (bible_key, language_code)
|
||||
bibles = None
|
||||
if bibles:
|
||||
self.web_bible_list[download_type] = {}
|
||||
for (bible_name, bible_key, language_code) in bibles:
|
||||
self.web_bible_list[download_type][bible_name] = (bible_key, language_code)
|
||||
self.web_progress_bar.setValue(download_type + 1)
|
||||
# Update combo box if something got into the list
|
||||
if self.web_bible_list:
|
||||
self.on_web_source_combo_box_index_changed(0)
|
||||
self.web_source_combo_box.setEnabled(True)
|
||||
self.web_translation_combo_box.setEnabled(True)
|
||||
self.web_update_button.setEnabled(True)
|
||||
self.web_progress_bar.setVisible(False)
|
||||
self.web_source_combo_box.setEnabled(True)
|
||||
self.web_translation_combo_box.setEnabled(True)
|
||||
self.web_update_button.setEnabled(True)
|
||||
self.web_progress_bar.setVisible(False)
|
||||
|
||||
def register_fields(self):
|
||||
"""
|
||||
|
@ -37,7 +37,7 @@ from openlp.core.common import Registry, RegistryProperties, AppLocation, transl
|
||||
from openlp.core.lib.db import BaseModel, init_db, Manager
|
||||
from openlp.core.lib.ui import critical_error_message_box
|
||||
from openlp.core.utils import clean_filename
|
||||
from . import upgrade
|
||||
from openlp.plugins.bibles.lib import upgrade
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -219,7 +219,7 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties):
|
||||
:param book_id: The id of the book being appended.
|
||||
:param chapter: The chapter number.
|
||||
:param text_list: A dict of the verses to be inserted. The key is the verse number, and the value is the
|
||||
verse text.
|
||||
verse text.
|
||||
"""
|
||||
log.debug('BibleDBcreate_chapter("%s", "%s")' % (book_id, chapter))
|
||||
# Text list has book and chapter as first two elements of the array.
|
||||
@ -476,16 +476,6 @@ class BibleDB(QtCore.QObject, Manager, RegistryProperties):
|
||||
self.save_meta('language_id', language_id)
|
||||
return language_id
|
||||
|
||||
def is_old_database(self):
|
||||
"""
|
||||
Returns ``True`` if it is a bible database, which has been created prior to 1.9.6.
|
||||
"""
|
||||
try:
|
||||
self.session.query(Book).all()
|
||||
except:
|
||||
return True
|
||||
return False
|
||||
|
||||
def dump_bible(self):
|
||||
"""
|
||||
Utility debugging method to dump the contents of a bible.
|
||||
|
@ -27,7 +27,6 @@ import re
|
||||
import socket
|
||||
import urllib.parse
|
||||
import urllib.error
|
||||
from html.parser import HTMLParseError
|
||||
|
||||
from bs4 import BeautifulSoup, NavigableString, Tag
|
||||
|
||||
@ -290,7 +289,7 @@ class BGExtract(RegistryProperties):
|
||||
page_source = str(page_source, 'cp1251')
|
||||
try:
|
||||
soup = BeautifulSoup(page_source)
|
||||
except HTMLParseError:
|
||||
except Exception:
|
||||
log.error('BeautifulSoup could not parse the Bible page.')
|
||||
send_error_message('parse')
|
||||
return None
|
||||
@ -743,7 +742,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
|
||||
:param reference_url: The URL to obtain the soup from.
|
||||
:param header: An optional HTTP header to pass to the bible web server.
|
||||
:param pre_parse_regex: A regular expression to run on the webpage. Allows manipulation of the webpage before
|
||||
passing to BeautifulSoup for parsing.
|
||||
passing to BeautifulSoup for parsing.
|
||||
:param pre_parse_substitute: The text to replace any matches to the regular expression with.
|
||||
"""
|
||||
if not reference_url:
|
||||
@ -762,7 +761,7 @@ def get_soup_for_bible_ref(reference_url, header=None, pre_parse_regex=None, pre
|
||||
try:
|
||||
soup = BeautifulSoup(page_source)
|
||||
CLEANER_REGEX.sub('', str(soup))
|
||||
except HTMLParseError:
|
||||
except Exception:
|
||||
log.exception('BeautifulSoup could not parse the bible page.')
|
||||
if not soup:
|
||||
send_error_message('parse')
|
||||
|
@ -129,11 +129,6 @@ class BibleManager(RegistryProperties):
|
||||
bible.session.close()
|
||||
delete_file(os.path.join(self.path, filename))
|
||||
continue
|
||||
# Find old database versions.
|
||||
if bible.is_old_database():
|
||||
self.old_bible_databases.append([filename, name])
|
||||
bible.session.close()
|
||||
continue
|
||||
log.debug('Bible Name: "%s"', name)
|
||||
self.db_cache[name] = bible
|
||||
# Look to see if lazy load bible exists and get create getter.
|
||||
|
@ -30,7 +30,8 @@ from openlp.core.lib.searchedit import SearchEdit
|
||||
from openlp.core.lib.ui import set_case_insensitive_completer, create_horizontal_adjusting_combo_box, \
|
||||
critical_error_message_box, find_and_set_in_combo_box, build_icon
|
||||
from openlp.core.utils import get_locale_key
|
||||
from openlp.plugins.bibles.forms import BibleImportForm, EditBibleForm
|
||||
from openlp.plugins.bibles.forms.bibleimportform import BibleImportForm
|
||||
from openlp.plugins.bibles.forms.editbibleform import EditBibleForm
|
||||
from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \
|
||||
LanguageSelection, BibleStrings
|
||||
from openlp.plugins.bibles.lib.db import BiblesResourcesDB
|
||||
|
@ -72,11 +72,9 @@ class CustomPlugin(Plugin):
|
||||
"""
|
||||
Called to find out if the custom plugin is currently using a theme.
|
||||
|
||||
Returns True if the theme is being used, otherwise returns False.
|
||||
Returns count of the times the theme is used.
|
||||
"""
|
||||
if self.db_manager.get_all_objects(CustomSlide, CustomSlide.theme_name == theme):
|
||||
return True
|
||||
return False
|
||||
return len(self.db_manager.get_all_objects(CustomSlide, CustomSlide.theme_name == theme))
|
||||
|
||||
def rename_theme(self, old_theme, new_theme):
|
||||
"""
|
||||
|
@ -84,7 +84,7 @@ class CustomXMLBuilder(object):
|
||||
Add a verse to the ``<lyrics>`` tag.
|
||||
|
||||
:param verse_type: A string denoting the type of verse. Possible values are "Chorus", "Verse", "Bridge",
|
||||
and "Custom".
|
||||
and "Custom".
|
||||
:param number: An integer denoting the number of the item, for example: verse 1.
|
||||
:param content: The actual text of the verse to be stored.
|
||||
|
||||
|
@ -85,6 +85,7 @@ class CustomMediaItem(MediaManagerItem):
|
||||
"""
|
||||
log.debug('Config loaded')
|
||||
self.add_custom_from_service = Settings().value(self.settings_section + '/add custom from service')
|
||||
self.is_search_as_you_type_enabled = Settings().value('advanced/search as type')
|
||||
|
||||
def retranslateUi(self):
|
||||
"""
|
||||
@ -269,11 +270,12 @@ class CustomMediaItem(MediaManagerItem):
|
||||
|
||||
:param text: The search text
|
||||
"""
|
||||
search_length = 2
|
||||
if len(text) > search_length:
|
||||
self.on_search_text_button_clicked()
|
||||
elif not text:
|
||||
self.on_clear_text_button_click()
|
||||
if self.is_search_as_you_type_enabled:
|
||||
search_length = 2
|
||||
if len(text) > search_length:
|
||||
self.on_search_text_button_clicked()
|
||||
elif not text:
|
||||
self.on_clear_text_button_click()
|
||||
|
||||
def service_load(self, item):
|
||||
"""
|
||||
|
@ -44,7 +44,7 @@ class AddGroupForm(QtGui.QDialog, Ui_AddGroupDialog):
|
||||
|
||||
:param clear: Set to False if the text input box should not be cleared when showing the dialog (default: True).
|
||||
:param show_top_level_group: Set to True when "-- Top level group --" should be showed as first item
|
||||
(default: False).
|
||||
(default: False).
|
||||
:param selected_group: The ID of the group that should be selected by default when showing the dialog.
|
||||
"""
|
||||
if clear:
|
||||
|
@ -67,36 +67,13 @@ class ImagePlugin(Plugin):
|
||||
'provided by the theme.')
|
||||
return about_text
|
||||
|
||||
def app_startup(self):
|
||||
"""
|
||||
Perform tasks on application startup.
|
||||
"""
|
||||
# TODO: Can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
|
||||
Plugin.app_startup(self)
|
||||
# Convert old settings-based image list to the database.
|
||||
files_from_config = Settings().get_files_from_config(self)
|
||||
if files_from_config:
|
||||
for file in files_from_config:
|
||||
filename = os.path.split(file)[1]
|
||||
thumb = os.path.join(self.media_item.service_path, filename)
|
||||
try:
|
||||
os.remove(thumb)
|
||||
except:
|
||||
pass
|
||||
log.debug('Importing images list from old config: %s' % files_from_config)
|
||||
self.media_item.save_new_images_list(files_from_config)
|
||||
|
||||
def upgrade_settings(self, settings):
|
||||
"""
|
||||
Upgrade the settings of this plugin.
|
||||
|
||||
:param settings: The Settings object containing the old settings.
|
||||
"""
|
||||
# TODO: Can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
|
||||
files_from_config = settings.get_files_from_config(self)
|
||||
if files_from_config:
|
||||
log.debug('Importing images list from old config: %s' % files_from_config)
|
||||
self.media_item.save_new_images_list(files_from_config)
|
||||
pass
|
||||
|
||||
def set_plugin_text_strings(self):
|
||||
"""
|
||||
|
@ -48,24 +48,24 @@ def init_schema(url):
|
||||
Setup the images database connection and initialise the database schema.
|
||||
|
||||
:param url: The database to setup
|
||||
The images database contains the following tables:
|
||||
The images database contains the following tables:
|
||||
|
||||
* image_groups
|
||||
* image_filenames
|
||||
* image_groups
|
||||
* image_filenames
|
||||
|
||||
**image_groups Table**
|
||||
This table holds the names of the images groups. It has the following columns:
|
||||
**image_groups Table**
|
||||
This table holds the names of the images groups. It has the following columns:
|
||||
|
||||
* id
|
||||
* parent_id
|
||||
* group_name
|
||||
* id
|
||||
* parent_id
|
||||
* group_name
|
||||
|
||||
**image_filenames Table**
|
||||
This table holds the filenames of the images and the group they belong to. It has the following columns:
|
||||
**image_filenames Table**
|
||||
This table holds the filenames of the images and the group they belong to. It has the following columns:
|
||||
|
||||
* id
|
||||
* group_id
|
||||
* filename
|
||||
* id
|
||||
* group_id
|
||||
* filename
|
||||
"""
|
||||
session, metadata = init_db(url)
|
||||
|
||||
|
@ -119,14 +119,6 @@ class ImageMediaItem(MediaManagerItem):
|
||||
icon=':/general/general_edit.png',
|
||||
triggers=self.on_edit_click)
|
||||
create_widget_action(self.list_view, separator=True)
|
||||
if self.has_delete_icon:
|
||||
create_widget_action(
|
||||
self.list_view,
|
||||
'listView%s%sItem' % (self.plugin.name.title(), StringContent.Delete.title()),
|
||||
text=self.plugin.get_string(StringContent.Delete)['title'],
|
||||
icon=':/general/general_delete.png',
|
||||
can_shortcuts=True, triggers=self.on_delete_click)
|
||||
create_widget_action(self.list_view, separator=True)
|
||||
create_widget_action(
|
||||
self.list_view,
|
||||
'listView%s%sItem' % (self.plugin.name.title(), StringContent.Preview.title()),
|
||||
@ -155,6 +147,14 @@ class ImageMediaItem(MediaManagerItem):
|
||||
text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
|
||||
icon=':/general/general_add.png',
|
||||
triggers=self.on_add_edit_click)
|
||||
create_widget_action(self.list_view, separator=True)
|
||||
if self.has_delete_icon:
|
||||
create_widget_action(
|
||||
self.list_view,
|
||||
'listView%s%sItem' % (self.plugin.name.title(), StringContent.Delete.title()),
|
||||
text=self.plugin.get_string(StringContent.Delete)['title'],
|
||||
icon=':/general/general_delete.png',
|
||||
can_shortcuts=True, triggers=self.on_delete_click)
|
||||
self.add_custom_context_actions()
|
||||
# Create the context menu and add all actions from the list_view.
|
||||
self.menu = QtGui.QMenu()
|
||||
@ -285,7 +285,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
:param combobox: The QComboBox to add the options to.
|
||||
:param parent_group_id: The ID of the group that will be added.
|
||||
:param prefix: A string containing the prefix that will be added in front of the groupname for each level of
|
||||
the tree.
|
||||
the tree.
|
||||
"""
|
||||
if parent_group_id == 0:
|
||||
combobox.clear()
|
||||
@ -333,7 +333,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
:param images: A List of Image Filenames objects that will be used to reload the mediamanager list.
|
||||
:param initial_load: When set to False, the busy cursor and progressbar will be shown while loading images.
|
||||
:param open_group: ImageGroups object of the group that must be expanded after reloading the list in the
|
||||
interface.
|
||||
interface.
|
||||
"""
|
||||
if not initial_load:
|
||||
self.application.set_busy_cursor()
|
||||
@ -469,7 +469,7 @@ class ImageMediaItem(MediaManagerItem):
|
||||
:param images_list: A List of strings containing image filenames
|
||||
:param group_id: The ID of the group to save the images in
|
||||
:param reload_list: This boolean is set to True when the list in the interface should be reloaded after saving
|
||||
the new images
|
||||
the new images
|
||||
"""
|
||||
for filename in images_list:
|
||||
if not isinstance(filename, str):
|
||||
|
@ -43,11 +43,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CLAPPERBOARD = ':/media/slidecontroller_multimedia.png'
|
||||
OPTICAL = ':/media/media_optical.png'
|
||||
VIDEO_ICON = build_icon(':/media/media_video.png')
|
||||
AUDIO_ICON = build_icon(':/media/media_audio.png')
|
||||
OPTICAL_ICON = build_icon(OPTICAL)
|
||||
ERROR_ICON = build_icon(':/general/general_delete.png')
|
||||
|
||||
|
||||
class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
@ -57,10 +52,20 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
log.info('%s MediaMediaItem loaded', __name__)
|
||||
|
||||
def __init__(self, parent, plugin):
|
||||
self.setup()
|
||||
super(MediaMediaItem, self).__init__(parent, plugin)
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Allow early setup to be mocked.
|
||||
"""
|
||||
self.icon_path = 'images/image'
|
||||
self.background = False
|
||||
self.automatic = ''
|
||||
super(MediaMediaItem, self).__init__(parent, plugin)
|
||||
self.optical_icon = build_icon(':/media/media_optical.png')
|
||||
self.video_icon = build_icon(':/media/media_video.png')
|
||||
self.audio_icon = build_icon(':/media/media_audio.png')
|
||||
self.error_icon = build_icon(':/general/general_delete.png')
|
||||
|
||||
def setup_item(self):
|
||||
"""
|
||||
@ -130,7 +135,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD')
|
||||
optical_button_tooltip = translate('MediaPlugin.MediaItem',
|
||||
'Load CD/DVD - only supported when VLC is installed and enabled')
|
||||
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text=optical_button_text,
|
||||
self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=self.optical_icon,
|
||||
text=optical_button_text,
|
||||
tooltip=optical_button_tooltip,
|
||||
triggers=self.on_load_optical)
|
||||
if disable_optical_button_text:
|
||||
@ -351,14 +357,14 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
# Handle optical based item
|
||||
(file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track)
|
||||
item_name = QtGui.QListWidgetItem(clip_name)
|
||||
item_name.setIcon(OPTICAL_ICON)
|
||||
item_name.setIcon(self.optical_icon)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
item_name.setToolTip('%s@%s-%s' % (file_name, format_milliseconds(start), format_milliseconds(end)))
|
||||
elif not os.path.exists(track):
|
||||
# File doesn't exist, mark as error.
|
||||
file_name = os.path.split(str(track))[1]
|
||||
item_name = QtGui.QListWidgetItem(file_name)
|
||||
item_name.setIcon(ERROR_ICON)
|
||||
item_name.setIcon(self.error_icon)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
item_name.setToolTip(track)
|
||||
elif track_info.isFile():
|
||||
@ -366,9 +372,9 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties):
|
||||
file_name = os.path.split(str(track))[1]
|
||||
item_name = QtGui.QListWidgetItem(file_name)
|
||||
if '*.%s' % (file_name.split('.')[-1].lower()) in self.media_controller.audio_extensions_list:
|
||||
item_name.setIcon(AUDIO_ICON)
|
||||
item_name.setIcon(self.audio_icon)
|
||||
else:
|
||||
item_name.setIcon(VIDEO_ICON)
|
||||
item_name.setIcon(self.video_icon)
|
||||
item_name.setData(QtCore.Qt.UserRole, track)
|
||||
item_name.setToolTip(track)
|
||||
if item_name:
|
||||
|
@ -290,6 +290,13 @@ class MessageListener(object):
|
||||
log.info('Message Listener loaded')
|
||||
|
||||
def __init__(self, media_item):
|
||||
self._setup(media_item)
|
||||
|
||||
def _setup(self, media_item):
|
||||
"""
|
||||
Start up code moved out to make mocking easier
|
||||
:param media_item: The plugin media item handing Presentations
|
||||
"""
|
||||
self.controllers = media_item.controllers
|
||||
self.media_item = media_item
|
||||
self.preview_handler = Controller(False)
|
||||
@ -346,6 +353,12 @@ class MessageListener(object):
|
||||
self.handler = self.media_item.find_controller_by_type(file)
|
||||
if not self.handler:
|
||||
return
|
||||
else:
|
||||
# the saved handler is not present so need to use one based on file suffix.
|
||||
if not self.controllers[self.handler].available:
|
||||
self.handler = self.media_item.find_controller_by_type(file)
|
||||
if not self.handler:
|
||||
return
|
||||
if is_live:
|
||||
controller = self.live_handler
|
||||
else:
|
||||
|
@ -137,22 +137,6 @@ class PresentationPlugin(Plugin):
|
||||
self.register_controllers(controller)
|
||||
return bool(self.controllers)
|
||||
|
||||
def app_startup(self):
|
||||
"""
|
||||
Perform tasks on application startup.
|
||||
"""
|
||||
# TODO: Can be removed when the upgrade path from 2.0.x to 2.2.x is no longer needed
|
||||
super().app_startup()
|
||||
files_from_config = Settings().value('presentations/presentations files')
|
||||
for file in files_from_config:
|
||||
try:
|
||||
self.media_item.clean_up_thumbnails(file, True)
|
||||
except AttributeError:
|
||||
pass
|
||||
self.media_item.list_view.clear()
|
||||
Settings().setValue('presentations/thumbnail_scheme', 'md5')
|
||||
self.media_item.validate_and_load(files_from_config)
|
||||
|
||||
def about(self):
|
||||
"""
|
||||
Return information about this plugin.
|
||||
|
@ -271,9 +271,15 @@ window.OpenLP = {
|
||||
if (typeof value[0] !== "number"){
|
||||
value[0] = OpenLP.escapeString(value[0])
|
||||
}
|
||||
var txt = "";
|
||||
if (value[2].length > 0) {
|
||||
txt = value[1] + " ( " + value[2] + " )";
|
||||
} else {
|
||||
txt = value[1];
|
||||
}
|
||||
ul.append($("<li>").append($("<a>").attr("href", "#options")
|
||||
.attr("data-rel", "dialog").attr("value", value[0])
|
||||
.click(OpenLP.showOptions).text(value[1])));
|
||||
.click(OpenLP.showOptions).text(txt)));
|
||||
});
|
||||
}
|
||||
ul.listview("refresh");
|
||||
|
@ -150,6 +150,7 @@ class HttpRouter(RegistryProperties):
|
||||
self.routes = [
|
||||
('^/$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)$', {'function': self.serve_file, 'secure': False}),
|
||||
('^/(stage)/(.*)$', {'function': self.stages, 'secure': False}),
|
||||
('^/(main)$', {'function': self.serve_file, 'secure': False}),
|
||||
(r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}),
|
||||
(r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}),
|
||||
@ -170,6 +171,7 @@ class HttpRouter(RegistryProperties):
|
||||
self.settings_section = 'remotes'
|
||||
self.translate()
|
||||
self.html_dir = os.path.join(AppLocation.get_directory(AppLocation.PluginsDir), 'remotes', 'html')
|
||||
self.config_dir = os.path.join(AppLocation.get_data_path(), 'stages')
|
||||
|
||||
def do_post_processor(self):
|
||||
"""
|
||||
@ -309,10 +311,13 @@ class HttpRouter(RegistryProperties):
|
||||
"""
|
||||
Translate various strings in the mobile app.
|
||||
"""
|
||||
remote = translate('RemotePlugin.Mobile', 'Remote')
|
||||
stage = translate('RemotePlugin.Mobile', 'Stage View')
|
||||
live = translate('RemotePlugin.Mobile', 'Live View')
|
||||
self.template_vars = {
|
||||
'app_title': translate('RemotePlugin.Mobile', '%s Remote' % UiStrings().OLPV2x),
|
||||
'stage_title': translate('RemotePlugin.Mobile', '%s Stage View' % UiStrings().OLPV2x),
|
||||
'live_title': translate('RemotePlugin.Mobile', '%s Live View' % UiStrings().OLPV2x),
|
||||
'app_title': "%s %s" % (UiStrings().OLPV2x, remote),
|
||||
'stage_title': "%s %s" % (UiStrings().OLPV2x, stage),
|
||||
'live_title': "%s %s" % (UiStrings().OLPV2x, live),
|
||||
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
|
||||
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
|
||||
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
|
||||
@ -337,24 +342,32 @@ class HttpRouter(RegistryProperties):
|
||||
'settings': translate('RemotePlugin.Mobile', 'Settings'),
|
||||
}
|
||||
|
||||
def serve_file(self, file_name=None):
|
||||
def stages(self, url_path, file_name):
|
||||
"""
|
||||
Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
|
||||
If subfolders requested return 404, easier for security for the present.
|
||||
Allow Stage view to be delivered with custom views.
|
||||
|
||||
Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
|
||||
where xx is the language, e.g. 'en'
|
||||
:param url_path: base path of the URL. Not used but passed by caller
|
||||
:param file_name: file name with path
|
||||
:return:
|
||||
"""
|
||||
log.debug('serve file request %s' % file_name)
|
||||
if not file_name:
|
||||
file_name = 'index.html'
|
||||
elif file_name == 'stage':
|
||||
file_name = 'stage.html'
|
||||
elif file_name == 'main':
|
||||
file_name = 'main.html'
|
||||
path = os.path.normpath(os.path.join(self.html_dir, file_name))
|
||||
if not path.startswith(self.html_dir):
|
||||
parts = file_name.split('/')
|
||||
if len(parts) == 1:
|
||||
file_name = os.path.join(parts[0], 'stage.html')
|
||||
elif len(parts) == 3:
|
||||
file_name = os.path.join(parts[1], parts[2])
|
||||
path = os.path.normpath(os.path.join(self.config_dir, file_name))
|
||||
if not path.startswith(self.config_dir):
|
||||
return self.do_not_found()
|
||||
return self._process_file(path)
|
||||
|
||||
def _process_file(self, path):
|
||||
"""
|
||||
Common file processing code
|
||||
|
||||
:param path: path to file to be loaded
|
||||
:return: web resource to be loaded
|
||||
"""
|
||||
content = None
|
||||
ext, content_type = self.get_content_type(path)
|
||||
file_handle = None
|
||||
@ -377,10 +390,32 @@ class HttpRouter(RegistryProperties):
|
||||
self.end_headers()
|
||||
return content
|
||||
|
||||
def serve_file(self, file_name=None):
|
||||
"""
|
||||
Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
|
||||
If subfolders requested return 404, easier for security for the present.
|
||||
|
||||
Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
|
||||
where xx is the language, e.g. 'en'
|
||||
"""
|
||||
log.debug('serve file request %s' % file_name)
|
||||
if not file_name:
|
||||
file_name = 'index.html'
|
||||
elif file_name == 'stage':
|
||||
file_name = 'stage.html'
|
||||
elif file_name == 'main':
|
||||
file_name = 'main.html'
|
||||
path = os.path.normpath(os.path.join(self.html_dir, file_name))
|
||||
if not path.startswith(self.html_dir):
|
||||
return self.do_not_found()
|
||||
return self._process_file(path)
|
||||
|
||||
def get_content_type(self, file_name):
|
||||
"""
|
||||
Examines the extension of the file and determines what the content_type should be, defaults to text/plain
|
||||
Returns the extension and the content_type
|
||||
|
||||
:param file_name: name of file
|
||||
"""
|
||||
ext = os.path.splitext(file_name)[1]
|
||||
content_type = FILE_TYPES.get(ext, 'text/plain')
|
||||
@ -389,6 +424,10 @@ class HttpRouter(RegistryProperties):
|
||||
def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None):
|
||||
"""
|
||||
Serve an image file. If not found return 404.
|
||||
|
||||
:param file_name: file name to be served
|
||||
:param dimensions: image size
|
||||
:param controller_name: controller to be called
|
||||
"""
|
||||
log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name))
|
||||
supported_controllers = ['presentations', 'images']
|
||||
@ -493,6 +532,8 @@ class HttpRouter(RegistryProperties):
|
||||
def controller_text(self, var):
|
||||
"""
|
||||
Perform an action on the slide controller.
|
||||
|
||||
:param var: variable - not used
|
||||
"""
|
||||
log.debug("controller_text var = %s" % var)
|
||||
current_item = self.live_controller.service_item
|
||||
@ -626,6 +667,8 @@ class HttpRouter(RegistryProperties):
|
||||
def go_live(self, plugin_name):
|
||||
"""
|
||||
Go live on an item of type ``plugin``.
|
||||
|
||||
:param plugin_name: name of plugin
|
||||
"""
|
||||
try:
|
||||
request_id = json.loads(self.request_data)['request']['id']
|
||||
@ -639,6 +682,8 @@ class HttpRouter(RegistryProperties):
|
||||
def add_to_service(self, plugin_name):
|
||||
"""
|
||||
Add item of type ``plugin_name`` to the end of the service.
|
||||
|
||||
:param plugin_name: name of plugin to be called
|
||||
"""
|
||||
try:
|
||||
request_id = json.loads(self.request_data)['request']['id']
|
||||
|
@ -167,7 +167,7 @@ class HTTPSServer(HTTPServer):
|
||||
local_data = AppLocation.get_directory(AppLocation.DataDir)
|
||||
self.socket = ssl.SSLSocket(
|
||||
sock=socket.socket(self.address_family, self.socket_type),
|
||||
ssl_version=ssl.PROTOCOL_TLSv1,
|
||||
ssl_version=ssl.PROTOCOL_TLSv1_2,
|
||||
certfile=os.path.join(local_data, 'remotes', 'openlp.crt'),
|
||||
keyfile=os.path.join(local_data, 'remotes', 'openlp.key'),
|
||||
server_side=True)
|
||||
|
@ -177,8 +177,9 @@ class RemoteTab(SettingsTab):
|
||||
'Show thumbnails of non-text slides in remote and stage view.'))
|
||||
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
|
||||
self.qr_description_label.setText(
|
||||
translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="https://play.google.com/store/'
|
||||
'apps/details?id=org.openlp.android">download</a> to install the Android app from Google Play.'))
|
||||
translate('RemotePlugin.RemoteTab', 'Scan the QR code or click <a href="%s">download</a> to install the '
|
||||
'Android app from Google Play.') %
|
||||
'https://play.google.com/store/apps/details?id=org.openlp.android2')
|
||||
self.https_settings_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'HTTPS Server'))
|
||||
self.https_error_label.setText(
|
||||
translate('RemotePlugin.RemoteTab', 'Could not find an SSL certificate. The HTTPS server will not be '
|
||||
|
@ -178,7 +178,7 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog, RegistryProperties):
|
||||
if invalid_verses:
|
||||
valid = create_separated_list(verse_names)
|
||||
if len(invalid_verses) > 1:
|
||||
msg = translate('SongsPlugin.EditSongForm', 'There are no verses corresponding to "%(invalid)s".'
|
||||
msg = translate('SongsPlugin.EditSongForm', 'There are no verses corresponding to "%(invalid)s". '
|
||||
'Valid entries are %(valid)s.\nPlease enter the verses separated by spaces.') % \
|
||||
{'invalid': ', '.join(invalid_verses), 'valid': valid}
|
||||
else:
|
||||
|
@ -228,6 +228,7 @@ class SongImportForm(OpenLPWizard, RegistryProperties):
|
||||
:param listbox: A listbox (QListWidget).
|
||||
:param filters: The file extension filters. It should contain the file descriptions as well as the file
|
||||
extensions. For example::
|
||||
|
||||
'SongBeamer Files (*.sng)'
|
||||
"""
|
||||
if filters:
|
||||
|
@ -173,6 +173,7 @@ def init_schema(url):
|
||||
Setup the songs database connection and initialise the database schema.
|
||||
|
||||
:param url: The database to setup
|
||||
|
||||
The song database contains the following tables:
|
||||
|
||||
* authors
|
||||
|
@ -36,7 +36,7 @@ class EasySlidesImport(SongImport):
|
||||
Import songs exported from EasySlides
|
||||
|
||||
The format example is here:
|
||||
http://wiki.openlp.org/Development:EasySlides_-_Song_Data_Format
|
||||
http://wiki.openlp.org/Development:EasySlides\_-_Song_Data_Format
|
||||
"""
|
||||
def __init__(self, manager, **kwargs):
|
||||
"""
|
||||
|
@ -171,7 +171,7 @@ class OpenOfficeImport(SongImport):
|
||||
"""
|
||||
log.debug('create property OpenOffice')
|
||||
if is_win():
|
||||
property_object = self.controller.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue')
|
||||
property_object = self.ooo_manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue')
|
||||
else:
|
||||
property_object = PropertyValue()
|
||||
property_object.Name = name
|
||||
|
@ -71,7 +71,7 @@ class PowerSongImport(SongImport):
|
||||
"""
|
||||
Checks if source is a PowerSong 1.0 folder:
|
||||
* is a directory
|
||||
* contains at least one *.song file
|
||||
* contains at least one \*.song file
|
||||
"""
|
||||
if os.path.isdir(import_source):
|
||||
for file in os.listdir(import_source):
|
||||
|
@ -105,7 +105,7 @@ class SongImport(QtCore.QObject):
|
||||
This should be called, when a song could not be imported.
|
||||
|
||||
:param file_path: This should be the file path if ``self.import_source`` is a list with different files. If it
|
||||
is not a list, but a single file (for instance a database), then this should be the song's title.
|
||||
is not a list, but a single file (for instance a database), then this should be the song's title.
|
||||
:param reason: The reason why the import failed. The string should be as informative as possible.
|
||||
"""
|
||||
self.set_defaults()
|
||||
|
@ -36,50 +36,49 @@ class SongProImport(SongImport):
|
||||
|
||||
**SongPro Song File Format:**
|
||||
|
||||
SongPro has the option to export under its File menu
|
||||
This produces files containing single or multiple songs
|
||||
The file is text with lines tagged with # followed by an identifier.
|
||||
This is documented here: http://creationsoftware.com/ImportIdentifiers.php
|
||||
An example here: http://creationsoftware.com/ExampleImportingManySongs.txt
|
||||
|
||||
#A - next line is the Song Author
|
||||
#B - the lines following until next tagged line are the "Bridge" words
|
||||
(can be in rtf or plain text) which we map as B1
|
||||
#C - the lines following until next tagged line are the chorus words
|
||||
(can be in rtf or plain text)
|
||||
which we map as C1
|
||||
#D - the lines following until next tagged line are the "Ending" words
|
||||
(can be in rtf or plain text) which we map as E1
|
||||
#E - this song ends here, so we process the song -
|
||||
and start again at the next line
|
||||
#G - next line is the Group
|
||||
#M - next line is the Song Number
|
||||
#N - next line are Notes
|
||||
#R - next line is the SongCopyright
|
||||
#O - next line is the Verse Sequence
|
||||
#T - next line is the Song Title
|
||||
#1 - #7 the lines following until next tagged line are the verse x words
|
||||
(can be in rtf or plain text)
|
||||
| SongPro has the option to export under its File menu
|
||||
| This produces files containing single or multiple songs
|
||||
| The file is text with lines tagged with # followed by an identifier.
|
||||
| This is documented here: http://creationsoftware.com/ImportIdentifiers.php
|
||||
| An example here: http://creationsoftware.com/ExampleImportingManySongs.txt
|
||||
|
|
||||
| #A - next line is the Song Author
|
||||
| #B - the lines following until next tagged line are the "Bridge" words
|
||||
| (can be in rtf or plain text) which we map as B1
|
||||
| #C - the lines following until next tagged line are the chorus words
|
||||
| (can be in rtf or plain text)
|
||||
| which we map as C1
|
||||
| #D - the lines following until next tagged line are the "Ending" words
|
||||
| (can be in rtf or plain text) which we map as E1
|
||||
| #E - this song ends here, so we process the song -
|
||||
| and start again at the next line
|
||||
| #G - next line is the Group
|
||||
| #M - next line is the Song Number
|
||||
| #N - next line are Notes
|
||||
| #R - next line is the SongCopyright
|
||||
| #O - next line is the Verse Sequence
|
||||
| #T - next line is the Song Title
|
||||
| #1 - #7 the lines following until next tagged line are the verse x words
|
||||
| (can be in rtf or plain text)
|
||||
"""
|
||||
def __init__(self, manager, **kwargs):
|
||||
"""
|
||||
Initialise the SongPro importer.
|
||||
"""
|
||||
SongImport.__init__(self, manager, **kwargs)
|
||||
super(SongProImport, self).__init__(manager, **kwargs)
|
||||
|
||||
def do_import(self):
|
||||
"""
|
||||
Receive a single file or a list of files to import.
|
||||
"""
|
||||
self.encoding = None
|
||||
with open(self.import_source, 'r') as songs_file:
|
||||
with open(self.import_source, 'rt') as songs_file:
|
||||
self.import_wizard.progress_bar.setMaximum(0)
|
||||
tag = ''
|
||||
text = ''
|
||||
for file_line in songs_file:
|
||||
if self.stop_import_flag:
|
||||
break
|
||||
file_line = str(file_line, 'cp1252')
|
||||
file_text = file_line.rstrip()
|
||||
if file_text and file_text[0] == '#':
|
||||
self.process_section(tag, text.rstrip())
|
||||
@ -87,6 +86,7 @@ class SongProImport(SongImport):
|
||||
text = ''
|
||||
else:
|
||||
text += file_line
|
||||
self.finish()
|
||||
|
||||
def process_section(self, tag, text):
|
||||
"""
|
||||
|
@ -60,22 +60,22 @@ class SongShowPlusImport(SongImport):
|
||||
|
||||
* Each piece of data in the song file has some information that precedes it.
|
||||
* The general format of this data is as follows:
|
||||
4 Bytes, forming a 32 bit number, a key if you will, this describes what the data is (see blockKey below)
|
||||
4 Bytes, forming a 32 bit number, which is the number of bytes until the next block starts
|
||||
1 Byte, which tells how many bytes follows
|
||||
1 or 4 Bytes, describes how long the string is, if its 1 byte, the string is less than 255
|
||||
The next bytes are the actual data.
|
||||
The next block of data follows on.
|
||||
| 4 Bytes, forming a 32 bit number, a key if you will, this describes what the data is (see blockKey below)
|
||||
| 4 Bytes, forming a 32 bit number, which is the number of bytes until the next block starts
|
||||
| 1 Byte, which tells how many bytes follows
|
||||
| 1 or 4 Bytes, describes how long the string is, if its 1 byte, the string is less than 255
|
||||
| The next bytes are the actual data.
|
||||
| The next block of data follows on.
|
||||
|
||||
This description does differ for verses. Which includes extra bytes stating the verse type or number. In some cases
|
||||
a "custom" verse is used, in that case, this block will in include 2 strings, with the associated string length
|
||||
descriptors. The first string is the name of the verse, the second is the verse content.
|
||||
This description does differ for verses. Which includes extra bytes stating the verse type or number. In some
|
||||
cases a "custom" verse is used, in that case, this block will in include 2 strings, with the associated string
|
||||
length descriptors. The first string is the name of the verse, the second is the verse content.
|
||||
|
||||
The file is ended with four null bytes.
|
||||
The file is ended with four null bytes.
|
||||
|
||||
Valid extensions for a SongShow Plus song file are:
|
||||
Valid extensions for a SongShow Plus song file are:
|
||||
|
||||
* .sbsong
|
||||
* .sbsong
|
||||
"""
|
||||
|
||||
other_count = 0
|
||||
|
@ -24,7 +24,7 @@ The :mod:`worshipcenterpro` module provides the functionality for importing
|
||||
a WorshipCenter Pro database into the OpenLP database.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import re
|
||||
import pyodbc
|
||||
|
||||
from openlp.core.common import translate
|
||||
@ -71,8 +71,41 @@ class WorshipCenterProImport(SongImport):
|
||||
break
|
||||
self.set_defaults()
|
||||
self.title = songs[song]['TITLE']
|
||||
if 'AUTHOR' in songs[song]:
|
||||
self.parse_author(songs[song]['AUTHOR'])
|
||||
if 'CCLISONGID' in songs[song]:
|
||||
self.ccli_number = songs[song]['CCLISONGID']
|
||||
if 'COMMENTS' in songs[song]:
|
||||
self.add_comment(songs[song]['COMMENTS'])
|
||||
if 'COPY' in songs[song]:
|
||||
self.add_copyright(songs[song]['COPY'])
|
||||
if 'SUBJECT' in songs[song]:
|
||||
self.topics.append(songs[song]['SUBJECT'])
|
||||
lyrics = songs[song]['LYRICS'].strip('&crlf;&crlf;')
|
||||
for verse in lyrics.split('&crlf;&crlf;'):
|
||||
verse = verse.replace('&crlf;', '\n')
|
||||
self.add_verse(verse)
|
||||
marker_type = 'v'
|
||||
# Find verse markers if any
|
||||
marker_start = verse.find('<')
|
||||
if marker_start > -1:
|
||||
marker_end = verse.find('>')
|
||||
marker = verse[marker_start + 1:marker_end]
|
||||
# Identify the marker type
|
||||
if 'REFRAIN' in marker or 'CHORUS' in marker:
|
||||
marker_type = 'c'
|
||||
elif 'BRIDGE' in marker:
|
||||
marker_type = 'b'
|
||||
elif 'PRECHORUS' in marker:
|
||||
marker_type = 'p'
|
||||
elif 'END' in marker:
|
||||
marker_type = 'e'
|
||||
elif 'INTRO' in marker:
|
||||
marker_type = 'i'
|
||||
elif 'TAG' in marker:
|
||||
marker_type = 'o'
|
||||
else:
|
||||
marker_type = 'v'
|
||||
# Strip tags from text
|
||||
verse = re.sub('<[^<]+?>', '', verse)
|
||||
self.add_verse(verse.strip(), marker_type)
|
||||
self.finish()
|
||||
|
@ -75,7 +75,8 @@ class ZionWorxImport(SongImport):
|
||||
"""
|
||||
Receive a CSV file (from a ZionWorx database dump) to import.
|
||||
"""
|
||||
with open(self.import_source, 'rb') as songs_file:
|
||||
# Encoding should always be ISO-8859-1
|
||||
with open(self.import_source, 'rt', encoding='ISO-8859-1') as songs_file:
|
||||
field_names = ['SongNum', 'Title1', 'Title2', 'Lyrics', 'Writer', 'Copyright', 'Keywords',
|
||||
'DefaultStyle']
|
||||
songs_reader = csv.DictReader(songs_file, field_names)
|
||||
@ -112,10 +113,10 @@ class ZionWorxImport(SongImport):
|
||||
if line and not line.isspace():
|
||||
verse += line + '\n'
|
||||
elif verse:
|
||||
self.add_verse(verse)
|
||||
self.add_verse(verse, 'v')
|
||||
verse = ''
|
||||
if verse:
|
||||
self.add_verse(verse)
|
||||
self.add_verse(verse, 'v')
|
||||
title = self.title
|
||||
if not self.finish():
|
||||
self.log_error(translate('SongsPlugin.ZionWorxImport', 'Record %d') % index +
|
||||
@ -123,8 +124,7 @@ class ZionWorxImport(SongImport):
|
||||
|
||||
def _decode(self, str):
|
||||
"""
|
||||
Decodes CSV input to unicode, stripping all control characters (except new lines).
|
||||
Strips all control characters (except new lines).
|
||||
"""
|
||||
# This encoding choice seems OK. ZionWorx has no option for setting the
|
||||
# encoding for its songs, so we assume encoding is always the same.
|
||||
return str(str, 'cp1252').translate(CONTROL_CHARS_MAP)
|
||||
# ZionWorx has no option for setting the encoding for its songs, so we assume encoding is always the same.
|
||||
return str.translate(CONTROL_CHARS_MAP)
|
||||
|
@ -115,7 +115,7 @@ class SongMediaItem(MediaManagerItem):
|
||||
Is triggered when the songs config is updated
|
||||
"""
|
||||
log.debug('config_updated')
|
||||
self.search_as_you_type = Settings().value(self.settings_section + '/search as type')
|
||||
self.is_search_as_you_type_enabled = Settings().value('advanced/search as type')
|
||||
self.update_service_on_edit = Settings().value(self.settings_section + '/update service on edit')
|
||||
self.add_song_from_service = Settings().value(self.settings_section + '/add song from service')
|
||||
self.display_songbook = Settings().value(self.settings_section + '/display songbook')
|
||||
@ -279,7 +279,7 @@ class SongMediaItem(MediaManagerItem):
|
||||
If search as type enabled invoke the search on each key press. If the Lyrics are being searched do not start
|
||||
till 7 characters have been entered.
|
||||
"""
|
||||
if self.search_as_you_type:
|
||||
if self.is_search_as_you_type_enabled:
|
||||
search_length = 1
|
||||
if self.search_text_edit.current_search_type() == SongSearch.Entire:
|
||||
search_length = 4
|
||||
@ -590,4 +590,4 @@ class SongMediaItem(MediaManagerItem):
|
||||
:param show_error: Is this an error?
|
||||
"""
|
||||
search_results = self.search_entire(string)
|
||||
return [[song.id, song.title] for song in search_results]
|
||||
return [[song.id, song.title, song.alternate_title] for song in search_results]
|
||||
|
@ -91,7 +91,7 @@ class SongXML(object):
|
||||
Add a verse to the ``<lyrics>`` tag.
|
||||
|
||||
:param type: A string denoting the type of verse. Possible values are *v*, *c*, *b*, *p*, *i*, *e* and *o*.
|
||||
Any other type is **not** allowed, this also includes translated types.
|
||||
Any other type is **not** allowed, this also includes translated types.
|
||||
:param number: An integer denoting the number of the item, for example: verse 1.
|
||||
:param content: The actual text of the verse to be stored.
|
||||
:param lang: The verse's language code (ISO-639). This is not required, but should be added if available.
|
||||
@ -113,6 +113,7 @@ class SongXML(object):
|
||||
Iterates through the verses in the XML and returns a list of verses and their attributes.
|
||||
|
||||
:param xml: The XML of the song to be parsed.
|
||||
|
||||
The returned list has the following format::
|
||||
|
||||
[[{'type': 'v', 'label': '1'}, u"optional slide split 1[---]optional slide split 2"],
|
||||
@ -120,17 +121,7 @@ class SongXML(object):
|
||||
"""
|
||||
self.song_xml = None
|
||||
verse_list = []
|
||||
if not xml.startswith('<?xml') and not xml.startswith('<song'):
|
||||
# This is an old style song, without XML. Let's handle it correctly by iterating through the verses, and
|
||||
# then recreating the internal xml object as well.
|
||||
self.song_xml = objectify.fromstring('<song version="1.0" />')
|
||||
self.lyrics = etree.SubElement(self.song_xml, 'lyrics')
|
||||
verses = xml.split('\n\n')
|
||||
for count, verse in enumerate(verses):
|
||||
verse_list.append([{'type': 'v', 'label': str(count)}, str(verse)])
|
||||
self.add_verse_to_lyrics('v', str(count), verse)
|
||||
return verse_list
|
||||
elif xml.startswith('<?xml'):
|
||||
if xml.startswith('<?xml'):
|
||||
xml = xml[38:]
|
||||
try:
|
||||
self.song_xml = objectify.fromstring(xml)
|
||||
@ -371,7 +362,7 @@ class OpenLyrics(object):
|
||||
|
||||
:param xml: The XML to parse (unicode).
|
||||
:param parse_and_temporary_save: Switch to skip processing the whole song and storing the songs in the database
|
||||
with a temporary flag. Defaults to ``False``.
|
||||
with a temporary flag. Defaults to ``False``.
|
||||
"""
|
||||
# No xml get out of here.
|
||||
if not xml:
|
||||
|
@ -21,20 +21,21 @@
|
||||
###############################################################################
|
||||
"""
|
||||
The :mod:`songcompare` module provides functionality to search for
|
||||
duplicate songs. It has one single :function:`songs_probably_equal`.
|
||||
duplicate songs. It has one single :func:`songs_probably_equal`.
|
||||
|
||||
The algorithm is based on the diff algorithm.
|
||||
First a diffset is calculated for two songs.
|
||||
To compensate for typos all differences that are smaller than a
|
||||
limit (<max_typo_size) and are surrounded by larger equal blocks
|
||||
(>min_fragment_size) are removed and the surrounding equal parts are merged.
|
||||
Finally two conditions can qualify a song tuple to be a duplicate:
|
||||
1. There is a block of equal content that is at least min_block_size large.
|
||||
This condition should hit for all larger songs that have a long enough
|
||||
equal part. Even if only one verse is equal this condition should still hit.
|
||||
2. Two thirds of the smaller song is contained in the larger song.
|
||||
This condition should hit if one of the two songs (or both) is small (smaller
|
||||
than the min_block_size), but most of the song is contained in the other song.
|
||||
| The algorithm is based on the diff algorithm.
|
||||
| First a diffset is calculated for two songs.
|
||||
| To compensate for typos all differences that are smaller than a
|
||||
limit (<max_typo_size) and are surrounded by larger equal blocks
|
||||
(>min_fragment_size) are removed and the surrounding equal parts are merged.
|
||||
| Finally two conditions can qualify a song tuple to be a duplicate:
|
||||
|
||||
1. There is a block of equal content that is at least min_block_size large.
|
||||
This condition should hit for all larger songs that have a long enough
|
||||
equal part. Even if only one verse is equal this condition should still hit.
|
||||
2. Two thirds of the smaller song is contained in the larger song.
|
||||
This condition should hit if one of the two songs (or both) is small (smaller
|
||||
than the min_block_size), but most of the song is contained in the other song.
|
||||
"""
|
||||
|
||||
import difflib
|
||||
|
@ -23,11 +23,12 @@
|
||||
The :mod:`~openlp.plugins.songs.lib.songselect` module contains the SongSelect importer itself.
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from http.cookiejar import CookieJar
|
||||
from urllib.parse import urlencode
|
||||
from urllib.request import HTTPCookieProcessor, URLError, build_opener
|
||||
from html.parser import HTMLParser
|
||||
|
||||
from html import unescape
|
||||
|
||||
from bs4 import BeautifulSoup, NavigableString
|
||||
|
||||
@ -130,8 +131,8 @@ class SongSelectImport(object):
|
||||
break
|
||||
for result in search_results:
|
||||
song = {
|
||||
'title': self.html_parser.unescape(result.find('h3').string),
|
||||
'authors': [self.html_parser.unescape(author.string) for author in result.find_all('li')],
|
||||
'title': unescape(result.find('h3').string),
|
||||
'authors': [unescape(author.string) for author in result.find_all('li')],
|
||||
'link': BASE_URL + result.find('a')['href']
|
||||
}
|
||||
if callback:
|
||||
@ -167,7 +168,7 @@ class SongSelectImport(object):
|
||||
if callback:
|
||||
callback()
|
||||
song['copyright'] = '/'.join([li.string for li in song_page.find('ul', 'copyright').find_all('li')])
|
||||
song['copyright'] = self.html_parser.unescape(song['copyright'])
|
||||
song['copyright'] = unescape(song['copyright'])
|
||||
song['ccli_number'] = song_page.find('ul', 'info').find('li').string.split(':')[1].strip()
|
||||
song['verses'] = []
|
||||
verses = lyrics_page.find('section', 'lyrics').find_all('p')
|
||||
@ -180,9 +181,9 @@ class SongSelectImport(object):
|
||||
else:
|
||||
verse['lyrics'] += '\n'
|
||||
verse['lyrics'] = verse['lyrics'].strip(' \n\r\t')
|
||||
song['verses'].append(self.html_parser.unescape(verse))
|
||||
song['verses'].append(unescape(verse))
|
||||
for counter, author in enumerate(song['authors']):
|
||||
song['authors'][counter] = self.html_parser.unescape(author)
|
||||
song['authors'][counter] = unescape(author)
|
||||
return song
|
||||
|
||||
def save_song(self, song):
|
||||
|
@ -41,9 +41,6 @@ class SongsTab(SettingsTab):
|
||||
self.mode_group_box.setObjectName('mode_group_box')
|
||||
self.mode_layout = QtGui.QVBoxLayout(self.mode_group_box)
|
||||
self.mode_layout.setObjectName('mode_layout')
|
||||
self.search_as_type_check_box = QtGui.QCheckBox(self.mode_group_box)
|
||||
self.search_as_type_check_box.setObjectName('SearchAsType_check_box')
|
||||
self.mode_layout.addWidget(self.search_as_type_check_box)
|
||||
self.tool_bar_active_check_box = QtGui.QCheckBox(self.mode_group_box)
|
||||
self.tool_bar_active_check_box.setObjectName('tool_bar_active_check_box')
|
||||
self.mode_layout.addWidget(self.tool_bar_active_check_box)
|
||||
@ -62,7 +59,6 @@ class SongsTab(SettingsTab):
|
||||
self.left_layout.addWidget(self.mode_group_box)
|
||||
self.left_layout.addStretch()
|
||||
self.right_layout.addStretch()
|
||||
self.search_as_type_check_box.stateChanged.connect(self.on_search_as_type_check_box_changed)
|
||||
self.tool_bar_active_check_box.stateChanged.connect(self.on_tool_bar_active_check_box_changed)
|
||||
self.update_on_edit_check_box.stateChanged.connect(self.on_update_on_edit_check_box_changed)
|
||||
self.add_from_service_check_box.stateChanged.connect(self.on_add_from_service_check_box_changed)
|
||||
@ -71,7 +67,6 @@ class SongsTab(SettingsTab):
|
||||
|
||||
def retranslateUi(self):
|
||||
self.mode_group_box.setTitle(translate('SongsPlugin.SongsTab', 'Songs Mode'))
|
||||
self.search_as_type_check_box.setText(translate('SongsPlugin.SongsTab', 'Enable search as you type'))
|
||||
self.tool_bar_active_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||
'Display verses on live tool bar'))
|
||||
self.update_on_edit_check_box.setText(translate('SongsPlugin.SongsTab', 'Update service from song edit'))
|
||||
@ -79,8 +74,8 @@ class SongsTab(SettingsTab):
|
||||
'Import missing songs from service files'))
|
||||
self.display_songbook_check_box.setText(translate('SongsPlugin.SongsTab', 'Display songbook in footer'))
|
||||
self.display_copyright_check_box.setText(translate('SongsPlugin.SongsTab',
|
||||
'Display "%s" symbol before copyright info' %
|
||||
SongStrings.CopyrightSymbol))
|
||||
'Display "%s" symbol before copyright info') %
|
||||
SongStrings.CopyrightSymbol)
|
||||
|
||||
def on_search_as_type_check_box_changed(self, check_state):
|
||||
self.song_search = (check_state == QtCore.Qt.Checked)
|
||||
@ -103,13 +98,11 @@ class SongsTab(SettingsTab):
|
||||
def load(self):
|
||||
settings = Settings()
|
||||
settings.beginGroup(self.settings_section)
|
||||
self.song_search = settings.value('search as type')
|
||||
self.tool_bar = settings.value('display songbar')
|
||||
self.update_edit = settings.value('update service on edit')
|
||||
self.update_load = settings.value('add song from service')
|
||||
self.display_songbook = settings.value('display songbook')
|
||||
self.display_copyright_symbol = settings.value('display copyright symbol')
|
||||
self.search_as_type_check_box.setChecked(self.song_search)
|
||||
self.tool_bar_active_check_box.setChecked(self.tool_bar)
|
||||
self.update_on_edit_check_box.setChecked(self.update_edit)
|
||||
self.add_from_service_check_box.setChecked(self.update_load)
|
||||
@ -120,7 +113,6 @@ class SongsTab(SettingsTab):
|
||||
def save(self):
|
||||
settings = Settings()
|
||||
settings.beginGroup(self.settings_section)
|
||||
settings.setValue('search as type', self.song_search)
|
||||
settings.setValue('display songbar', self.tool_bar)
|
||||
settings.setValue('update service on edit', self.update_edit)
|
||||
settings.setValue('add song from service', self.update_load)
|
||||
|
@ -57,7 +57,6 @@ __default_settings__ = {
|
||||
'songs/last search type': SongSearch.Entire,
|
||||
'songs/last import type': SongFormat.OpenLyrics,
|
||||
'songs/update service on edit': False,
|
||||
'songs/search as type': True,
|
||||
'songs/add song from service': True,
|
||||
'songs/display songbar': True,
|
||||
'songs/display songbook': False,
|
||||
@ -226,11 +225,9 @@ class SongsPlugin(Plugin):
|
||||
Called to find out if the song plugin is currently using a theme.
|
||||
|
||||
:param theme: The theme to check for usage
|
||||
:return: True if the theme is being used, otherwise returns False
|
||||
:return: count of the number of times the theme is used.
|
||||
"""
|
||||
if self.manager.get_all_objects(Song, Song.theme_name == theme):
|
||||
return True
|
||||
return False
|
||||
return len(self.manager.get_all_objects(Song, Song.theme_name == theme))
|
||||
|
||||
def rename_theme(self, old_theme, new_theme):
|
||||
"""
|
||||
|
1358
resources/i18n/af.ts
1358
resources/i18n/af.ts
File diff suppressed because it is too large
Load Diff
1070
resources/i18n/bg.ts
1070
resources/i18n/bg.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1366
resources/i18n/el.ts
1366
resources/i18n/el.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1398
resources/i18n/fi.ts
1398
resources/i18n/fi.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1506
resources/i18n/ko.ts
1506
resources/i18n/ko.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 766 B |
151
scripts/lp-merge.py
Executable file
151
scripts/lp-merge.py
Executable file
@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2015 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
"""
|
||||
This script is used by developers to merge branches from launchpad.net into
|
||||
other branches, which is basically what happens every time a merge proposal
|
||||
is merged into trunk. This script simplifies the process and helps avoiding
|
||||
merging to the wrong branch by doing some checks.
|
||||
For the script to work it is assumed that the developer doing the merging
|
||||
has a checkout (not branch) of the target branch.
|
||||
|
||||
Merge a branch
|
||||
--------------
|
||||
Once a branch has been approved for merging, go to the local folder with the
|
||||
checkout of the target branch, copy the url from the merge proposal on launchpad
|
||||
and use it like this:
|
||||
|
||||
script/lp-merge.py <url>
|
||||
|
||||
The url could look like this:
|
||||
https://code.launchpad.net/~tomasgroth/openlp/doc22update4/+merge/271874
|
||||
|
||||
First you'll be asked whether to merge the branch from the url to the local
|
||||
branch, which will be done if you answer 'y'. The script checks that the current
|
||||
folder is the right one before merging.
|
||||
Then you'll be asked whether to commit the changes to the branch. The script
|
||||
shows the command it will run, including detected linked bugs and author. If you
|
||||
wish to change it, you can chose to run 'qcommit', an bzr GUI. Note that you'll
|
||||
have to install qbzr for this to work. If you choose to run qcommit the script
|
||||
will print the detected bugs + author for easy copying into the GUI.
|
||||
"""
|
||||
import subprocess
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
import urllib.request
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# Check that the argument count is correct
|
||||
if len(sys.argv) != 2:
|
||||
print('\n\tUsage: ' + sys.argv[0] + ' <url to approved merge request>\n')
|
||||
exit()
|
||||
|
||||
url = sys.argv[1]
|
||||
pattern = re.compile('.+?/\+merge/\d+')
|
||||
match = pattern.match(url)
|
||||
|
||||
# Check that the given argument is an url in the right format
|
||||
if not url.startswith('https://code.launchpad.net/~') or match is None:
|
||||
print('The url is not valid! It should look like this:\n '
|
||||
'https://code.launchpad.net/~tomasgroth/openlp/doc22update4/+merge/271874')
|
||||
|
||||
page = urllib.request.urlopen(url)
|
||||
soup = BeautifulSoup(page.read(), 'lxml')
|
||||
|
||||
# Find this span tag that contains the branch url
|
||||
# <span class="branch-url">
|
||||
span_branch_url = soup.find('span', class_='branch-url')
|
||||
branch_url = span_branch_url.contents[0]
|
||||
|
||||
# Find this tag that describes the branch. We'll use that for commit message
|
||||
# <meta name="description" content="...">
|
||||
meta = soup.find('meta', attrs={"name": "description"})
|
||||
|
||||
commit_message = meta.get('content')
|
||||
|
||||
# Find all tr-tags with this class. Makes it possible to get bug numbers.
|
||||
# <tr class="bug-branch-summary"
|
||||
bug_rows = soup.find_all('tr', class_='bug-branch-summary')
|
||||
bugs = []
|
||||
for row in bug_rows:
|
||||
id_attr = row.get('id')
|
||||
bugs.append(id_attr[8:])
|
||||
|
||||
# Find target branch name using the tag below
|
||||
# <div class="context-publication"><h1>Merge ... into...
|
||||
div_branches = soup.find('div', class_='context-publication')
|
||||
branches = div_branches.h1.contents[0]
|
||||
target_branch = '+branch/' + branches[(branches.find(' into lp:')+9):]
|
||||
|
||||
# Check that we are in the right branch
|
||||
bzr_info_output = subprocess.check_output(['bzr', 'info'])
|
||||
if target_branch not in bzr_info_output.decode():
|
||||
print('ERROR: It seems you are not in the right folder...')
|
||||
exit()
|
||||
|
||||
# Find the authors email address. It is hidden in a javascript line like this:
|
||||
# conf = {"status_value": "Needs review", "source_revid": "tomasgroth@yahoo.dk-20150921204550-gxduegmcmty9rljf",
|
||||
# "user_can_edit_status": false, ...
|
||||
script_tag = soup.find('script', attrs={"id": "codereview-script"})
|
||||
content = script_tag.contents[0]
|
||||
start_pos = content.find('source_revid') + 16
|
||||
pattern = re.compile('.*\w-\d\d\d\d\d+')
|
||||
match = pattern.match(content[start_pos:])
|
||||
author_email = match.group()[:-15]
|
||||
|
||||
# Merge the branch
|
||||
do_merge = input('Merge ' + branch_url + ' into local branch? (y/N/q): ').lower()
|
||||
if do_merge == 'y':
|
||||
subprocess.call(['bzr', 'merge', branch_url])
|
||||
elif do_merge == 'q':
|
||||
exit()
|
||||
|
||||
# Create commit command
|
||||
commit_command = ['bzr', 'commit']
|
||||
|
||||
for bug in bugs:
|
||||
commit_command.append('--fixes')
|
||||
commit_command.append('lp:' + bug)
|
||||
|
||||
commit_command.append('-m')
|
||||
commit_command.append(commit_message)
|
||||
|
||||
commit_command.append('--author')
|
||||
commit_command.append('"' + author_email + '"')
|
||||
|
||||
print('About to run the bzr command below:\n')
|
||||
print(' '.join(commit_command))
|
||||
do_commit = input('Run the command (y), use qcommit (qcommit) or cancel (C): ').lower()
|
||||
|
||||
if do_commit == 'y':
|
||||
subprocess.call(commit_command)
|
||||
elif do_commit == 'qcommit':
|
||||
# Setup QT workaround to make qbzr look right on my box
|
||||
my_env = os.environ.copy()
|
||||
my_env['QT_GRAPHICSSYSTEM'] = 'native'
|
||||
# Print stuff that kan be copy/pasted into qbzr GUI
|
||||
print('These bugs can be copy/pasted in: lp:' + ' lp:'.join(bugs))
|
||||
print('The authors email is: ' + author_email)
|
||||
# Run qcommit
|
||||
subprocess.call(['bzr', 'qcommit', '-m', commit_message], env=my_env)
|
144
tests/functional/openlp_core/test_init.py
Normal file
144
tests/functional/openlp_core/test_init.py
Normal file
@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
|
||||
|
||||
###############################################################################
|
||||
# OpenLP - Open Source Lyrics Projection #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Copyright (c) 2008-2015 OpenLP Developers #
|
||||
# --------------------------------------------------------------------------- #
|
||||
# This program is free software; you can redistribute it and/or modify it #
|
||||
# under the terms of the GNU General Public License as published by the Free #
|
||||
# Software Foundation; version 2 of the License. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT #
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
|
||||
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
|
||||
# more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License along #
|
||||
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
|
||||
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
|
||||
###############################################################################
|
||||
|
||||
import sys
|
||||
from unittest import TestCase
|
||||
|
||||
from openlp.core import parse_options
|
||||
from tests.helpers.testmixin import TestMixin
|
||||
|
||||
|
||||
class TestInitFunctions(TestMixin, TestCase):
|
||||
|
||||
def parse_options_basic_test(self):
|
||||
"""
|
||||
Test the parse options process works
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = []
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
|
||||
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
|
||||
self.assertFalse(args.portable, 'The portable flag should be set to false')
|
||||
self.assertEquals(args.style, None, 'There are no style flags to be processed')
|
||||
self.assertEquals(args.rargs, [], 'The service file should be blank')
|
||||
|
||||
def parse_options_debug_test(self):
|
||||
"""
|
||||
Test the parse options process works for debug only
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['-l debug']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
|
||||
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
|
||||
self.assertFalse(args.portable, 'The portable flag should be set to false')
|
||||
self.assertEquals(args.style, None, 'There are no style flags to be processed')
|
||||
self.assertEquals(args.rargs, [], 'The service file should be blank')
|
||||
|
||||
def parse_options_debug_and_portable_test(self):
|
||||
"""
|
||||
Test the parse options process works for debug and portable
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['--portable']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
|
||||
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
|
||||
self.assertTrue(args.portable, 'The portable flag should be set to true')
|
||||
self.assertEquals(args.style, None, 'There are no style flags to be processed')
|
||||
self.assertEquals(args.rargs, [], 'The service file should be blank')
|
||||
|
||||
def parse_options_all_no_file_test(self):
|
||||
"""
|
||||
Test the parse options process works with two options
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['-l debug', '-d']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertTrue(args.dev_version, 'The dev_version flag should be True')
|
||||
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
|
||||
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
|
||||
self.assertFalse(args.portable, 'The portable flag should be set to false')
|
||||
self.assertEquals(args.style, None, 'There are no style flags to be processed')
|
||||
self.assertEquals(args.rargs, [], 'The service file should be blank')
|
||||
|
||||
def parse_options_file_test(self):
|
||||
"""
|
||||
Test the parse options process works with a file
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['dummy_temp']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, 'warning', 'The log level should be set to warning')
|
||||
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
|
||||
self.assertFalse(args.portable, 'The portable flag should be set to false')
|
||||
self.assertEquals(args.style, None, 'There are no style flags to be processed')
|
||||
self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
|
||||
|
||||
def parse_options_file_and_debug_test(self):
|
||||
"""
|
||||
Test the parse options process works with a file
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['-l debug', 'dummy_temp']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertFalse(args.dev_version, 'The dev_version flag should be False')
|
||||
self.assertEquals(args.loglevel, ' debug', 'The log level should be set to debug')
|
||||
self.assertFalse(args.no_error_form, 'The no_error_form should be set to False')
|
||||
self.assertFalse(args.portable, 'The portable flag should be set to false')
|
||||
self.assertEquals(args.style, None, 'There are no style flags to be processed')
|
||||
self.assertEquals(args.rargs, 'dummy_temp', 'The service file should not be blank')
|
||||
|
||||
def parse_options_two_files_test(self):
|
||||
"""
|
||||
Test the parse options process works with a file
|
||||
|
||||
"""
|
||||
# GIVEN: a a set of system arguments.
|
||||
sys.argv[1:] = ['dummy_temp', 'dummy_temp2']
|
||||
# WHEN: We we parse them to expand to options
|
||||
args = parse_options()
|
||||
# THEN: the following fields will have been extracted.
|
||||
self.assertEquals(args, None, 'The args should be None')
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user