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