diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py
index 25f8201b1..6b77a8c6f 100644
--- a/openlp/core/ui/exceptionform.py
+++ b/openlp/core/ui/exceptionform.py
@@ -69,6 +69,14 @@ try:
MAKO_VERSION = mako.__version__
except ImportError:
MAKO_VERSION = u'-'
+try:
+ import icu
+ try:
+ ICU_VERSION = icu.VERSION
+ except AttributeError:
+ ICU_VERSION = u'OK'
+except ImportError:
+ ICU_VERSION = u'-'
try:
import uno
arg = uno.createUnoStruct(u'com.sun.star.beans.PropertyValue')
@@ -143,6 +151,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog):
u'PyEnchant: %s\n' % ENCHANT_VERSION + \
u'PySQLite: %s\n' % SQLITE_VERSION + \
u'Mako: %s\n' % MAKO_VERSION + \
+ u'pyICU: %s\n' % ICU_VERSION + \
u'pyUNO bridge: %s\n' % UNO_VERSION + \
u'VLC: %s\n' % VLC_VERSION
if platform.system() == u'Linux':
diff --git a/openlp/core/ui/media/vendor/vlc.py b/openlp/core/ui/media/vendor/vlc.py
index dbb2971f7..f2cb3cad4 100644
--- a/openlp/core/ui/media/vendor/vlc.py
+++ b/openlp/core/ui/media/vendor/vlc.py
@@ -48,7 +48,7 @@ import sys
from inspect import getargspec
__version__ = "N/A"
-build_date = "Thu Mar 21 22:33:03 2013"
+build_date = "Mon Apr 1 23:47:38 2013"
if sys.version_info[0] > 2:
str = str
@@ -70,7 +70,7 @@ if sys.version_info[0] > 2:
if isinstance(b, bytes):
return b.decode(sys.getfilesystemencoding())
else:
- return str(b)
+ return b
else:
str = str
unicode = unicode
@@ -278,6 +278,11 @@ def class_result(classname):
return classname(result)
return wrap_errcheck
+# Wrapper for the opaque struct libvlc_log_t
+class Log(ctypes.Structure):
+ pass
+Log_ptr = ctypes.POINTER(Log)
+
# FILE* ctypes wrapper, copied from
# http://svn.python.org/projects/ctypes/trunk/ctypeslib/ctypeslib/contrib/pythonhdr.py
class FILE(ctypes.Structure):
@@ -675,11 +680,14 @@ class Callback(ctypes.c_void_p):
pass
class LogCb(ctypes.c_void_p):
"""Callback prototype for LibVLC log message handler.
-\param data data pointer as given to L{libvlc_log_subscribe}()
+\param data data pointer as given to L{libvlc_log_set}()
\param level message level (@ref enum libvlc_log_level)
+\param ctx message context (meta-informations about the message)
\param fmt printf() format string (as defined by ISO C11)
\param args variable argument list for the format
\note Log message handlers must be thread-safe.
+\warning The message context pointer, the format string parameters and the
+ variable arguments are only valid until the callback returns.
"""
pass
class VideoLockCb(ctypes.c_void_p):
@@ -813,13 +821,16 @@ class CallbackDecorators(object):
Callback.__doc__ = '''Callback function notification
\param p_event the event triggering the callback
'''
- LogCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p)
+ LogCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, Log_ptr, ctypes.c_char_p, ctypes.c_void_p)
LogCb.__doc__ = '''Callback prototype for LibVLC log message handler.
-\param data data pointer as given to L{libvlc_log_subscribe}()
+\param data data pointer as given to L{libvlc_log_set}()
\param level message level (@ref enum libvlc_log_level)
+\param ctx message context (meta-informations about the message)
\param fmt printf() format string (as defined by ISO C11)
\param args variable argument list for the format
\note Log message handlers must be thread-safe.
+\warning The message context pointer, the format string parameters and the
+ variable arguments are only valid until the callback returns.
'''
VideoLockCb = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_void_p, ListPOINTER(ctypes.c_void_p))
VideoLockCb.__doc__ = '''Callback prototype to allocate and lock a picture buffer.
@@ -1407,7 +1418,7 @@ class Instance(_Ctype):
@param name: interface name, or NULL for default.
@return: 0 on success, -1 on error.
'''
- return libvlc_add_intf(self, name)
+ return libvlc_add_intf(self, str_to_bytes(name))
def set_user_agent(self, name, http):
'''Sets the application name. LibVLC passes this as the user agent string
@@ -1416,7 +1427,33 @@ class Instance(_Ctype):
@param http: HTTP User Agent, e.g. "FooBar/1.2.3 Python/2.6.0".
@version: LibVLC 1.1.1 or later.
'''
- return libvlc_set_user_agent(self, name, http)
+ return libvlc_set_user_agent(self, str_to_bytes(name), str_to_bytes(http))
+
+ def log_unset(self):
+ '''Unsets the logging callback for a LibVLC instance. This is rarely needed:
+ the callback is implicitly unset when the instance is destroyed.
+ This function will wait for any pending callbacks invocation to complete
+ (causing a deadlock if called from within the callback).
+ @version: LibVLC 2.1.0 or later.
+ '''
+ return libvlc_log_unset(self)
+
+ def log_set(self, data, p_instance):
+ '''Sets the logging callback for a LibVLC instance.
+ This function is thread-safe: it will wait for any pending callbacks
+ invocation to complete.
+ @param data: opaque data pointer for the callback function @note Some log messages (especially debug) are emitted by LibVLC while is being initialized. These messages cannot be captured with this interface. @warning A deadlock may occur if this function is called from the callback.
+ @param p_instance: libvlc instance.
+ @version: LibVLC 2.1.0 or later.
+ '''
+ return libvlc_log_set(self, data, p_instance)
+
+ def log_set_file(self, stream):
+ '''Sets up logging to a file.
+ @param stream: FILE pointer opened for writing (the FILE pointer must remain valid until L{log_unset}()).
+ @version: LibVLC 2.1.0 or later.
+ '''
+ return libvlc_log_set_file(self, stream)
def media_new_location(self, psz_mrl):
'''Create a media with a certain given media resource location,
@@ -1429,7 +1466,7 @@ class Instance(_Ctype):
@param psz_mrl: the media location.
@return: the newly created media or NULL on error.
'''
- return libvlc_media_new_location(self, psz_mrl)
+ return libvlc_media_new_location(self, str_to_bytes(psz_mrl))
def media_new_path(self, path):
'''Create a media for a certain file path.
@@ -1437,7 +1474,7 @@ class Instance(_Ctype):
@param path: local filesystem path.
@return: the newly created media or NULL on error.
'''
- return libvlc_media_new_path(self, path)
+ return libvlc_media_new_path(self, str_to_bytes(path))
def media_new_fd(self, fd):
'''Create a media for an already open file descriptor.
@@ -1465,14 +1502,14 @@ class Instance(_Ctype):
@param psz_name: the name of the node.
@return: the new empty media or NULL on error.
'''
- return libvlc_media_new_as_node(self, psz_name)
+ return libvlc_media_new_as_node(self, str_to_bytes(psz_name))
def media_discoverer_new_from_name(self, psz_name):
'''Discover media service by name.
@param psz_name: service name.
@return: media discover object or NULL in case of error.
'''
- return libvlc_media_discoverer_new_from_name(self, psz_name)
+ return libvlc_media_discoverer_new_from_name(self, str_to_bytes(psz_name))
def media_library_new(self):
'''Create an new Media Library object.
@@ -1500,7 +1537,7 @@ class Instance(_Ctype):
@return: A NULL-terminated linked list of potential audio output devices. It must be freed it with L{audio_output_device_list_release}().
@version: LibVLC 2.1.0 or later.
'''
- return libvlc_audio_output_device_list_get(self, aout)
+ return libvlc_audio_output_device_list_get(self, str_to_bytes(aout))
def vlm_release(self):
'''Release the vlm instance related to the given L{Instance}.
@@ -1518,7 +1555,7 @@ class Instance(_Ctype):
@param b_loop: Should this broadcast be played in loop ?
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_add_broadcast(self, psz_name, psz_input, psz_output, i_options, ppsz_options, b_enabled, b_loop)
+ return libvlc_vlm_add_broadcast(self, str_to_bytes(psz_name), str_to_bytes(psz_input), str_to_bytes(psz_output), i_options, ppsz_options, b_enabled, b_loop)
def vlm_add_vod(self, psz_name, psz_input, i_options, ppsz_options, b_enabled, psz_mux):
'''Add a vod, with one input.
@@ -1530,14 +1567,14 @@ class Instance(_Ctype):
@param psz_mux: the muxer of the vod media.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_add_vod(self, psz_name, psz_input, i_options, ppsz_options, b_enabled, psz_mux)
+ return libvlc_vlm_add_vod(self, str_to_bytes(psz_name), str_to_bytes(psz_input), i_options, ppsz_options, b_enabled, str_to_bytes(psz_mux))
def vlm_del_media(self, psz_name):
'''Delete a media (VOD or broadcast).
@param psz_name: the media to delete.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_del_media(self, psz_name)
+ return libvlc_vlm_del_media(self, str_to_bytes(psz_name))
def vlm_set_enabled(self, psz_name, b_enabled):
'''Enable or disable a media (VOD or broadcast).
@@ -1545,7 +1582,7 @@ class Instance(_Ctype):
@param b_enabled: the new status.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_set_enabled(self, psz_name, b_enabled)
+ return libvlc_vlm_set_enabled(self, str_to_bytes(psz_name), b_enabled)
def vlm_set_output(self, psz_name, psz_output):
'''Set the output for a media.
@@ -1553,7 +1590,7 @@ class Instance(_Ctype):
@param psz_output: the output MRL (the parameter to the "sout" variable).
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_set_output(self, psz_name, psz_output)
+ return libvlc_vlm_set_output(self, str_to_bytes(psz_name), str_to_bytes(psz_output))
def vlm_set_input(self, psz_name, psz_input):
'''Set a media's input MRL. This will delete all existing inputs and
@@ -1562,7 +1599,7 @@ class Instance(_Ctype):
@param psz_input: the input MRL.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_set_input(self, psz_name, psz_input)
+ return libvlc_vlm_set_input(self, str_to_bytes(psz_name), str_to_bytes(psz_input))
def vlm_add_input(self, psz_name, psz_input):
'''Add a media's input MRL. This will add the specified one.
@@ -1570,7 +1607,7 @@ class Instance(_Ctype):
@param psz_input: the input MRL.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_add_input(self, psz_name, psz_input)
+ return libvlc_vlm_add_input(self, str_to_bytes(psz_name), str_to_bytes(psz_input))
def vlm_set_loop(self, psz_name, b_loop):
'''Set a media's loop status.
@@ -1578,7 +1615,7 @@ class Instance(_Ctype):
@param b_loop: the new status.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_set_loop(self, psz_name, b_loop)
+ return libvlc_vlm_set_loop(self, str_to_bytes(psz_name), b_loop)
def vlm_set_mux(self, psz_name, psz_mux):
'''Set a media's vod muxer.
@@ -1586,7 +1623,7 @@ class Instance(_Ctype):
@param psz_mux: the new muxer.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_set_mux(self, psz_name, psz_mux)
+ return libvlc_vlm_set_mux(self, str_to_bytes(psz_name), str_to_bytes(psz_mux))
def vlm_change_media(self, psz_name, psz_input, psz_output, i_options, ppsz_options, b_enabled, b_loop):
'''Edit the parameters of a media. This will delete all existing inputs and
@@ -1600,28 +1637,28 @@ class Instance(_Ctype):
@param b_loop: Should this broadcast be played in loop ?
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_change_media(self, psz_name, psz_input, psz_output, i_options, ppsz_options, b_enabled, b_loop)
+ return libvlc_vlm_change_media(self, str_to_bytes(psz_name), str_to_bytes(psz_input), str_to_bytes(psz_output), i_options, ppsz_options, b_enabled, b_loop)
def vlm_play_media(self, psz_name):
'''Play the named broadcast.
@param psz_name: the name of the broadcast.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_play_media(self, psz_name)
+ return libvlc_vlm_play_media(self, str_to_bytes(psz_name))
def vlm_stop_media(self, psz_name):
'''Stop the named broadcast.
@param psz_name: the name of the broadcast.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_stop_media(self, psz_name)
+ return libvlc_vlm_stop_media(self, str_to_bytes(psz_name))
def vlm_pause_media(self, psz_name):
'''Pause the named broadcast.
@param psz_name: the name of the broadcast.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_pause_media(self, psz_name)
+ return libvlc_vlm_pause_media(self, str_to_bytes(psz_name))
def vlm_seek_media(self, psz_name, f_percentage):
'''Seek in the named broadcast.
@@ -1629,7 +1666,7 @@ class Instance(_Ctype):
@param f_percentage: the percentage to seek to.
@return: 0 on success, -1 on error.
'''
- return libvlc_vlm_seek_media(self, psz_name, f_percentage)
+ return libvlc_vlm_seek_media(self, str_to_bytes(psz_name), f_percentage)
def vlm_show_media(self, psz_name):
'''Return information about the named media as a JSON
@@ -1643,7 +1680,7 @@ class Instance(_Ctype):
@param psz_name: the name of the media, if the name is an empty string, all media is described.
@return: string with information about named media, or NULL on error.
'''
- return libvlc_vlm_show_media(self, psz_name)
+ return libvlc_vlm_show_media(self, str_to_bytes(psz_name))
def vlm_get_media_instance_position(self, psz_name, i_instance):
'''Get vlm_media instance position by name or instance id.
@@ -1651,7 +1688,7 @@ class Instance(_Ctype):
@param i_instance: instance id.
@return: position as float or -1. on error.
'''
- return libvlc_vlm_get_media_instance_position(self, psz_name, i_instance)
+ return libvlc_vlm_get_media_instance_position(self, str_to_bytes(psz_name), i_instance)
def vlm_get_media_instance_time(self, psz_name, i_instance):
'''Get vlm_media instance time by name or instance id.
@@ -1659,7 +1696,7 @@ class Instance(_Ctype):
@param i_instance: instance id.
@return: time as integer or -1 on error.
'''
- return libvlc_vlm_get_media_instance_time(self, psz_name, i_instance)
+ return libvlc_vlm_get_media_instance_time(self, str_to_bytes(psz_name), i_instance)
def vlm_get_media_instance_length(self, psz_name, i_instance):
'''Get vlm_media instance length by name or instance id.
@@ -1667,7 +1704,7 @@ class Instance(_Ctype):
@param i_instance: instance id.
@return: length of media item or -1 on error.
'''
- return libvlc_vlm_get_media_instance_length(self, psz_name, i_instance)
+ return libvlc_vlm_get_media_instance_length(self, str_to_bytes(psz_name), i_instance)
def vlm_get_media_instance_rate(self, psz_name, i_instance):
'''Get vlm_media instance playback rate by name or instance id.
@@ -1675,7 +1712,7 @@ class Instance(_Ctype):
@param i_instance: instance id.
@return: playback rate or -1 on error.
'''
- return libvlc_vlm_get_media_instance_rate(self, psz_name, i_instance)
+ return libvlc_vlm_get_media_instance_rate(self, str_to_bytes(psz_name), i_instance)
def vlm_get_media_instance_title(self, psz_name, i_instance):
'''Get vlm_media instance title number by name or instance id.
@@ -1684,7 +1721,7 @@ class Instance(_Ctype):
@return: title as number or -1 on error.
@bug: will always return 0.
'''
- return libvlc_vlm_get_media_instance_title(self, psz_name, i_instance)
+ return libvlc_vlm_get_media_instance_title(self, str_to_bytes(psz_name), i_instance)
def vlm_get_media_instance_chapter(self, psz_name, i_instance):
'''Get vlm_media instance chapter number by name or instance id.
@@ -1693,7 +1730,7 @@ class Instance(_Ctype):
@return: chapter as number or -1 on error.
@bug: will always return 0.
'''
- return libvlc_vlm_get_media_instance_chapter(self, psz_name, i_instance)
+ return libvlc_vlm_get_media_instance_chapter(self, str_to_bytes(psz_name), i_instance)
def vlm_get_media_instance_seekable(self, psz_name, i_instance):
'''Is libvlc instance seekable ?
@@ -1702,7 +1739,7 @@ class Instance(_Ctype):
@return: 1 if seekable, 0 if not, -1 if media does not exist.
@bug: will always return 0.
'''
- return libvlc_vlm_get_media_instance_seekable(self, psz_name, i_instance)
+ return libvlc_vlm_get_media_instance_seekable(self, str_to_bytes(psz_name), i_instance)
def vlm_get_event_manager(self):
'''Get libvlc_event_manager from a vlm media.
@@ -1751,7 +1788,7 @@ class Media(_Ctype):
self.add_option(o)
- def add_option(self, ppsz_options):
+ def add_option(self, psz_options):
'''Add an option to the media.
This option will be used to determine how the media_player will
read the media. This allows to use VLC's advanced
@@ -1763,11 +1800,11 @@ class Media(_Ctype):
Specifically, due to architectural issues most audio and video options,
such as text renderer options, have no effects on an individual media.
These options must be set through L{new}() instead.
- @param ppsz_options: the options (as a string).
+ @param psz_options: the options (as a string).
'''
- return libvlc_media_add_option(self, ppsz_options)
+ return libvlc_media_add_option(self, str_to_bytes(psz_options))
- def add_option_flag(self, ppsz_options, i_flags):
+ def add_option_flag(self, psz_options, i_flags):
'''Add an option to the media with configurable flags.
This option will be used to determine how the media_player will
read the media. This allows to use VLC's advanced
@@ -1777,10 +1814,10 @@ class Media(_Ctype):
specifically, due to architectural issues, video-related options
such as text renderer options cannot be set on a single media. They
must be set on the whole libvlc instance instead.
- @param ppsz_options: the options (as a string).
+ @param psz_options: the options (as a string).
@param i_flags: the flags for this option.
'''
- return libvlc_media_add_option_flag(self, ppsz_options, i_flags)
+ return libvlc_media_add_option_flag(self, str_to_bytes(psz_options), i_flags)
def retain(self):
'''Retain a reference to a media descriptor object (libvlc_media_t). Use
@@ -1829,7 +1866,7 @@ class Media(_Ctype):
@param e_meta: the meta to write.
@param psz_value: the media's meta.
'''
- return libvlc_media_set_meta(self, e_meta, psz_value)
+ return libvlc_media_set_meta(self, e_meta, str_to_bytes(psz_value))
def save_meta(self):
'''Save the meta previously set.
@@ -2490,7 +2527,7 @@ class MediaPlayer(_Ctype):
@version: LibVLC 1.1.1 or later.
@bug: All pixel planes are expected to have the same pitch. To use the YCbCr color space with chrominance subsampling, consider using L{video_set_format_callbacks}() instead.
'''
- return libvlc_video_set_format(self, chroma, width, height, pitch)
+ return libvlc_video_set_format(self, str_to_bytes(chroma), width, height, pitch)
def video_set_format_callbacks(self, setup, cleanup):
'''Set decoded video chroma and dimensions. This only works in combination with
@@ -2617,7 +2654,7 @@ class MediaPlayer(_Ctype):
@param channels: channels count.
@version: LibVLC 2.0.0 or later.
'''
- return libvlc_audio_set_format(self, format, rate, channels)
+ return libvlc_audio_set_format(self, str_to_bytes(format), rate, channels)
def get_length(self):
'''Get the current movie length (in ms).
@@ -2843,7 +2880,7 @@ class MediaPlayer(_Ctype):
'''Set new video aspect ratio.
@param psz_aspect: new video aspect-ratio or NULL to reset to default @note Invalid aspect ratios are ignored.
'''
- return libvlc_video_set_aspect_ratio(self, psz_aspect)
+ return libvlc_video_set_aspect_ratio(self, str_to_bytes(psz_aspect))
def video_get_spu(self):
'''Get current video subtitle.
@@ -2859,7 +2896,7 @@ class MediaPlayer(_Ctype):
def video_set_spu(self, i_spu):
'''Set new video subtitle.
- @param i_spu: new video subtitle to select.
+ @param i_spu: video subtitle track to select (i_id from track description).
@return: 0 on success, -1 if out of range.
'''
return libvlc_video_set_spu(self, i_spu)
@@ -2869,7 +2906,7 @@ class MediaPlayer(_Ctype):
@param psz_subtitle: new video subtitle file.
@return: the success status (boolean).
'''
- return libvlc_video_set_subtitle_file(self, psz_subtitle)
+ return libvlc_video_set_subtitle_file(self, str_to_bytes(psz_subtitle))
def video_get_spu_delay(self):
'''Get the current subtitle delay. Positive values means subtitles are being
@@ -2900,7 +2937,7 @@ class MediaPlayer(_Ctype):
'''Set new crop filter geometry.
@param psz_geometry: new crop filter geometry (NULL to unset).
'''
- return libvlc_video_set_crop_geometry(self, psz_geometry)
+ return libvlc_video_set_crop_geometry(self, str_to_bytes(psz_geometry))
def video_get_teletext(self):
'''Get current teletext page requested.
@@ -2948,13 +2985,13 @@ class MediaPlayer(_Ctype):
@param i_height: the snapshot's height.
@return: 0 on success, -1 if the video was not found.
'''
- return libvlc_video_take_snapshot(self, num, psz_filepath, i_width, i_height)
+ return libvlc_video_take_snapshot(self, num, str_to_bytes(psz_filepath), i_width, i_height)
def video_set_deinterlace(self, psz_mode):
'''Enable or disable deinterlace filter.
@param psz_mode: type of deinterlace filter, NULL to disable.
'''
- return libvlc_video_set_deinterlace(self, psz_mode)
+ return libvlc_video_set_deinterlace(self, str_to_bytes(psz_mode))
def video_get_marquee_int(self, option):
'''Get an integer marquee option value.
@@ -2982,7 +3019,7 @@ class MediaPlayer(_Ctype):
@param option: marq option to set See libvlc_video_marquee_string_option_t.
@param psz_text: marq option value.
'''
- return libvlc_video_set_marquee_string(self, option, psz_text)
+ return libvlc_video_set_marquee_string(self, option, str_to_bytes(psz_text))
def video_get_logo_int(self, option):
'''Get integer logo option.
@@ -3006,7 +3043,7 @@ class MediaPlayer(_Ctype):
@param option: logo option to set, values of libvlc_video_logo_option_t.
@param psz_value: logo option value.
'''
- return libvlc_video_set_logo_string(self, option, psz_value)
+ return libvlc_video_set_logo_string(self, option, str_to_bytes(psz_value))
def video_get_adjust_int(self, option):
'''Get integer adjust option.
@@ -3049,7 +3086,7 @@ class MediaPlayer(_Ctype):
@param psz_name: name of audio output, use psz_name of See L{AudioOutput}.
@return: 0 if function succeded, -1 on error.
'''
- return libvlc_audio_output_set(self, psz_name)
+ return libvlc_audio_output_set(self, str_to_bytes(psz_name))
def audio_output_device_set(self, psz_audio_output, psz_device_id):
'''Configures an explicit audio output device for a given audio output plugin.
@@ -3065,7 +3102,7 @@ class MediaPlayer(_Ctype):
@param psz_device_id: device.
@return: Nothing. Errors are ignored.
'''
- return libvlc_audio_output_device_set(self, psz_audio_output, psz_device_id)
+ return libvlc_audio_output_device_set(self, str_to_bytes(psz_audio_output), str_to_bytes(psz_device_id))
def audio_toggle_mute(self):
'''Toggle mute status.
@@ -3314,44 +3351,43 @@ def libvlc_event_type_name(event_type):
ctypes.c_char_p, ctypes.c_uint)
return f(event_type)
-def libvlc_log_subscribe(sub, cb, data):
- '''Registers a logging callback to LibVLC.
- This function is thread-safe.
- @param sub: uninitialized subscriber structure.
+def libvlc_log_unset(p_instance):
+ '''Unsets the logging callback for a LibVLC instance. This is rarely needed:
+ the callback is implicitly unset when the instance is destroyed.
+ This function will wait for any pending callbacks invocation to complete
+ (causing a deadlock if called from within the callback).
+ @param p_instance: libvlc instance.
+ @version: LibVLC 2.1.0 or later.
+ '''
+ f = _Cfunctions.get('libvlc_log_unset', None) or \
+ _Cfunction('libvlc_log_unset', ((1,),), None,
+ None, Instance)
+ return f(p_instance)
+
+def libvlc_log_set(cb, data, p_instance):
+ '''Sets the logging callback for a LibVLC instance.
+ This function is thread-safe: it will wait for any pending callbacks
+ invocation to complete.
@param cb: callback function pointer.
- @param data: opaque data pointer for the callback function @note Some log messages (especially debug) are emitted by LibVLC while initializing, before any LibVLC instance even exists. Thus this function does not require a LibVLC instance parameter. @warning As a consequence of not depending on a LibVLC instance, all logging callbacks are shared by all LibVLC instances within the process / address space. This also enables log messages to be emitted by LibVLC components that are not specific to any given LibVLC instance. @warning Do not call this function from within a logging callback. It would trigger a dead lock.
+ @param data: opaque data pointer for the callback function @note Some log messages (especially debug) are emitted by LibVLC while is being initialized. These messages cannot be captured with this interface. @warning A deadlock may occur if this function is called from the callback.
+ @param p_instance: libvlc instance.
@version: LibVLC 2.1.0 or later.
'''
- f = _Cfunctions.get('libvlc_log_subscribe', None) or \
- _Cfunction('libvlc_log_subscribe', ((1,), (1,), (1,),), None,
- None, ctypes.c_void_p, LogCb, ctypes.c_void_p)
- return f(sub, cb, data)
+ f = _Cfunctions.get('libvlc_log_set', None) or \
+ _Cfunction('libvlc_log_set', ((1,), (1,), (1,),), None,
+ None, Instance, LogCb, ctypes.c_void_p)
+ return f(cb, data, p_instance)
-def libvlc_log_subscribe_file(sub, stream):
- '''Registers a logging callback to a file.
- @param stream: FILE pointer opened for writing (the FILE pointer must remain valid until L{libvlc_log_unsubscribe}()).
+def libvlc_log_set_file(p_instance, stream):
+ '''Sets up logging to a file.
+ @param p_instance: libvlc instance.
+ @param stream: FILE pointer opened for writing (the FILE pointer must remain valid until L{libvlc_log_unset}()).
@version: LibVLC 2.1.0 or later.
'''
- f = _Cfunctions.get('libvlc_log_subscribe_file', None) or \
- _Cfunction('libvlc_log_subscribe_file', ((1,), (1,),), None,
- None, ctypes.c_void_p, FILE_ptr)
- return f(sub, stream)
-
-def libvlc_log_unsubscribe(sub):
- '''Deregisters a logging callback from LibVLC.
- This function is thread-safe.
- @note: After (and only after) L{libvlc_log_unsubscribe}() has returned,
- LibVLC warrants that there are no more pending calls of the subscription
- callback function.
- @warning: Do not call this function from within a logging callback.
- It would trigger a dead lock.
- @param sub: initialized subscriber structure.
- @version: LibVLC 2.1.0 or later.
- '''
- f = _Cfunctions.get('libvlc_log_unsubscribe', None) or \
- _Cfunction('libvlc_log_unsubscribe', ((1,),), None,
- None, ctypes.c_void_p)
- return f(sub)
+ f = _Cfunctions.get('libvlc_log_set_file', None) or \
+ _Cfunction('libvlc_log_set_file', ((1,), (1,),), None,
+ None, Instance, FILE_ptr)
+ return f(p_instance, stream)
def libvlc_module_description_list_release(p_list):
'''Release a list of module descriptions.
@@ -3460,7 +3496,7 @@ def libvlc_media_new_as_node(p_instance, psz_name):
ctypes.c_void_p, Instance, ctypes.c_char_p)
return f(p_instance, psz_name)
-def libvlc_media_add_option(p_md, ppsz_options):
+def libvlc_media_add_option(p_md, psz_options):
'''Add an option to the media.
This option will be used to determine how the media_player will
read the media. This allows to use VLC's advanced
@@ -3473,14 +3509,14 @@ def libvlc_media_add_option(p_md, ppsz_options):
such as text renderer options, have no effects on an individual media.
These options must be set through L{libvlc_new}() instead.
@param p_md: the media descriptor.
- @param ppsz_options: the options (as a string).
+ @param psz_options: the options (as a string).
'''
f = _Cfunctions.get('libvlc_media_add_option', None) or \
_Cfunction('libvlc_media_add_option', ((1,), (1,),), None,
None, Media, ctypes.c_char_p)
- return f(p_md, ppsz_options)
+ return f(p_md, psz_options)
-def libvlc_media_add_option_flag(p_md, ppsz_options, i_flags):
+def libvlc_media_add_option_flag(p_md, psz_options, i_flags):
'''Add an option to the media with configurable flags.
This option will be used to determine how the media_player will
read the media. This allows to use VLC's advanced
@@ -3491,13 +3527,13 @@ def libvlc_media_add_option_flag(p_md, ppsz_options, i_flags):
such as text renderer options cannot be set on a single media. They
must be set on the whole libvlc instance instead.
@param p_md: the media descriptor.
- @param ppsz_options: the options (as a string).
+ @param psz_options: the options (as a string).
@param i_flags: the flags for this option.
'''
f = _Cfunctions.get('libvlc_media_add_option_flag', None) or \
_Cfunction('libvlc_media_add_option_flag', ((1,), (1,), (1,),), None,
None, Media, ctypes.c_char_p, ctypes.c_uint)
- return f(p_md, ppsz_options, i_flags)
+ return f(p_md, psz_options, i_flags)
def libvlc_media_retain(p_md):
'''Retain a reference to a media descriptor object (libvlc_media_t). Use
@@ -4949,12 +4985,12 @@ def libvlc_video_get_spu_description(p_mi):
def libvlc_video_set_spu(p_mi, i_spu):
'''Set new video subtitle.
@param p_mi: the media player.
- @param i_spu: new video subtitle to select.
+ @param i_spu: video subtitle track to select (i_id from track description).
@return: 0 on success, -1 if out of range.
'''
f = _Cfunctions.get('libvlc_video_set_spu', None) or \
_Cfunction('libvlc_video_set_spu', ((1,), (1,),), None,
- ctypes.c_int, MediaPlayer, ctypes.c_uint)
+ ctypes.c_int, MediaPlayer, ctypes.c_int)
return f(p_mi, i_spu)
def libvlc_video_set_subtitle_file(p_mi, psz_subtitle):
@@ -5791,7 +5827,7 @@ def libvlc_vlm_get_event_manager(p_instance):
# libvlc_printerr
# libvlc_set_exit_handler
-# 18 function(s) not wrapped as methods:
+# 15 function(s) not wrapped as methods:
# libvlc_audio_output_device_list_release
# libvlc_audio_output_list_release
# libvlc_clearerr
@@ -5802,9 +5838,6 @@ def libvlc_vlm_get_event_manager(p_instance):
# libvlc_get_changeset
# libvlc_get_compiler
# libvlc_get_version
-# libvlc_log_subscribe
-# libvlc_log_subscribe_file
-# libvlc_log_unsubscribe
# libvlc_media_tracks_release
# libvlc_module_description_list_release
# libvlc_new
diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py
index 6490d2b8d..6e0f4ad95 100644
--- a/openlp/core/ui/servicemanager.py
+++ b/openlp/core/ui/servicemanager.py
@@ -234,18 +234,22 @@ class ServiceManagerDialog(object):
icon=u':/general/general_edit.png', triggers=self.create_custom)
self.menu.addSeparator()
# Add AutoPlay menu actions
- self.auto_play_slides_group = QtGui.QMenu(translate('OpenLP.ServiceManager', '&Auto play slides'))
- self.menu.addMenu(self.auto_play_slides_group)
- self.auto_play_slides_loop = create_widget_action(self.auto_play_slides_group,
+ self.auto_play_slides_menu = QtGui.QMenu(translate('OpenLP.ServiceManager', '&Auto play slides'))
+ self.menu.addMenu(self.auto_play_slides_menu)
+ auto_play_slides_group = QtGui.QActionGroup(self.auto_play_slides_menu)
+ auto_play_slides_group.setExclusive(True)
+ self.auto_play_slides_loop = create_widget_action(self.auto_play_slides_menu,
text=translate('OpenLP.ServiceManager', 'Auto play slides &Loop'),
checked=False, triggers=self.toggle_auto_play_slides_loop)
- self.auto_play_slides_once = create_widget_action(self.auto_play_slides_group,
+ auto_play_slides_group.addAction(self.auto_play_slides_loop)
+ self.auto_play_slides_once = create_widget_action(self.auto_play_slides_menu,
text=translate('OpenLP.ServiceManager', 'Auto play slides &Once'),
checked=False, triggers=self.toggle_auto_play_slides_once)
- self.auto_play_slides_group.addSeparator()
- self.timed_slide_interval = create_widget_action(self.auto_play_slides_group,
+ auto_play_slides_group.addAction(self.auto_play_slides_once)
+ self.auto_play_slides_menu.addSeparator()
+ self.timed_slide_interval = create_widget_action(self.auto_play_slides_menu,
text=translate('OpenLP.ServiceManager', '&Delay between slides'),
- checked=False, triggers=self.on_timed_slide_interval)
+ triggers=self.on_timed_slide_interval)
self.menu.addSeparator()
self.preview_action = create_widget_action(self.menu, text=translate('OpenLP.ServiceManager', 'Show &Preview'),
icon=u':/general/general_preview.png', triggers=self.make_preview)
@@ -786,7 +790,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
self.notes_action.setVisible(True)
if service_item[u'service_item'].is_capable(ItemCapabilities.CanLoop) and \
len(service_item[u'service_item'].get_frames()) > 1:
- self.auto_play_slides_group.menuAction().setVisible(True)
+ self.auto_play_slides_menu.menuAction().setVisible(True)
self.auto_play_slides_once.setChecked(service_item[u'service_item'].auto_play_slides_once)
self.auto_play_slides_loop.setChecked(service_item[u'service_item'].auto_play_slides_loop)
self.timed_slide_interval.setChecked(service_item[u'service_item'].timed_slide_interval > 0)
@@ -798,7 +802,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
translate('OpenLP.ServiceManager', '&Delay between slides') + delay_suffix)
# TODO for future: make group explains itself more visually
else:
- self.auto_play_slides_group.menuAction().setVisible(False)
+ self.auto_play_slides_menu.menuAction().setVisible(False)
if service_item[u'service_item'].is_capable(ItemCapabilities.HasVariableStartTime):
self.time_action.setVisible(True)
if service_item[u'service_item'].is_capable(ItemCapabilities.CanAutoStartForLive):
diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py
index f0c5aa170..eaeebfba8 100644
--- a/openlp/core/ui/slidecontroller.py
+++ b/openlp/core/ui/slidecontroller.py
@@ -44,6 +44,8 @@ from openlp.core.utils.actions import ActionList, CategoryOrder
log = logging.getLogger(__name__)
+# Threshold which has to be trespassed to toggle.
+HIDE_MENU_THRESHOLD = 27
AUDIO_TIME_LABEL_STYLESHEET = u'background-color: palette(background); ' \
u'border-top-color: palette(shadow); ' \
u'border-left-color: palette(shadow); ' \
@@ -588,12 +590,12 @@ class SlideController(DisplayController):
if self.is_live:
# Space used by the toolbar.
used_space = self.toolbar.size().width() + self.hide_menu.size().width()
- # The + 40 is needed to prevent flickering. This can be considered a "buffer".
- if width > used_space + 40 and self.hide_menu.isVisible():
+ # Add the threshold to prevent flickering.
+ if width > used_space + HIDE_MENU_THRESHOLD and self.hide_menu.isVisible():
self.toolbar.set_widget_visible(self.narrow_menu, False)
self.toolbar.set_widget_visible(self.wide_menu)
- # The - 40 is needed to prevent flickering. This can be considered a "buffer".
- elif width < used_space - 40 and not self.hide_menu.isVisible():
+ # Take away a threshold to prevent flickering.
+ elif width < used_space - HIDE_MENU_THRESHOLD and not self.hide_menu.isVisible():
self.toolbar.set_widget_visible(self.wide_menu, False)
self.toolbar.set_widget_visible(self.narrow_menu)
diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py
index 89bbe86f8..be0e3bfa1 100644
--- a/openlp/core/ui/thememanager.py
+++ b/openlp/core/ui/thememanager.py
@@ -44,7 +44,7 @@ from openlp.core.lib.theme import ThemeXML, BackgroundType, VerticalType, Backgr
from openlp.core.lib.ui import critical_error_message_box, create_widget_action
from openlp.core.theme import Theme
from openlp.core.ui import FileRenameForm, ThemeForm
-from openlp.core.utils import AppLocation, delete_file, locale_compare, get_filesystem_encoding
+from openlp.core.utils import AppLocation, delete_file, get_locale_key, get_filesystem_encoding
log = logging.getLogger(__name__)
@@ -418,7 +418,7 @@ class ThemeManager(QtGui.QWidget):
self.theme_list_widget.clear()
files = AppLocation.get_files(self.settings_section, u'.png')
# Sort the themes by its name considering language specific
- files.sort(key=lambda file_name: unicode(file_name), cmp=locale_compare)
+ files.sort(key=lambda file_name: get_locale_key(unicode(file_name)))
# now process the file list of png files
for name in files:
# check to see file is in theme root directory
diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py
index 9cd8f8c81..9a03c2b0e 100644
--- a/openlp/core/utils/__init__.py
+++ b/openlp/core/utils/__init__.py
@@ -38,6 +38,7 @@ import re
from subprocess import Popen, PIPE
import sys
import urllib2
+import icu
from PyQt4 import QtGui, QtCore
@@ -56,10 +57,12 @@ from openlp.core.lib import translate
log = logging.getLogger(__name__)
APPLICATION_VERSION = {}
IMAGES_FILTER = None
+ICU_COLLATOR = None
UNO_CONNECTION_TYPE = u'pipe'
#UNO_CONNECTION_TYPE = u'socket'
CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
+DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
class VersionThread(QtCore.QThread):
@@ -378,21 +381,32 @@ def format_time(text, local_time):
return re.sub('\%[a-zA-Z]', match_formatting, text)
-def locale_compare(string1, string2):
+def get_locale_key(string):
"""
- Compares two strings according to the current locale settings.
-
- As any other compare function, returns a negative, or a positive value,
- or 0, depending on whether string1 collates before or after string2 or
- is equal to it. Comparison is case insensitive.
+ Creates a key for case insensitive, locale aware string sorting.
"""
- # Function locale.strcoll() from standard Python library does not work properly on Windows.
- return locale.strcoll(string1.lower(), string2.lower())
+ string = string.lower()
+ # For Python 3 on platforms other than Windows ICU is not necessary. In those cases locale.strxfrm(str) can be used.
+ global ICU_COLLATOR
+ if ICU_COLLATOR is None:
+ from languagemanager import LanguageManager
+ locale = LanguageManager.get_language()
+ icu_locale = icu.Locale(locale)
+ ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
+ return ICU_COLLATOR.getSortKey(string)
-# For performance reasons provide direct reference to compare function without wrapping it in another function making
-# the string lowercase. This is needed for sorting songs.
-locale_direct_compare = locale.strcoll
+def get_natural_key(string):
+ """
+ Generate a key for locale aware natural string sorting.
+ Returns a list of string compare keys and integers.
+ """
+ key = DIGITS_OR_NONDIGITS.findall(string)
+ key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
+ # Python 3 does not support comparision of different types anymore. So make sure, that we do not compare str and int.
+ #if string[0].isdigit():
+ # return [''] + key
+ return key
from applocation import AppLocation
@@ -402,4 +416,4 @@ from actions import ActionList
__all__ = [u'AppLocation', u'ActionList', u'LanguageManager', u'get_application_version', u'check_latest_version',
u'add_actions', u'get_filesystem_encoding', u'get_web_page', u'get_uno_command', u'get_uno_instance',
- u'delete_file', u'clean_filename', u'format_time', u'locale_compare', u'locale_direct_compare']
+ u'delete_file', u'clean_filename', u'format_time', u'get_locale_key', u'get_natural_key']
diff --git a/openlp/plugins/bibles/forms/bibleimportform.py b/openlp/plugins/bibles/forms/bibleimportform.py
index e360cd4a1..f8d771e77 100644
--- a/openlp/plugins/bibles/forms/bibleimportform.py
+++ b/openlp/plugins/bibles/forms/bibleimportform.py
@@ -38,7 +38,7 @@ from openlp.core.lib import Settings, UiStrings, translate
from openlp.core.lib.db import delete_database
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
-from openlp.core.utils import AppLocation, locale_compare
+from openlp.core.utils import AppLocation, get_locale_key
from openlp.plugins.bibles.lib.manager import BibleFormat
from openlp.plugins.bibles.lib.db import BiblesResourcesDB, clean_filename
@@ -455,7 +455,7 @@ class BibleImportForm(OpenLPWizard):
"""
self.webTranslationComboBox.clear()
bibles = self.web_bible_list[index].keys()
- bibles.sort(cmp=locale_compare)
+ bibles.sort(key=get_locale_key)
self.webTranslationComboBox.addItems(bibles)
def onOsisBrowseButtonClicked(self):
diff --git a/openlp/plugins/bibles/lib/mediaitem.py b/openlp/plugins/bibles/lib/mediaitem.py
index abe3cc45a..86a507612 100644
--- a/openlp/plugins/bibles/lib/mediaitem.py
+++ b/openlp/plugins/bibles/lib/mediaitem.py
@@ -36,7 +36,7 @@ from openlp.core.lib import Registry, MediaManagerItem, ItemCapabilities, Servic
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 locale_compare
+from openlp.core.utils import get_locale_key
from openlp.plugins.bibles.forms import BibleImportForm, EditBibleForm
from openlp.plugins.bibles.lib import LayoutStyle, DisplayStyle, VerseReferenceList, get_reference_separator, \
LanguageSelection, BibleStrings
@@ -325,7 +325,7 @@ class BibleMediaItem(MediaManagerItem):
# Get all bibles and sort the list.
bibles = self.plugin.manager.get_bibles().keys()
bibles = filter(None, bibles)
- bibles.sort(cmp=locale_compare)
+ bibles.sort(key=get_locale_key)
# Load the bibles into the combo boxes.
self.quickVersionComboBox.addItems(bibles)
self.quickSecondComboBox.addItems(bibles)
@@ -461,7 +461,7 @@ class BibleMediaItem(MediaManagerItem):
for book in book_data:
data = BiblesResourcesDB.get_book_by_id(book.book_reference_id)
books.append(data[u'name'] + u' ')
- books.sort(cmp=locale_compare)
+ books.sort(key=get_locale_key)
set_case_insensitive_completer(books, self.quickSearchEdit)
def on_import_click(self):
diff --git a/openlp/plugins/custom/lib/db.py b/openlp/plugins/custom/lib/db.py
index cc6e45742..253ca5432 100644
--- a/openlp/plugins/custom/lib/db.py
+++ b/openlp/plugins/custom/lib/db.py
@@ -35,7 +35,7 @@ from sqlalchemy import Column, Table, types
from sqlalchemy.orm import mapper
from openlp.core.lib.db import BaseModel, init_db
-from openlp.core.utils import locale_compare
+from openlp.core.utils import get_locale_key
class CustomSlide(BaseModel):
"""
@@ -44,11 +44,10 @@ class CustomSlide(BaseModel):
# By default sort the customs by its title considering language specific
# characters.
def __lt__(self, other):
- r = locale_compare(self.title, other.title)
- return True if r < 0 else False
+ return get_locale_key(self.title) < get_locale_key(other.title)
def __eq__(self, other):
- return 0 == locale_compare(self.title, other.title)
+ return get_locale_key(self.title) == get_locale_key(other.title)
def init_schema(url):
diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py
index d74b1ccab..fc575ec0a 100644
--- a/openlp/plugins/images/lib/mediaitem.py
+++ b/openlp/plugins/images/lib/mediaitem.py
@@ -36,7 +36,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, Registry, Servic
StringContent, TreeWidgetWithDnD, UiStrings, build_icon, check_directory_exists, check_item_selected, \
create_thumb, translate, validate_thumb
from openlp.core.lib.ui import create_widget_action, critical_error_message_box
-from openlp.core.utils import AppLocation, delete_file, locale_compare, get_images_filter
+from openlp.core.utils import AppLocation, delete_file, get_locale_key, get_images_filter
from openlp.plugins.images.forms import AddGroupForm, ChooseGroupForm
from openlp.plugins.images.lib.db import ImageFilenames, ImageGroups
@@ -255,7 +255,7 @@ class ImageMediaItem(MediaManagerItem):
The ID of the group that will be added recursively
"""
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
- image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
+ image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name))
folder_icon = build_icon(u':/images/image_group.png')
for image_group in image_groups:
group = QtGui.QTreeWidgetItem()
@@ -286,7 +286,7 @@ class ImageMediaItem(MediaManagerItem):
combobox.clear()
combobox.top_level_group_added = False
image_groups = self.manager.get_all_objects(ImageGroups, ImageGroups.parent_id == parent_group_id)
- image_groups.sort(cmp=locale_compare, key=lambda group_object: group_object.group_name)
+ image_groups.sort(key=lambda group_object: get_locale_key(group_object.group_name))
for image_group in image_groups:
combobox.addItem(prefix + image_group.group_name, image_group.id)
self.fill_groups_combobox(combobox, image_group.id, prefix + ' ')
@@ -338,7 +338,7 @@ class ImageMediaItem(MediaManagerItem):
self.expand_group(open_group.id)
# Sort the images by its filename considering language specific
# characters.
- images.sort(cmp=locale_compare, key=lambda image_object: os.path.split(unicode(image_object.filename))[1])
+ images.sort(key=lambda image_object: get_locale_key(os.path.split(unicode(image_object.filename))[1]))
for imageFile in images:
log.debug(u'Loading image: %s', imageFile.filename)
filename = os.path.split(imageFile.filename)[1]
@@ -525,9 +525,9 @@ class ImageMediaItem(MediaManagerItem):
group_items.append(item)
if isinstance(item.data(0, QtCore.Qt.UserRole), ImageFilenames):
image_items.append(item)
- group_items.sort(cmp=locale_compare, key=lambda item: item.text(0))
+ group_items.sort(key=lambda item: get_locale_key(item.text(0)))
target_group.addChildren(group_items)
- image_items.sort(cmp=locale_compare, key=lambda item: item.text(0))
+ image_items.sort(key=lambda item: get_locale_key(item.text(0)))
target_group.addChildren(image_items)
def generate_slide_data(self, service_item, item=None, xmlVersion=False,
diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py
index 57bc6947b..7d492bc69 100644
--- a/openlp/plugins/media/lib/mediaitem.py
+++ b/openlp/plugins/media/lib/mediaitem.py
@@ -37,7 +37,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem,MediaType, Regist
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
from openlp.core.ui import DisplayController, Display, DisplayControllerType
from openlp.core.ui.media import get_media_players, set_media_players
-from openlp.core.utils import AppLocation, locale_compare
+from openlp.core.utils import AppLocation, get_locale_key
log = logging.getLogger(__name__)
@@ -261,7 +261,7 @@ class MediaMediaItem(MediaManagerItem):
def load_list(self, media, target_group=None):
# Sort the media by its filename considering language specific
# characters.
- media.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])
+ media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
for track in media:
track_info = QtCore.QFileInfo(track)
if not os.path.exists(track):
@@ -287,7 +287,7 @@ class MediaMediaItem(MediaManagerItem):
def getList(self, type=MediaType.Audio):
media = Settings().value(self.settings_section + u'/media files')
- media.sort(cmp=locale_compare, key=lambda filename: os.path.split(unicode(filename))[1])
+ media.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
ext = []
if type == MediaType.Audio:
ext = self.media_controller.audio_extensions_list
diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py
index f92562541..52dcd891f 100644
--- a/openlp/plugins/presentations/lib/mediaitem.py
+++ b/openlp/plugins/presentations/lib/mediaitem.py
@@ -35,7 +35,7 @@ from PyQt4 import QtCore, QtGui
from openlp.core.lib import MediaManagerItem, Registry, ItemCapabilities, ServiceItemContext, Settings, UiStrings, \
build_icon, check_item_selected, create_thumb, translate, validate_thumb
from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box
-from openlp.core.utils import locale_compare
+from openlp.core.utils import get_locale_key
from openlp.plugins.presentations.lib import MessageListener
log = logging.getLogger(__name__)
@@ -153,8 +153,7 @@ class PresentationMediaItem(MediaManagerItem):
if not initialLoad:
self.main_window.display_progress_bar(len(files))
# Sort the presentations by its filename considering language specific characters.
- files.sort(cmp=locale_compare,
- key=lambda filename: os.path.split(unicode(filename))[1])
+ files.sort(key=lambda filename: get_locale_key(os.path.split(unicode(filename))[1]))
for file in files:
if not initialLoad:
self.main_window.increment_progress_bar()
diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py
index 79f21a454..f0554f588 100644
--- a/openlp/plugins/songs/forms/songexportform.py
+++ b/openlp/plugins/songs/forms/songexportform.py
@@ -37,7 +37,6 @@ from PyQt4 import QtCore, QtGui
from openlp.core.lib import Registry, UiStrings, create_separated_list, build_icon, translate
from openlp.core.lib.ui import critical_error_message_box
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
-from openlp.plugins.songs.lib import natcmp
from openlp.plugins.songs.lib.db import Song
from openlp.plugins.songs.lib.openlyricsexport import OpenLyricsExport
@@ -222,7 +221,7 @@ class SongExportForm(OpenLPWizard):
# Load the list of songs.
self.application.set_busy_cursor()
songs = self.plugin.manager.get_all_objects(Song)
- songs.sort(cmp=natcmp, key=lambda song: song.sort_key)
+ songs.sort(key=lambda song: song.sort_key)
for song in songs:
# No need to export temporary songs.
if song.temporary:
diff --git a/openlp/plugins/songs/lib/__init__.py b/openlp/plugins/songs/lib/__init__.py
index 5c1485b9e..d3005c9b2 100644
--- a/openlp/plugins/songs/lib/__init__.py
+++ b/openlp/plugins/songs/lib/__init__.py
@@ -34,7 +34,7 @@ import re
from PyQt4 import QtGui
from openlp.core.lib import translate
-from openlp.core.utils import CONTROL_CHARS, locale_direct_compare
+from openlp.core.utils import CONTROL_CHARS
from db import Author
from ui import SongStrings
@@ -168,6 +168,7 @@ class VerseType(object):
translate('SongsPlugin.VerseType', 'Intro'),
translate('SongsPlugin.VerseType', 'Ending'),
translate('SongsPlugin.VerseType', 'Other')]
+
translated_tags = [name[0].lower() for name in translated_names]
@staticmethod
@@ -592,37 +593,3 @@ def strip_rtf(text, default_encoding=None):
text = u''.join(out)
return text, default_encoding
-
-def natcmp(a, b):
- """
- Natural string comparison which mimics the behaviour of Python's internal cmp function.
- """
- if len(a) <= len(b):
- for i, key in enumerate(a):
- if isinstance(key, int) and isinstance(b[i], int):
- result = cmp(key, b[i])
- elif isinstance(key, int) and not isinstance(b[i], int):
- result = locale_direct_compare(str(key), b[i])
- elif not isinstance(key, int) and isinstance(b[i], int):
- result = locale_direct_compare(key, str(b[i]))
- else:
- result = locale_direct_compare(key, b[i])
- if result != 0:
- return result
- if len(a) == len(b):
- return 0
- else:
- return -1
- else:
- for i, key in enumerate(b):
- if isinstance(a[i], int) and isinstance(key, int):
- result = cmp(a[i], key)
- elif isinstance(a[i], int) and not isinstance(key, int):
- result = locale_direct_compare(str(a[i]), key)
- elif not isinstance(a[i], int) and isinstance(key, int):
- result = locale_direct_compare(a[i], str(key))
- else:
- result = locale_direct_compare(a[i], key)
- if result != 0:
- return result
- return 1
diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py
index db5f59357..015caa87d 100644
--- a/openlp/plugins/songs/lib/db.py
+++ b/openlp/plugins/songs/lib/db.py
@@ -38,6 +38,7 @@ from sqlalchemy.orm import mapper, relation, reconstructor
from sqlalchemy.sql.expression import func
from openlp.core.lib.db import BaseModel, init_db
+from openlp.core.utils import get_natural_key
class Author(BaseModel):
@@ -69,36 +70,15 @@ class Song(BaseModel):
def __init__(self):
self.sort_key = ()
- def _try_int(self, s):
- """
- Convert to integer if possible.
- """
- try:
- return int(s)
- except:
- return s.lower()
-
- def _natsort_key(self, s):
- """
- Used internally to get a tuple by which s is sorted.
- """
- return map(self._try_int, re.findall(r'(\d+|\D+)', s))
-
- # This decorator tells sqlalchemy to call this method everytime
- # any data on this object is updated.
-
@reconstructor
def init_on_load(self):
"""
- Precompute a tuple to be used for sorting.
+ Precompute a natural sorting, locale aware sorting key.
Song sorting is performance sensitive operation.
- To get maximum speed lets precompute the string
- used for comparison.
+ To get maximum speed lets precompute the sorting key.
"""
- # Avoid the overhead of converting string to lowercase and to QString
- # with every call to sort().
- self.sort_key = self._natsort_key(self.title)
+ self.sort_key = get_natural_key(self.title)
class Topic(BaseModel):
diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py
index 0c4898fd9..d75124d84 100644
--- a/openlp/plugins/songs/lib/mediaitem.py
+++ b/openlp/plugins/songs/lib/mediaitem.py
@@ -43,7 +43,7 @@ from openlp.plugins.songs.forms.editsongform import EditSongForm
from openlp.plugins.songs.forms.songmaintenanceform import SongMaintenanceForm
from openlp.plugins.songs.forms.songimportform import SongImportForm
from openlp.plugins.songs.forms.songexportform import SongExportForm
-from openlp.plugins.songs.lib import VerseType, clean_string, natcmp
+from openlp.plugins.songs.lib import VerseType, clean_string
from openlp.plugins.songs.lib.db import Author, Song, Book, MediaFile
from openlp.plugins.songs.lib.ui import SongStrings
from openlp.plugins.songs.lib.xml import OpenLyrics, SongXML
@@ -225,7 +225,7 @@ class SongMediaItem(MediaManagerItem):
log.debug(u'display results Song')
self.save_auto_select_id()
self.list_view.clear()
- searchresults.sort(cmp=natcmp, key=lambda song: song.sort_key)
+ searchresults.sort(key=lambda song: song.sort_key)
for song in searchresults:
# Do not display temporary songs
if song.temporary:
diff --git a/openlp/plugins/songs/lib/songimport.py b/openlp/plugins/songs/lib/songimport.py
index c5a63333c..8886e2884 100644
--- a/openlp/plugins/songs/lib/songimport.py
+++ b/openlp/plugins/songs/lib/songimport.py
@@ -260,7 +260,10 @@ class SongImport(QtCore.QObject):
elif int(verse_def[1:]) > self.verseCounts[verse_def[0]]:
self.verseCounts[verse_def[0]] = int(verse_def[1:])
self.verses.append([verse_def, verse_text.rstrip(), lang])
- self.verseOrderListGenerated.append(verse_def)
+ # A verse_def refers to all verses with that name, adding it once adds every instance, so do not add if already
+ # used.
+ if verse_def not in self.verseOrderListGenerated:
+ self.verseOrderListGenerated.append(verse_def)
def repeatVerse(self):
"""
diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py
index aadc61719..a72f83c4f 100644
--- a/openlp/plugins/songs/lib/songshowplusimport.py
+++ b/openlp/plugins/songs/lib/songshowplusimport.py
@@ -32,6 +32,7 @@ SongShow Plus songs into the OpenLP database.
"""
import os
import logging
+import re
import struct
from openlp.core.ui.wizard import WizardStrings
@@ -44,43 +45,36 @@ COPYRIGHT = 3
CCLI_NO = 5
VERSE = 12
CHORUS = 20
+BRIDGE = 24
TOPIC = 29
COMMENTS = 30
VERSE_ORDER = 31
SONG_BOOK = 35
SONG_NUMBER = 36
CUSTOM_VERSE = 37
-BRIDGE = 24
log = logging.getLogger(__name__)
class SongShowPlusImport(SongImport):
"""
- The :class:`SongShowPlusImport` class provides the ability to import song
- files from SongShow Plus.
+ The :class:`SongShowPlusImport` class provides the ability to import song files from SongShow Plus.
**SongShow Plus Song File Format:**
The SongShow Plus song file format is as follows:
- * Each piece of data in the song file has some information that precedes
- it.
+ * 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
+ 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
+ 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.
@@ -88,8 +82,9 @@ class SongShowPlusImport(SongImport):
* .sbsong
"""
- otherList = {}
- otherCount = 0
+
+ other_count = 0
+ other_list = {}
def __init__(self, manager, **kwargs):
"""
@@ -107,9 +102,9 @@ class SongShowPlusImport(SongImport):
for file in self.import_source:
if self.stop_import_flag:
return
- self.sspVerseOrderList = []
- other_count = 0
- other_list = {}
+ self.ssp_verse_order_list = []
+ self.other_count = 0
+ self.other_list = {}
file_name = os.path.split(file)[1]
self.import_wizard.increment_progress_bar(WizardStrings.ImportingType % file_name, 0)
song_data = open(file, 'rb')
@@ -162,34 +157,37 @@ class SongShowPlusImport(SongImport):
elif block_key == COMMENTS:
self.comments = unicode(data, u'cp1252')
elif block_key == VERSE_ORDER:
- verse_tag = self.toOpenLPVerseTag(data, True)
+ verse_tag = self.to_openlp_verse_tag(data, True)
if verse_tag:
if not isinstance(verse_tag, unicode):
verse_tag = unicode(verse_tag, u'cp1252')
- self.sspVerseOrderList.append(verse_tag)
+ self.ssp_verse_order_list.append(verse_tag)
elif block_key == SONG_BOOK:
self.songBookName = unicode(data, u'cp1252')
elif block_key == SONG_NUMBER:
self.songNumber = ord(data)
elif block_key == CUSTOM_VERSE:
- verse_tag = self.toOpenLPVerseTag(verse_name)
+ verse_tag = self.to_openlp_verse_tag(verse_name)
self.addVerse(unicode(data, u'cp1252'), verse_tag)
else:
log.debug("Unrecognised blockKey: %s, data: %s" % (block_key, data))
song_data.seek(next_block_starts)
- self.verseOrderList = self.sspVerseOrderList
+ self.verseOrderList = self.ssp_verse_order_list
song_data.close()
if not self.finish():
self.logError(file)
- def toOpenLPVerseTag(self, verse_name, ignore_unique=False):
- if verse_name.find(" ") != -1:
- verse_parts = verse_name.split(" ")
- verse_type = verse_parts[0]
- verse_number = verse_parts[1]
+ def to_openlp_verse_tag(self, verse_name, ignore_unique=False):
+ # Have we got any digits? If so, verse number is everything from the digits to the end (OpenLP does not have
+ # concept of part verses, so just ignore any non integers on the end (including floats))
+ match = re.match(r'(\D*)(\d+)', verse_name)
+ if match:
+ verse_type = match.group(1).strip()
+ verse_number = match.group(2)
else:
+ # otherwise we assume number 1 and take the whole prefix as the verse tag
verse_type = verse_name
- verse_number = "1"
+ verse_number = u'1'
verse_type = verse_type.lower()
if verse_type == "verse":
verse_tag = VerseType.tags[VerseType.Verse]
@@ -200,11 +198,11 @@ class SongShowPlusImport(SongImport):
elif verse_type == "pre-chorus":
verse_tag = VerseType.tags[VerseType.PreChorus]
else:
- if verse_name not in self.otherList:
+ if verse_name not in self.other_list:
if ignore_unique:
return None
- self.otherCount += 1
- self.otherList[verse_name] = str(self.otherCount)
+ self.other_count += 1
+ self.other_list[verse_name] = str(self.other_count)
verse_tag = VerseType.tags[VerseType.Other]
- verse_number = self.otherList[verse_name]
+ verse_number = self.other_list[verse_name]
return verse_tag + verse_number
diff --git a/scripts/check_dependencies.py b/scripts/check_dependencies.py
index 4c0f69b91..a6e075db4 100755
--- a/scripts/check_dependencies.py
+++ b/scripts/check_dependencies.py
@@ -83,6 +83,7 @@ MODULES = [
'mako',
'migrate',
'uno',
+ 'icu',
]
diff --git a/tests/functional/openlp_core_utils/test_utils.py b/tests/functional/openlp_core_utils/test_utils.py
index 6d95c6583..8e3a427ed 100644
--- a/tests/functional/openlp_core_utils/test_utils.py
+++ b/tests/functional/openlp_core_utils/test_utils.py
@@ -5,7 +5,9 @@ from unittest import TestCase
from mock import patch
-from openlp.core.utils import get_filesystem_encoding, _get_frozen_path, clean_filename, split_filename
+from openlp.core.utils import clean_filename, get_filesystem_encoding, _get_frozen_path, get_locale_key, \
+ get_natural_key, split_filename
+
class TestUtils(TestCase):
"""
@@ -89,7 +91,6 @@ class TestUtils(TestCase):
assert result == wanted_result, \
u'A two-entry tuple with the directory and file name (empty) should have been returned.'
-
def clean_filename_test(self):
"""
Test the clean_filename() function
@@ -103,3 +104,30 @@ class TestUtils(TestCase):
# THEN: The file name should be cleaned.
assert result == wanted_name, u'The file name should not contain any special characters.'
+
+ def get_locale_key_test(self):
+ """
+ Test the get_locale_key(string) function
+ """
+ with patch(u'openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
+ # GIVEN: The language is German
+ # 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss".
+ mocked_get_language.return_value = u'de'
+ unsorted_list = [u'Auszug', u'Aushang', u'\u00C4u\u00DFerung']
+ # WHEN: We sort the list and use get_locale_key() to generate the sorting keys
+ # THEN: We get a properly sorted list
+ test_passes = sorted(unsorted_list, key=get_locale_key) == [u'Aushang', u'\u00C4u\u00DFerung', u'Auszug']
+ assert test_passes, u'Strings should be sorted properly'
+
+ def get_natural_key_test(self):
+ """
+ Test the get_natural_key(string) function
+ """
+ with patch(u'openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
+ # GIVEN: The language is English (a language, which sorts digits before letters)
+ mocked_get_language.return_value = u'en'
+ unsorted_list = [u'item 10a', u'item 3b', u'1st item']
+ # WHEN: We sort the list and use get_natural_key() to generate the sorting keys
+ # THEN: We get a properly sorted list
+ test_passes = sorted(unsorted_list, key=get_natural_key) == [u'1st item', u'item 3b', u'item 10a']
+ assert test_passes, u'Numbers should be sorted naturally'
diff --git a/tests/functional/openlp_plugins/songs/test_songshowplusimport.py b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py
new file mode 100644
index 000000000..86d77bbdc
--- /dev/null
+++ b/tests/functional/openlp_plugins/songs/test_songshowplusimport.py
@@ -0,0 +1,235 @@
+"""
+This module contains tests for the SongShow Plus song importer.
+"""
+
+import os
+from unittest import TestCase
+from mock import patch, MagicMock
+
+from openlp.plugins.songs.lib import VerseType
+from openlp.plugins.songs.lib.songshowplusimport import SongShowPlusImport
+
+TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'../../../resources/songshowplussongs'))
+SONG_TEST_DATA = {u'Amazing Grace.sbsong':
+ {u'title': u'Amazing Grace (Demonstration)',
+ u'authors': [u'John Newton', u'Edwin Excell', u'John P. Rees'],
+ u'copyright': u'Public Domain ',
+ u'ccli_number': 22025,
+ u'verses':
+ [(u'Amazing grace! How sweet the sound!\r\nThat saved a wretch like me!\r\n'
+ u'I once was lost, but now am found;\r\nWas blind, but now I see.', u'v1'),
+ (u'\'Twas grace that taught my heart to fear,\r\nAnd grace my fears relieved.\r\n'
+ u'How precious did that grace appear,\r\nThe hour I first believed.', u'v2'),
+ (u'The Lord has promised good to me,\r\nHis Word my hope secures.\r\n'
+ u'He will my shield and portion be\r\nAs long as life endures.', u'v3'),
+ (u'Thro\' many dangers, toils and snares\r\nI have already come.\r\n'
+ u'\'Tis grace that brought me safe thus far,\r\nAnd grace will lead me home.', u'v4'),
+ (u'When we\'ve been there ten thousand years,\r\nBright shining as the sun,\r\n'
+ u'We\'ve no less days to sing God\'s praise,\r\nThan when we first begun.', u'v5')],
+ u'topics': [u'Assurance', u'Grace', u'Praise', u'Salvation'],
+ u'comments': u'\n\n\n',
+ u'song_book_name': u'Demonstration Songs',
+ u'song_number': 0,
+ u'verse_order_list': []},
+ u'Beautiful Garden Of Prayer.sbsong':
+ {u'title': u'Beautiful Garden Of Prayer (Demonstration)',
+ u'authors': [u'Eleanor Allen Schroll', u'James H. Fillmore'],
+ u'copyright': u'Public Domain ',
+ u'ccli_number': 60252,
+ u'verses':
+ [(u'There\'s a garden where Jesus is waiting,\r\nThere\'s a place that is wondrously fair.\r\n'
+ u'For it glows with the light of His presence,\r\n\'Tis the beautiful garden of prayer.', u'v1'),
+ (u'There\'s a garden where Jesus is waiting,\r\nAnd I go with my burden and care.\r\n'
+ u'Just to learn from His lips, words of comfort,\r\nIn the beautiful garden of prayer.', u'v2'),
+ (u'There\'s a garden where Jesus is waiting,\r\nAnd He bids you to come meet Him there,\r\n'
+ u'Just to bow and receive a new blessing,\r\nIn the beautiful garden of prayer.', u'v3'),
+ (u'O the beautiful garden, the garden of prayer,\r\nO the beautiful garden of prayer.\r\n'
+ u'There my Savior awaits, and He opens the gates\r\nTo the beautiful garden of prayer.', u'c1')],
+ u'topics': [u'Devotion', u'Prayer'],
+ u'comments': u'',
+ u'song_book_name': u'',
+ u'song_number': 0,
+ u'verse_order_list': []}}
+
+
+class TestSongShowPlusImport(TestCase):
+ """
+ Test the functions in the :mod:`songshowplusimport` module.
+ """
+ def create_importer_test(self):
+ """
+ Test creating an instance of the SongShow Plus file importer
+ """
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
+ mocked_manager = MagicMock()
+
+ # WHEN: An importer object is created
+ importer = SongShowPlusImport(mocked_manager)
+
+ # THEN: The importer object should not be None
+ self.assertIsNotNone(importer, u'Import should not be none')
+
+ def invalid_import_source_test(self):
+ """
+ Test SongShowPlusImport.doImport handles different invalid import_source values
+ """
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
+ mocked_manager = MagicMock()
+ mocked_import_wizard = MagicMock()
+ importer = SongShowPlusImport(mocked_manager)
+ importer.import_wizard = mocked_import_wizard
+ importer.stop_import_flag = True
+
+ # WHEN: Import source is not a list
+ for source in [u'not a list', 0]:
+ importer.import_source = source
+
+ # THEN: doImport should return none and the progress bar maximum should not be set.
+ self.assertIsNone(importer.doImport(), u'doImport should return None when import_source is not a list')
+ self.assertEquals(mocked_import_wizard.progress_bar.setMaximum.called, False,
+ u'setMaxium on import_wizard.progress_bar should not have been called')
+
+ def valid_import_source_test(self):
+ """
+ Test SongShowPlusImport.doImport handles different invalid import_source values
+ """
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
+ mocked_manager = MagicMock()
+ mocked_import_wizard = MagicMock()
+ importer = SongShowPlusImport(mocked_manager)
+ importer.import_wizard = mocked_import_wizard
+ importer.stop_import_flag = True
+
+ # WHEN: Import source is a list
+ importer.import_source = [u'List', u'of', u'files']
+
+ # THEN: doImport should return none and the progress bar setMaximum should be called with the length of
+ # import_source.
+ self.assertIsNone(importer.doImport(),
+ u'doImport should return None when import_source is a list and stop_import_flag is True')
+ mocked_import_wizard.progress_bar.setMaximum.assert_called_with(len(importer.import_source))
+
+ def to_openlp_verse_tag_test(self):
+ """
+ Test to_openlp_verse_tag method by simulating adding a verse
+ """
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
+ mocked_manager = MagicMock()
+ importer = SongShowPlusImport(mocked_manager)
+
+ # WHEN: Supplied with the following arguments replicating verses being added
+ test_values = [(u'Verse 1', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'Verse 2', VerseType.tags[VerseType.Verse] + u'2'),
+ (u'verse1', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'Verse', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'Verse1', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'chorus 1', VerseType.tags[VerseType.Chorus] + u'1'),
+ (u'bridge 1', VerseType.tags[VerseType.Bridge] + u'1'),
+ (u'pre-chorus 1', VerseType.tags[VerseType.PreChorus] + u'1'),
+ (u'different 1', VerseType.tags[VerseType.Other] + u'1'),
+ (u'random 1', VerseType.tags[VerseType.Other] + u'2')]
+
+ # THEN: The returned value should should correlate with the input arguments
+ for original_tag, openlp_tag in test_values:
+ self.assertEquals(importer.to_openlp_verse_tag(original_tag), openlp_tag,
+ u'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"'
+ % (openlp_tag, original_tag))
+
+ def to_openlp_verse_tag_verse_order_test(self):
+ """
+ Test to_openlp_verse_tag method by simulating adding a verse to the verse order
+ """
+ # GIVEN: A mocked out SongImport class, and a mocked out "manager"
+ with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
+ mocked_manager = MagicMock()
+ importer = SongShowPlusImport(mocked_manager)
+
+ # WHEN: Supplied with the following arguments replicating a verse order being added
+ test_values = [(u'Verse 1', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'Verse 2', VerseType.tags[VerseType.Verse] + u'2'),
+ (u'verse1', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'Verse', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'Verse1', VerseType.tags[VerseType.Verse] + u'1'),
+ (u'chorus 1', VerseType.tags[VerseType.Chorus] + u'1'),
+ (u'bridge 1', VerseType.tags[VerseType.Bridge] + u'1'),
+ (u'pre-chorus 1', VerseType.tags[VerseType.PreChorus] + u'1'),
+ (u'different 1', VerseType.tags[VerseType.Other] + u'1'),
+ (u'random 1', VerseType.tags[VerseType.Other] + u'2'),
+ (u'unused 2', None)]
+
+ # THEN: The returned value should should correlate with the input arguments
+ for original_tag, openlp_tag in test_values:
+ self.assertEquals(importer.to_openlp_verse_tag(original_tag, ignore_unique=True), openlp_tag,
+ u'SongShowPlusImport.to_openlp_verse_tag should return "%s" when called with "%s"'
+ % (openlp_tag, original_tag))
+
+ def file_import_test(self):
+ """
+ Test the actual import of real song files and check that the imported data is correct.
+ """
+
+ # GIVEN: Test files with a mocked out SongImport class, a mocked out "manager", a mocked out "import_wizard",
+ # and mocked out "author", "add_copyright", "add_verse", "finish" methods.
+ with patch(u'openlp.plugins.songs.lib.songshowplusimport.SongImport'):
+ for song_file in SONG_TEST_DATA:
+ mocked_manager = MagicMock()
+ mocked_import_wizard = MagicMock()
+ mocked_parse_author = MagicMock()
+ mocked_add_copyright = MagicMock()
+ mocked_add_verse = MagicMock()
+ mocked_finish = MagicMock()
+ mocked_finish.return_value = True
+ importer = SongShowPlusImport(mocked_manager)
+ importer.import_wizard = mocked_import_wizard
+ importer.stop_import_flag = False
+ importer.parse_author = mocked_parse_author
+ importer.addCopyright = mocked_add_copyright
+ importer.addVerse = mocked_add_verse
+ importer.finish = mocked_finish
+ importer.topics = []
+
+ # WHEN: Importing each file
+ importer.import_source = [os.path.join(TEST_PATH, song_file)]
+ title = SONG_TEST_DATA[song_file][u'title']
+ author_calls = SONG_TEST_DATA[song_file][u'authors']
+ song_copyright = SONG_TEST_DATA[song_file][u'copyright']
+ ccli_number = SONG_TEST_DATA[song_file][u'ccli_number']
+ add_verse_calls = SONG_TEST_DATA[song_file][u'verses']
+ topics = SONG_TEST_DATA[song_file][u'topics']
+ comments = SONG_TEST_DATA[song_file][u'comments']
+ song_book_name = SONG_TEST_DATA[song_file][u'song_book_name']
+ song_number = SONG_TEST_DATA[song_file][u'song_number']
+ verse_order_list = SONG_TEST_DATA[song_file][u'verse_order_list']
+
+ # THEN: doImport should return none, the song data should be as expected, and finish should have been
+ # called.
+ self.assertIsNone(importer.doImport(), u'doImport should return None when it has completed')
+ self.assertEquals(importer.title, title, u'title for %s should be "%s"' % (song_file, title))
+ for author in author_calls:
+ mocked_parse_author.assert_any_call(author)
+ if song_copyright:
+ mocked_add_copyright.assert_called_with(song_copyright)
+ if ccli_number:
+ self.assertEquals(importer.ccliNumber, ccli_number, u'ccliNumber for %s should be %s'
+ % (song_file, ccli_number))
+ for verse_text, verse_tag in add_verse_calls:
+ mocked_add_verse.assert_any_call(verse_text, verse_tag)
+ if topics:
+ self.assertEquals(importer.topics, topics, u'topics for %s should be %s' % (song_file, topics))
+ if comments:
+ self.assertEquals(importer.comments, comments, u'comments for %s should be "%s"'
+ % (song_file, comments))
+ if song_book_name:
+ self.assertEquals(importer.songBookName, song_book_name, u'songBookName for %s should be "%s"'
+ % (song_file, song_book_name))
+ if song_number:
+ self.assertEquals(importer.songNumber, song_number, u'songNumber for %s should be %s'
+ % (song_file, song_number))
+ if verse_order_list:
+ self.assertEquals(importer.verseOrderList, [], u'verseOrderList for %s should be %s'
+ % (song_file, verse_order_list))
+ mocked_finish.assert_called_with()
diff --git a/tests/interfaces/openlp_core_ui/test_servicemanager.py b/tests/interfaces/openlp_core_ui/test_servicemanager.py
index 4e309e889..a651ca29c 100644
--- a/tests/interfaces/openlp_core_ui/test_servicemanager.py
+++ b/tests/interfaces/openlp_core_ui/test_servicemanager.py
@@ -3,11 +3,11 @@
"""
from unittest import TestCase
-from mock import MagicMock, patch
+from mock import MagicMock, Mock, patch
from PyQt4 import QtGui
-from openlp.core.lib import Registry, ScreenList
+from openlp.core.lib import Registry, ScreenList, ServiceItem
from openlp.core.ui.mainwindow import MainWindow
@@ -42,3 +42,44 @@ class TestServiceManager(TestCase):
# THEN the count of items should be zero
self.assertEqual(self.service_manager.service_manager_list.topLevelItemCount(), 0,
u'The service manager list should be empty ')
+
+ def context_menu_test(self):
+ """
+ Test the context_menu() method.
+ """
+ # GIVEN: A service item added
+ with patch(u'PyQt4.QtGui.QTreeWidget.itemAt') as mocked_item_at_method, \
+ patch(u'PyQt4.QtGui.QWidget.mapToGlobal') as mocked_map_to_global, \
+ patch(u'PyQt4.QtGui.QMenu.exec_') as mocked_exec:
+ mocked_item = MagicMock()
+ mocked_item.parent.return_value = None
+ mocked_item_at_method.return_value = mocked_item
+ # We want 1 to be returned for the position
+ mocked_item.data.return_value = 1
+ # A service item without capabilities.
+ service_item = ServiceItem()
+ self.service_manager.service_items = [{u'service_item': service_item}]
+ q_point = None
+ # Mocked actions.
+ self.service_manager.edit_action.setVisible = Mock()
+ self.service_manager.create_custom_action.setVisible = Mock()
+ self.service_manager.maintain_action.setVisible = Mock()
+ self.service_manager.notes_action.setVisible = Mock()
+ self.service_manager.time_action.setVisible = Mock()
+ self.service_manager.auto_start_action.setVisible = Mock()
+
+ # WHEN: Show the context menu.
+ self.service_manager.context_menu(q_point)
+
+ # THEN: The following actions should be not visible.
+ self.service_manager.edit_action.setVisible.assert_called_once_with(False), \
+ u'The action should be set invisible.'
+ self.service_manager.create_custom_action.setVisible.assert_called_once_with(False), \
+ u'The action should be set invisible.'
+ self.service_manager.maintain_action.setVisible.assert_called_once_with(False), \
+ u'The action should be set invisible.'
+ self.service_manager.notes_action.setVisible.assert_called_with(True), u'The action should be set visible.'
+ self.service_manager.time_action.setVisible.assert_called_once_with(False), \
+ u'The action should be set invisible.'
+ self.service_manager.auto_start_action.setVisible.assert_called_once_with(False), \
+ u'The action should be set invisible.'
diff --git a/tests/resources/songshowplussongs/Amazing Grace.sbsong b/tests/resources/songshowplussongs/Amazing Grace.sbsong
new file mode 100644
index 000000000..14b7c3597
Binary files /dev/null and b/tests/resources/songshowplussongs/Amazing Grace.sbsong differ
diff --git a/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong b/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong
new file mode 100644
index 000000000..c227d4809
Binary files /dev/null and b/tests/resources/songshowplussongs/Beautiful Garden Of Prayer.sbsong differ