This commit is contained in:
Andreas Preikschat 2013-06-16 18:40:47 +02:00
commit 5cce4c9443
29 changed files with 1672 additions and 290 deletions

View File

@ -1 +1 @@
2.1.0-bzr2141
2.1.0-bzr2234

View File

@ -272,6 +272,7 @@ class Settings(QtCore.QSettings):
u'shortcuts/songImportItem': [],
u'shortcuts/themeScreen': [QtGui.QKeySequence(u'T')],
u'shortcuts/toolsReindexItem': [],
u'shortcuts/toolsFindDuplicates': [],
u'shortcuts/toolsAlertItem': [QtGui.QKeySequence(u'F7')],
u'shortcuts/toolsFirstTimeWizard': [],
u'shortcuts/toolsOpenDataFolder': [],

View File

@ -0,0 +1,173 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`listpreviewwidget` is a widget that lists the slides in the slide controller.
It is based on a QTableWidget but represents its contents in list form.
"""
from PyQt4 import QtCore, QtGui
from openlp.core.lib import ImageSource, Registry, ServiceItem
class ListPreviewWidget(QtGui.QTableWidget):
def __init__(self, parent, screen_ratio):
"""
Initializes the widget to default state.
An empty ServiceItem is used per default.
One needs to call replace_service_manager_item() to make this widget display something.
"""
super(QtGui.QTableWidget, self).__init__(parent)
# Set up the widget.
self.setColumnCount(1)
self.horizontalHeader().setVisible(False)
self.setColumnWidth(0, parent.width())
self.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.setAlternatingRowColors(True)
# Initialize variables.
self.service_item = ServiceItem()
self.screen_ratio = screen_ratio
def resizeEvent(self, QResizeEvent):
"""
Overloaded method from QTableWidget. Will recalculate the layout.
"""
self.__recalculate_layout()
def __recalculate_layout(self):
"""
Recalculates the layout of the table widget. It will set height and width
of the table cells. QTableWidget does not adapt the cells to the widget size on its own.
"""
self.setColumnWidth(0, self.viewport().width())
if self.service_item:
# Sort out songs, bibles, etc.
if self.service_item.is_text():
self.resizeRowsToContents()
else:
# Sort out image heights.
for framenumber in range(len(self.service_item.get_frames())):
height = self.viewport().width() / self.screen_ratio
self.setRowHeight(framenumber, height)
def screen_size_changed(self, screen_ratio):
"""
To be called whenever the live screen size changes.
Because this makes a layout recalculation necessary.
"""
self.screen_ratio = screen_ratio
self.__recalculate_layout()
def replace_service_item(self, service_item, width, slideNumber):
"""
Replaces the current preview items with the ones in service_item.
Displays the given slide.
"""
self.service_item = service_item
self.clear()
self.setRowCount(0)
self.setColumnWidth(0, width)
row = 0
text = []
for framenumber, frame in enumerate(self.service_item.get_frames()):
self.setRowCount(self.slide_count() + 1)
item = QtGui.QTableWidgetItem()
slideHeight = 0
if self.service_item.is_text():
if frame[u'verseTag']:
# These tags are already translated.
verse_def = frame[u'verseTag']
verse_def = u'%s%s' % (verse_def[0], verse_def[1:])
two_line_def = u'%s\n%s' % (verse_def[0], verse_def[1:])
row = two_line_def
else:
row += 1
item.setText(frame[u'text'])
else:
label = QtGui.QLabel()
label.setMargin(4)
if self.service_item.is_media():
label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
else:
label.setScaledContents(True)
if self.service_item.is_command():
label.setPixmap(QtGui.QPixmap(frame[u'image']))
else:
image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin)
label.setPixmap(QtGui.QPixmap.fromImage(image))
self.setCellWidget(framenumber, 0, label)
slideHeight = width / self.screen_ratio
row += 1
text.append(unicode(row))
self.setItem(framenumber, 0, item)
if slideHeight:
self.setRowHeight(framenumber, slideHeight)
self.setVerticalHeaderLabels(text)
if self.service_item.is_text():
self.resizeRowsToContents()
self.setColumnWidth(0, self.viewport().width())
self.setFocus()
self.change_slide(slideNumber)
def change_slide(self, slide):
"""
Switches to the given row.
"""
if slide >= self.slide_count():
slide = self.slide_count() - 1
# Scroll to next item if possible.
if slide + 1 < self.slide_count():
self.scrollToItem(self.item(slide + 1, 0))
self.selectRow(slide)
def current_slide_number(self):
"""
Returns the position of the currently active item. Will return -1 if the widget is empty.
"""
return super(ListPreviewWidget, self).currentRow()
def slide_count(self):
"""
Returns the number of slides this widget holds.
"""
return super(ListPreviewWidget, self).rowCount()
def _get_image_manager(self):
"""
Adds the image manager to the class dynamically.
"""
if not hasattr(self, u'_image_manager'):
self._image_manager = Registry().get(u'image_manager')
return self._image_manager
image_manager = property(_get_image_manager)

View File

@ -793,8 +793,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
"""
We need to make sure, that the SlidePreview's size is correct.
"""
self.preview_controller.previewSizeChanged()
self.live_controller.previewSizeChanged()
self.preview_controller.preview_size_changed()
self.live_controller.preview_size_changed()
def on_settings_shortcuts_item_clicked(self):
"""
@ -1001,8 +1001,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
self.application.set_busy_cursor()
self.image_manager.update_display()
self.renderer.update_display()
self.preview_controller.screenSizeChanged()
self.live_controller.screenSizeChanged()
self.preview_controller.screen_size_changed()
self.live_controller.screen_size_changed()
self.setFocus()
self.activateWindow()
self.application.set_normal_cursor()

View File

@ -1369,7 +1369,7 @@ class ServiceManager(QtGui.QWidget, ServiceManagerDialog):
self.preview_controller.addServiceManagerItem(self.service_items[item][u'service_item'], 0)
next_item = self.service_manager_list.topLevelItem(item)
self.service_manager_list.setCurrentItem(next_item)
self.live_controller.preview_list_widget.setFocus()
self.live_controller.preview_widget.setFocus()
else:
critical_error_message_box(translate('OpenLP.ServiceManager', 'Missing Display Handler'),
translate('OpenLP.ServiceManager',

View File

@ -41,6 +41,7 @@ from openlp.core.lib import OpenLPToolbar, ItemCapabilities, ServiceItem, ImageS
from openlp.core.ui import HideMode, MainDisplay, Display, DisplayControllerType
from openlp.core.lib.ui import create_action
from openlp.core.utils.actions import ActionList, CategoryOrder
from openlp.core.ui.listpreviewwidget import ListPreviewWidget
log = logging.getLogger(__name__)
@ -89,7 +90,7 @@ class SlideController(DisplayController):
Set up the Slide Controller.
"""
DisplayController.__init__(self, parent, is_live)
Registry().register_function(u'bootstrap_post_set_up', self.screenSizeChanged)
Registry().register_function(u'bootstrap_post_set_up', self.screen_size_changed)
self.screens = ScreenList()
try:
self.ratio = float(self.screens.current[u'size'].width()) / float(self.screens.current[u'size'].height())
@ -121,6 +122,8 @@ class SlideController(DisplayController):
self.update_slide_limits()
self.panel = QtGui.QWidget(parent.control_splitter)
self.slideList = {}
self.slide_count = 0
self.slide_image = None
# Layout for holding panel
self.panel_layout = QtGui.QVBoxLayout(self.panel)
self.panel_layout.setSpacing(0)
@ -157,18 +160,8 @@ class SlideController(DisplayController):
self.controller_layout.setSpacing(0)
self.controller_layout.setMargin(0)
# Controller list view
self.preview_list_widget = QtGui.QTableWidget(self.controller)
self.preview_list_widget.setColumnCount(1)
self.preview_list_widget.horizontalHeader().setVisible(False)
self.preview_list_widget.setColumnWidth(0, self.controller.width())
self.preview_list_widget.is_live = self.is_live
self.preview_list_widget.setObjectName(u'preview_list_widget')
self.preview_list_widget.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.preview_list_widget.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
self.preview_list_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.preview_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.preview_list_widget.setAlternatingRowColors(True)
self.controller_layout.addWidget(self.preview_list_widget)
self.preview_widget = ListPreviewWidget(self, self.ratio)
self.controller_layout.addWidget(self.preview_widget)
# Build the full toolbar
self.toolbar = OpenLPToolbar(self)
size_toolbar_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
@ -321,18 +314,18 @@ class SlideController(DisplayController):
self.slide_layout.insertWidget(0, self.preview_display)
self.preview_display.hide()
# Actual preview screen
self.slidePreview = QtGui.QLabel(self)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.slidePreview.sizePolicy().hasHeightForWidth())
self.slidePreview.setSizePolicy(sizePolicy)
self.slidePreview.setFrameShape(QtGui.QFrame.Box)
self.slidePreview.setFrameShadow(QtGui.QFrame.Plain)
self.slidePreview.setLineWidth(1)
self.slidePreview.setScaledContents(True)
self.slidePreview.setObjectName(u'slidePreview')
self.slide_layout.insertWidget(0, self.slidePreview)
self.slide_preview = QtGui.QLabel(self)
size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
size_policy.setHorizontalStretch(0)
size_policy.setVerticalStretch(0)
size_policy.setHeightForWidth(self.slide_preview.sizePolicy().hasHeightForWidth())
self.slide_preview.setSizePolicy(size_policy)
self.slide_preview.setFrameShape(QtGui.QFrame.Box)
self.slide_preview.setFrameShadow(QtGui.QFrame.Plain)
self.slide_preview.setLineWidth(1)
self.slide_preview.setScaledContents(True)
self.slide_preview.setObjectName(u'slide_preview')
self.slide_layout.insertWidget(0, self.slide_preview)
self.grid.addLayout(self.slide_layout, 0, 0, 1, 1)
if self.is_live:
self.current_shortcut = u''
@ -350,7 +343,7 @@ class SlideController(DisplayController):
{u'key': u'O', u'configurable': True, u'text': translate('OpenLP.SlideController', 'Go to "Other"')}
]
shortcuts.extend([{u'key': unicode(number)} for number in range(10)])
self.preview_list_widget.addActions([create_action(self,
self.controller.addActions([create_action(self,
u'shortcutAction_%s' % s[u'key'], text=s.get(u'text'),
can_shortcuts=True,
context=QtCore.Qt.WidgetWithChildrenShortcut,
@ -358,7 +351,7 @@ class SlideController(DisplayController):
triggers=self._slideShortcutActivated) for s in shortcuts])
self.shortcutTimer.timeout.connect(self._slideShortcutActivated)
# Signals
self.preview_list_widget.clicked.connect(self.onSlideSelected)
self.preview_widget.clicked.connect(self.onSlideSelected)
if self.is_live:
# Need to use event as called across threads and UI is updated
QtCore.QObject.connect(self, QtCore.SIGNAL(u'slidecontroller_toggle_display'), self.toggle_display)
@ -366,13 +359,13 @@ class SlideController(DisplayController):
self.toolbar.set_widget_visible(self.loop_list, False)
self.toolbar.set_widget_visible(self.wide_menu, False)
else:
self.preview_list_widget.doubleClicked.connect(self.onGoLiveClick)
self.preview_widget.doubleClicked.connect(self.onGoLiveClick)
self.toolbar.set_widget_visible([u'editSong'], False)
if self.is_live:
self.setLiveHotkeys(self)
self.__addActionsToWidget(self.preview_list_widget)
self.__addActionsToWidget(self.controller)
else:
self.preview_list_widget.addActions([self.nextItem, self.previous_item])
self.controller.addActions([self.nextItem, self.previous_item])
Registry().register_function(u'slidecontroller_%s_stop_loop' % self.type_prefix, self.on_stop_loop)
Registry().register_function(u'slidecontroller_%s_change' % self.type_prefix, self.on_slide_change)
Registry().register_function(u'slidecontroller_%s_blank' % self.type_prefix, self.on_slide_blank)
@ -431,7 +424,7 @@ class SlideController(DisplayController):
if len(matches) == 1:
self.shortcutTimer.stop()
self.current_shortcut = u''
self.__checkUpdateSelectedSlide(self.slideList[matches[0]])
self.preview_widget.change_slide(self.slideList[matches[0]])
self.slideSelected()
elif sender_name != u'shortcutTimer':
# Start the time as we did not have any match.
@ -441,7 +434,7 @@ class SlideController(DisplayController):
if self.current_shortcut in keys:
# We had more than one match for example "V1" and "V10", but
# "V1" was the slide we wanted to go.
self.__checkUpdateSelectedSlide(self.slideList[self.current_shortcut])
self.preview_widget.change_slide(self.slideList[self.current_shortcut])
self.slideSelected()
# Reset the shortcut.
self.current_shortcut = u''
@ -517,10 +510,9 @@ class SlideController(DisplayController):
self.service_manager.next_item()
self.keypress_loop = False
def screenSizeChanged(self):
def screen_size_changed(self):
"""
Settings dialog has changed the screen size of adjust output and
screen previews.
Settings dialog has changed the screen size of adjust output and screen previews.
"""
# rebuild display as screen size changed
if self.display:
@ -536,14 +528,15 @@ class SlideController(DisplayController):
except ZeroDivisionError:
self.ratio = 1
self.media_controller.setup_display(self.display, False)
self.previewSizeChanged()
self.preview_size_changed()
self.preview_widget.screen_size_changed(self.ratio)
self.preview_display.setup()
service_item = ServiceItem()
self.preview_display.web_view.setHtml(build_html(service_item, self.preview_display.screen, None, self.is_live,
plugins=self.plugin_manager.plugins))
self.media_controller.setup_display(self.preview_display, True)
if self.service_item:
self.refreshServiceItem()
self.refresh_service_item()
def __addActionsToWidget(self, widget):
"""
@ -554,7 +547,7 @@ class SlideController(DisplayController):
self.previousService, self.nextService,
self.escapeItem])
def previewSizeChanged(self):
def preview_size_changed(self):
"""
Takes care of the SlidePreview's size. Is called when one of the the
splitters is moved or when the screen size is changed. Note, that this
@ -563,28 +556,16 @@ class SlideController(DisplayController):
if self.ratio < float(self.preview_frame.width()) / float(self.preview_frame.height()):
# We have to take the height as limit.
max_height = self.preview_frame.height() - self.grid.margin() * 2
self.slidePreview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
self.slide_preview.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
self.preview_display.setFixedSize(QtCore.QSize(max_height * self.ratio, max_height))
self.preview_display.screen = {
u'size': self.preview_display.geometry()}
else:
# We have to take the width as limit.
max_width = self.preview_frame.width() - self.grid.margin() * 2
self.slidePreview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
self.slide_preview.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
self.preview_display.setFixedSize(QtCore.QSize(max_width, max_width / self.ratio))
self.preview_display.screen = {
u'size': self.preview_display.geometry()}
# Make sure that the frames have the correct size.
self.preview_list_widget.setColumnWidth(0, self.preview_list_widget.viewport().size().width())
if self.service_item:
# Sort out songs, bibles, etc.
if self.service_item.is_text():
self.preview_list_widget.resizeRowsToContents()
else:
# Sort out image heights.
width = self.main_window.control_splitter.sizes()[self.split]
for framenumber in range(len(self.service_item.get_frames())):
self.preview_list_widget.setRowHeight(framenumber, width / self.ratio)
self.preview_display.screen = {u'size': self.preview_display.geometry()}
self.onControllerSizeChanged(self.controller.width())
def onControllerSizeChanged(self, width):
@ -624,17 +605,16 @@ class SlideController(DisplayController):
"""
self.slide_limits = Settings().value(self.main_window.advanced_settings_section + u'/slide limits')
def enableToolBar(self, item):
def enable_tool_bar(self, item):
"""
Allows the toolbars to be reconfigured based on Controller Type
and ServiceItem Type
Allows the toolbars to be reconfigured based on Controller Type and ServiceItem Type
"""
if self.is_live:
self.enableLiveToolBar(item)
self.enable_live_tool_bar(item)
else:
self.enablePreviewToolBar(item)
self.enable_preview_tool_bar(item)
def enableLiveToolBar(self, item):
def enable_live_tool_bar(self, item):
"""
Allows the live toolbar to be customised
"""
@ -663,7 +643,7 @@ class SlideController(DisplayController):
# See bug #791050
self.toolbar.show()
def enablePreviewToolBar(self, item):
def enable_preview_tool_bar(self, item):
"""
Allows the Preview toolbar to be customised
"""
@ -682,15 +662,15 @@ class SlideController(DisplayController):
# See bug #791050
self.toolbar.show()
def refreshServiceItem(self):
def refresh_service_item(self):
"""
Method to update the service item if the screen has changed
"""
log.debug(u'refreshServiceItem live = %s' % self.is_live)
log.debug(u'refresh_service_item live = %s' % self.is_live)
if self.service_item.is_text() or self.service_item.is_image():
item = self.service_item
item.render()
self._processItem(item, self.selected_row)
self._process_item(item, self.selected_row)
def add_service_item(self, item):
"""
@ -703,14 +683,14 @@ class SlideController(DisplayController):
if self.song_edit:
slideno = self.selected_row
self.song_edit = False
self._processItem(item, slideno)
self._process_item(item, slideno)
def replaceServiceManagerItem(self, item):
"""
Replacement item following a remote edit
"""
if item == self.service_item:
self._processItem(item, self.preview_list_widget.currentRow())
self._process_item(item, self.preview_widget.current_slide_number())
def addServiceManagerItem(self, item, slideno):
"""
@ -726,10 +706,10 @@ class SlideController(DisplayController):
slidenum = 0
# If service item is the same as the current one, only change slide
if slideno >= 0 and item == self.service_item:
self.__checkUpdateSelectedSlide(slidenum)
self.preview_widget.change_slide(slidenum)
self.slideSelected()
else:
self._processItem(item, slidenum)
self._process_item(item, slidenum)
if self.is_live and item.auto_play_slides_loop and item.timed_slide_interval > 0:
self.play_slides_loop.setChecked(item.auto_play_slides_loop)
self.delay_spin_box.setValue(int(item.timed_slide_interval))
@ -739,7 +719,7 @@ class SlideController(DisplayController):
self.delay_spin_box.setValue(int(item.timed_slide_interval))
self.onPlaySlidesOnce()
def _processItem(self, service_item, slideno):
def _process_item(self, service_item, slideno):
"""
Loads a ServiceItem into the system from ServiceManager
Display the slide number passed
@ -751,12 +731,9 @@ class SlideController(DisplayController):
self.service_item = copy.copy(service_item)
if old_item and self.is_live and old_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
self._resetBlank()
Registry().execute(u'%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slideno])
Registry().execute(
u'%s_start' % service_item.name.lower(), [service_item, self.is_live, self.hide_mode(), slideno])
self.slideList = {}
width = self.main_window.control_splitter.sizes()[self.split]
self.preview_list_widget.clear()
self.preview_list_widget.setRowCount(0)
self.preview_list_widget.setColumnWidth(0, width)
if self.is_live:
self.song_menu.menu().clear()
self.display.audio_player.reset()
@ -781,9 +758,8 @@ class SlideController(DisplayController):
self.setAudioItemsVisibility(True)
row = 0
text = []
width = self.main_window.controlSplitter.sizes()[self.split]
for framenumber, frame in enumerate(self.service_item.get_frames()):
self.preview_list_widget.setRowCount(self.preview_list_widget.rowCount() + 1)
item = QtGui.QTableWidgetItem()
slideHeight = 0
if self.service_item.is_text():
if frame[u'verseTag']:
@ -799,38 +775,16 @@ class SlideController(DisplayController):
else:
row += 1
self.slideList[unicode(row)] = row - 1
item.setText(frame[u'text'])
else:
label = QtGui.QLabel()
label.setMargin(4)
if service_item.is_media():
label.setAlignment(QtCore.Qt.AlignHCenter | QtCore.Qt.AlignVCenter)
else:
label.setScaledContents(True)
if self.service_item.is_command():
label.setPixmap(QtGui.QPixmap(frame[u'image']))
else:
# If current slide set background to image
if framenumber == slideno:
self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'],
ImageSource.ImagePlugin)
image = self.image_manager.get_image(frame[u'path'], ImageSource.ImagePlugin)
label.setPixmap(QtGui.QPixmap.fromImage(image))
self.preview_list_widget.setCellWidget(framenumber, 0, label)
slideHeight = width * (1 / self.ratio)
row += 1
self.slideList[unicode(row)] = row - 1
text.append(unicode(row))
self.preview_list_widget.setItem(framenumber, 0, item)
if slideHeight:
self.preview_list_widget.setRowHeight(framenumber, slideHeight)
self.preview_list_widget.setVerticalHeaderLabels(text)
if self.service_item.is_text():
self.preview_list_widget.resizeRowsToContents()
self.preview_list_widget.setColumnWidth(0,
self.preview_list_widget.viewport().size().width())
self.__updatePreviewSelection(slideno)
self.enableToolBar(service_item)
# If current slide set background to image
if not self.service_item.is_command() and framenumber == slideno:
self.service_item.bg_image_bytes = self.image_manager.get_image_bytes(frame[u'path'],
ImageSource.ImagePlugin)
self.preview_widget.replace_service_item(self.service_item, width, slideno)
self.enable_tool_bar(service_item)
# Pass to display for viewing.
# Postpone image build, we need to do this later to avoid the theme
# flashing on the screen
@ -839,7 +793,6 @@ class SlideController(DisplayController):
if service_item.is_media():
self.onMediaStart(service_item)
self.slideSelected(True)
self.preview_list_widget.setFocus()
if old_item:
# Close the old item after the new one is opened
# This avoids the service theme/desktop flashing on screen
@ -851,16 +804,6 @@ class SlideController(DisplayController):
self.onMediaClose()
Registry().execute(u'slidecontroller_%s_started' % self.type_prefix, [service_item])
def __updatePreviewSelection(self, slideno):
"""
Utility method to update the selected slide in the list.
"""
if slideno > self.preview_list_widget.rowCount():
self.preview_list_widget.selectRow(
self.preview_list_widget.rowCount() - 1)
else:
self.__checkUpdateSelectedSlide(slideno)
# Screen event methods
def on_slide_selected_index(self, message):
"""
@ -873,7 +816,7 @@ class SlideController(DisplayController):
Registry().execute(u'%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index])
self.updatePreview()
else:
self.__checkUpdateSelectedSlide(index)
self.preview_widget.change_slide(index)
self.slideSelected()
def mainDisplaySetBackground(self):
@ -1016,9 +959,9 @@ class SlideController(DisplayController):
Generate the preview when you click on a slide.
if this is the Live Controller also display on the screen
"""
row = self.preview_list_widget.currentRow()
row = self.preview_widget.current_slide_number()
self.selected_row = 0
if -1 < row < self.preview_list_widget.rowCount():
if -1 < row < self.preview_widget.slide_count():
if self.service_item.is_command():
if self.is_live and not start:
Registry().execute(u'%s_slide' % self.service_item.name.lower(),
@ -1036,7 +979,7 @@ class SlideController(DisplayController):
self.service_item.bg_image_bytes = None
self.updatePreview()
self.selected_row = row
self.__checkUpdateSelectedSlide(row)
self.preview_widget.change_slide(row)
Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row)
self.display.setFocus()
@ -1044,33 +987,34 @@ class SlideController(DisplayController):
"""
The slide has been changed. Update the slidecontroller accordingly
"""
self.__checkUpdateSelectedSlide(row)
self.preview_widget.change_slide(row)
self.updatePreview()
Registry().execute(u'slidecontroller_%s_changed' % self.type_prefix, row)
def updatePreview(self):
"""
This updates the preview frame, for example after changing a slide or
using *Blank to Theme*.
This updates the preview frame, for example after changing a slide or using *Blank to Theme*.
"""
log.debug(u'updatePreview %s ' % self.screens.current[u'primary'])
if not self.screens.current[u'primary'] and self.service_item and \
self.service_item.is_capable(ItemCapabilities.ProvidesOwnDisplay):
# Grab now, but try again in a couple of seconds if slide change
# is slow
QtCore.QTimer.singleShot(0.5, self.grabMainDisplay)
QtCore.QTimer.singleShot(2.5, self.grabMainDisplay)
# Grab now, but try again in a couple of seconds if slide change is slow
QtCore.QTimer.singleShot(0.5, self.grab_maindisplay)
QtCore.QTimer.singleShot(2.5, self.grab_maindisplay)
else:
self.slidePreview.setPixmap(self.display.preview())
self.slide_image = self.display.preview()
self.slide_preview.setPixmap(self.slide_image)
self.slide_count += 1
def grabMainDisplay(self):
def grab_maindisplay(self):
"""
Creates an image of the current screen and updates the preview frame.
"""
winid = QtGui.QApplication.desktop().winId()
win_id = QtGui.QApplication.desktop().winId()
rect = self.screens.current[u'size']
winimg = QtGui.QPixmap.grabWindow(winid, rect.x(), rect.y(), rect.width(), rect.height())
self.slidePreview.setPixmap(winimg)
win_image = QtGui.QPixmap.grabWindow(win_id, rect.x(), rect.y(), rect.width(), rect.height())
self.slide_preview.setPixmap(win_image)
self.slide_image = win_image
def on_slide_selected_next_action(self, checked):
"""
@ -1089,8 +1033,8 @@ class SlideController(DisplayController):
if self.service_item.is_command() and self.is_live:
self.updatePreview()
else:
row = self.preview_list_widget.currentRow() + 1
if row == self.preview_list_widget.rowCount():
row = self.preview_widget.current_slide_number() + 1
if row == self.preview_widget.slide_count():
if wrap is None:
if self.slide_limits == SlideLimits.Wrap:
row = 0
@ -1098,12 +1042,12 @@ class SlideController(DisplayController):
self.serviceNext()
return
else:
row = self.preview_list_widget.rowCount() - 1
row = self.preview_widget.slide_count() - 1
elif wrap:
row = 0
else:
row = self.preview_list_widget.rowCount() - 1
self.__checkUpdateSelectedSlide(row)
row = self.preview_widget.slide_count() - 1
self.preview_widget.change_slide(row)
self.slideSelected()
def on_slide_selected_previous(self):
@ -1116,27 +1060,19 @@ class SlideController(DisplayController):
if self.service_item.is_command() and self.is_live:
self.updatePreview()
else:
row = self.preview_list_widget.currentRow() - 1
row = self.preview_widget.current_slide_number() - 1
if row == -1:
if self.slide_limits == SlideLimits.Wrap:
row = self.preview_list_widget.rowCount() - 1
row = self.preview_widget.slide_count() - 1
elif self.is_live and self.slide_limits == SlideLimits.Next:
self.keypress_queue.append(ServiceItemAction.PreviousLastSlide)
self._process_queue()
return
else:
row = 0
self.__checkUpdateSelectedSlide(row)
self.preview_widget.change_slide(row)
self.slideSelected()
def __checkUpdateSelectedSlide(self, row):
"""
Check if this slide has been updated
"""
if row + 1 < self.preview_list_widget.rowCount():
self.preview_list_widget.scrollToItem(self.preview_list_widget.item(row + 1, 0))
self.preview_list_widget.selectRow(row)
def onToggleLoop(self):
"""
Toggles the loop state.
@ -1151,7 +1087,7 @@ class SlideController(DisplayController):
"""
Start the timer loop running and store the timer id
"""
if self.preview_list_widget.rowCount() > 1:
if self.preview_widget.slide_count() > 1:
self.timer_id = self.startTimer(int(self.delay_spin_box.value()) * 1000)
def on_stop_loop(self):
@ -1261,8 +1197,8 @@ class SlideController(DisplayController):
"""
If preview copy slide item to live controller from Preview Controller
"""
row = self.preview_list_widget.currentRow()
if -1 < row < self.preview_list_widget.rowCount():
row = self.preview_widget.current_slide_number()
if -1 < row < self.preview_widget.slide_count():
if self.service_item.from_service:
self.service_manager.preview_live(self.service_item.unique_identifier, row)
else:
@ -1276,7 +1212,7 @@ class SlideController(DisplayController):
self.media_controller.video(self.controller_type, item, self.hide_mode())
if not self.is_live:
self.preview_display.show()
self.slidePreview.hide()
self.slide_preview.hide()
def onMediaClose(self):
"""
@ -1285,7 +1221,7 @@ class SlideController(DisplayController):
log.debug(u'SlideController onMediaClose')
self.media_controller.media_reset(self)
self.preview_display.hide()
self.slidePreview.show()
self.slide_preview.show()
def _resetBlank(self):
"""

View File

@ -75,13 +75,30 @@ class OpenLPWizard(QtGui.QWizard):
"""
Generic OpenLP wizard to provide generic functionality and a unified look
and feel.
``parent``
The QWidget-derived parent of the wizard.
``plugin``
Plugin this wizard is part of. The plugin will be saved in the "plugin" variable.
The plugin will also be used as basis for the file dialog methods this class provides.
``name``
The object name this wizard should have.
``image``
The image to display on the "welcome" page of the wizard. Should be 163x350.
``add_progress_page``
Whether to add a progress page with a progressbar at the end of the wizard.
"""
def __init__(self, parent, plugin, name, image):
def __init__(self, parent, plugin, name, image, add_progress_page=True):
"""
Constructor
"""
QtGui.QWizard.__init__(self, parent)
self.plugin = plugin
self.with_progress_page = add_progress_page
self.setObjectName(name)
self.open_icon = build_icon(u':/general/general_open.png')
self.delete_icon = build_icon(u':/general/general_delete.png')
@ -92,8 +109,9 @@ class OpenLPWizard(QtGui.QWizard):
self.custom_init()
self.custom_signals()
self.currentIdChanged.connect(self.on_current_id_changed)
self.error_copy_to_button.clicked.connect(self.on_error_copy_to_button_clicked)
self.error_save_to_button.clicked.connect(self.on_error_save_to_button_clicked)
if self.with_progress_page:
self.error_copy_to_button.clicked.connect(self.on_error_copy_to_button_clicked)
self.error_save_to_button.clicked.connect(self.on_error_save_to_button_clicked)
def setupUi(self, image):
"""
@ -105,7 +123,8 @@ class OpenLPWizard(QtGui.QWizard):
QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage)
add_welcome_page(self, image)
self.add_custom_pages()
self.add_progress_page()
if self.with_progress_page:
self.add_progress_page()
self.retranslateUi()
def register_fields(self):
@ -185,7 +204,7 @@ class OpenLPWizard(QtGui.QWizard):
Stop the wizard on cancel button, close button or ESC key.
"""
log.debug(u'Wizard cancelled by user.')
if self.currentPage() == self.progress_page:
if self.with_progress_page and self.currentPage() == self.progress_page:
Registry().execute(u'openlp_stop_wizard')
self.done(QtGui.QDialog.Rejected)
@ -193,14 +212,14 @@ class OpenLPWizard(QtGui.QWizard):
"""
Perform necessary functions depending on which wizard page is active.
"""
if self.page(pageId) == self.progress_page:
if self.with_progress_page and self.page(pageId) == self.progress_page:
self.pre_wizard()
self.performWizard()
self.post_wizard()
else:
self.custom_cage_changed(pageId)
self.custom_page_changed(pageId)
def custom_cage_changed(self, pageId):
def custom_page_changed(self, pageId):
"""
Called when changing to a page other than the progress page
"""

View File

@ -101,46 +101,38 @@ def get_application_version():
if APPLICATION_VERSION:
return APPLICATION_VERSION
if u'--dev-version' in sys.argv or u'-d' in sys.argv:
# If we're running the dev version, let's use bzr to get the version.
try:
# If bzrlib is available, use it.
from bzrlib.branch import Branch
b = Branch.open_containing('.')[0]
b.lock_read()
try:
# Get the branch's latest revision number.
revno = b.revno()
# Convert said revision number into a bzr revision id.
revision_id = b.dotted_revno_to_revision_id((revno,))
# Get a dict of tags, with the revision id as the key.
tags = b.tags.get_reverse_tag_dict()
# Check if the latest
if revision_id in tags:
full_version = u'%s' % tags[revision_id][0]
else:
full_version = '%s-bzr%s' % (sorted(b.tags.get_tag_dict().keys())[-1], revno)
finally:
b.unlock()
except:
# Otherwise run the command line bzr client.
bzr = Popen((u'bzr', u'tags', u'--sort', u'time'), stdout=PIPE)
output, error = bzr.communicate()
code = bzr.wait()
if code != 0:
raise Exception(u'Error running bzr tags')
lines = output.splitlines()
if not lines:
tag = u'0.0.0'
revision = u'0'
else:
tag, revision = lines[-1].split()
bzr = Popen((u'bzr', u'log', u'--line', u'-r', u'-1'), stdout=PIPE)
output, error = bzr.communicate()
code = bzr.wait()
if code != 0:
raise Exception(u'Error running bzr log')
latest = output.split(u':')[0]
full_version = latest == revision and tag or u'%s-bzr%s' % (tag, latest)
# NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
# there.
# Get the revision of this tree.
bzr = Popen((u'bzr', u'revno'), stdout=PIPE)
tree_revision, error = bzr.communicate()
code = bzr.wait()
if code != 0:
raise Exception(u'Error running bzr log')
# Get all tags.
bzr = Popen((u'bzr', u'tags'), stdout=PIPE)
output, error = bzr.communicate()
code = bzr.wait()
if code != 0:
raise Exception(u'Error running bzr tags')
tags = output.splitlines()
if not tags:
tag_version = u'0.0.0'
tag_revision = u'0'
else:
# Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
# another series.
tags = [tag for tag in tags if tag.split()[-1].strip() != u'?']
# Get the last tag and split it in a revision and tag name.
tag_version, tag_revision = tags[-1].split()
# If they are equal, then this tree is tarball with the source for the release. We do not want the revision
# number in the full version.
if tree_revision == tag_revision:
full_version = tag_version
else:
full_version = u'%s-bzr%s' % (tag_version, tree_revision)
else:
# We're not running the development version, let's use the file.
filepath = AppLocation.get_directory(AppLocation.VersionDir)

View File

@ -0,0 +1,39 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2013 Raoul Snyman *
* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan *
* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, *
* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. *
* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, *
* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, *
* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, *
* Frode Woldsund, Martin Zibricky *
* --------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
body {
background-color: black;
font-family: sans-serif;
overflow: hidden;
}
.size {
position: absolute;
top: 0px;
vertical-align: middle;
height: 100%;
background-size: cover;
background-repeat: no-repeat;
}

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html>
<!--
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
-->
<head>
<meta charset="utf-8" />
<title>${live_title}</title>
<link rel="stylesheet" href="/files/live.css" />
<link rel="shortcut icon" type="image/x-icon" href="/files/images/favicon.ico">
<script type="text/javascript" src="/files/jquery.js"></script>
<script type="text/javascript" src="/files/live.js"></script>
</head>
<body>
<img id="image" class="size"/>
</body>
</html>

View File

@ -0,0 +1,52 @@
/******************************************************************************
* OpenLP - Open Source Lyrics Projection *
* --------------------------------------------------------------------------- *
* Copyright (c) 2008-2013 Raoul Snyman *
* Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan *
* Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, *
* Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. *
* Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, *
* Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, *
* Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, *
* Frode Woldsund, Martin Zibricky *
* --------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the Free *
* Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or *
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for *
* more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., 59 *
* Temple Place, Suite 330, Boston, MA 02111-1307 USA *
******************************************************************************/
window.OpenLP = {
loadSlide: function (event) {
$.getJSON(
"/live/image",
function (data, status) {
var img = document.getElementById('image');
img.src = data.results.slide_image;
img.style.display = 'block';
}
);
},
pollServer: function () {
$.getJSON(
"/live/poll",
function (data, status) {
if (OpenLP.slideCount != data.results.slide_count) {
OpenLP.slideCount = data.results.slide_count;
OpenLP.loadSlide();
}
}
);
}
}
$.ajaxSetup({ cache: false });
setInterval("OpenLP.pollServer();", 500);
OpenLP.pollServer();

View File

@ -124,7 +124,7 @@ import cherrypy
from mako.template import Template
from PyQt4 import QtCore
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent
from openlp.core.lib import Registry, Settings, PluginStatus, StringContent, image_to_byte
from openlp.core.utils import AppLocation, translate
from cherrypy._cpcompat import sha, ntob
@ -136,6 +136,7 @@ def make_sha_hash(password):
"""
Create an encrypted password for the given password.
"""
log.debug("make_sha_hash")
return sha(ntob(password)).hexdigest()
@ -143,6 +144,7 @@ def fetch_password(username):
"""
Fetch the password for a provided user.
"""
log.debug("Fetch Password")
if username != Settings().value(u'remotes/user id'):
return None
return make_sha_hash(Settings().value(u'remotes/password'))
@ -175,9 +177,11 @@ class HttpServer(object):
self.root = self.Public()
self.root.files = self.Files()
self.root.stage = self.Stage()
self.root.live = self.Live()
self.root.router = self.router
self.root.files.router = self.router
self.root.stage.router = self.router
self.root.live.router = self.router
cherrypy.tree.mount(self.root, '/', config=self.define_config())
# Turn off the flood of access messages cause by poll
cherrypy.log.access_log.propagate = False
@ -212,6 +216,9 @@ class HttpServer(object):
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False},
u'/stage': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False},
u'/live': {u'tools.staticdir.on': True,
u'tools.staticdir.dir': self.router.html_dir,
u'tools.basic_auth.on': False}}
return directory_config
@ -239,7 +246,16 @@ class HttpServer(object):
class Stage(object):
"""
Stageview is read only so security is not relevant and would reduce it's usability
Stage view is read only so security is not relevant and would reduce it's usability
"""
@cherrypy.expose
def default(self, *args, **kwargs):
url = urlparse.urlparse(cherrypy.url())
return self.router.process_http_request(url.path, *args)
class Live(object):
"""
Live view is read only so security is not relevant and would reduce it's usability
"""
@cherrypy.expose
def default(self, *args, **kwargs):
@ -265,9 +281,12 @@ class HttpRouter(object):
self.routes = [
(u'^/$', self.serve_file),
(u'^/(stage)$', self.serve_file),
(u'^/(live)$', self.serve_file),
(r'^/files/(.*)$', self.serve_file),
(r'^/api/poll$', self.poll),
(r'^/stage/poll$', self.poll),
(r'^/live/poll$', self.live_poll),
(r'^/live/image$', self.live_image),
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/stage/controller/(live|preview)/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service),
@ -305,6 +324,7 @@ class HttpRouter(object):
if response:
return response
else:
log.debug('Path not found %s', url_path)
return self._http_not_found()
def _get_service_items(self):
@ -334,6 +354,7 @@ class HttpRouter(object):
self.template_vars = {
'app_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Remote'),
'stage_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Stage View'),
'live_title': translate('RemotePlugin.Mobile', 'OpenLP 2.1 Live View'),
'service_manager': translate('RemotePlugin.Mobile', 'Service Manager'),
'slide_controller': translate('RemotePlugin.Mobile', 'Slide Controller'),
'alerts': translate('RemotePlugin.Mobile', 'Alerts'),
@ -359,18 +380,19 @@ class HttpRouter(object):
def serve_file(self, filename=None):
"""
Send a file to the socket. For now, just a subset of file types
and must be top level inside the html folder.
Send a file to the socket. For now, just a subset of file types and must be top level inside the html folder.
If subfolders requested return 404, easier for security for the present.
Ultimately for i18n, this could first look for xx/file.html before
falling back to file.html... where xx is the language, e.g. 'en'
Ultimately for i18n, this could first look for xx/file.html before falling back to file.html.
where xx is the language, e.g. 'en'
"""
log.debug(u'serve file request %s' % filename)
if not filename:
filename = u'index.html'
elif filename == u'stage':
filename = u'stage.html'
elif filename == u'live':
filename = u'live.html'
path = os.path.normpath(os.path.join(self.html_dir, filename))
if not path.startswith(self.html_dir):
return self._http_not_found()
@ -425,6 +447,26 @@ class HttpRouter(object):
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result})
def live_poll(self):
"""
Poll OpenLP to determine the current slide count.
"""
result = {
u'slide_count': self.live_controller.slide_count
}
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result})
def live_image(self):
"""
Return the latest display image as a byte stream.
"""
result = {
u'slide_image': u'data:image/png;base64,' + str(image_to_byte(self.live_controller.slide_image))
}
cherrypy.response.headers['Content-Type'] = u'application/json'
return json.dumps({u'results': result})
def display(self, action):
"""
Hide or show the display screen.

View File

@ -86,6 +86,12 @@ class RemoteTab(SettingsTab):
self.stage_url.setObjectName(u'stage_url')
self.stage_url.setOpenExternalLinks(True)
self.http_setting_layout.addRow(self.stage_url_label, self.stage_url)
self.live_url_label = QtGui.QLabel(self.http_settings_group_box)
self.live_url_label.setObjectName(u'live_url_label')
self.live_url = QtGui.QLabel(self.http_settings_group_box)
self.live_url.setObjectName(u'live_url')
self.live_url.setOpenExternalLinks(True)
self.http_setting_layout.addRow(self.live_url_label, self.live_url)
self.left_layout.addWidget(self.http_settings_group_box)
self.https_settings_group_box = QtGui.QGroupBox(self.left_column)
self.https_settings_group_box.setCheckable(True)
@ -116,6 +122,12 @@ class RemoteTab(SettingsTab):
self.stage_https_url.setObjectName(u'stage_https_url')
self.stage_https_url.setOpenExternalLinks(True)
self.https_settings_layout.addRow(self.stage_https_url_label, self.stage_https_url)
self.live_https_url_label = QtGui.QLabel(self.https_settings_group_box)
self.live_https_url_label.setObjectName(u'live_url_label')
self.live_https_url = QtGui.QLabel(self.https_settings_group_box)
self.live_https_url.setObjectName(u'live_https_url')
self.live_https_url.setOpenExternalLinks(True)
self.https_settings_layout.addRow(self.live_https_url_label, self.live_https_url)
self.left_layout.addWidget(self.https_settings_group_box)
self.user_login_group_box = QtGui.QGroupBox(self.left_column)
self.user_login_group_box.setCheckable(True)
@ -163,6 +175,7 @@ class RemoteTab(SettingsTab):
self.port_label.setText(translate('RemotePlugin.RemoteTab', 'Port number:'))
self.remote_url_label.setText(translate('RemotePlugin.RemoteTab', 'Remote URL:'))
self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:'))
self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:'))
self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format'))
self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App'))
self.qr_description_label.setText(translate('RemotePlugin.RemoteTab',
@ -176,6 +189,7 @@ class RemoteTab(SettingsTab):
self.https_port_label.setText(self.port_label.text())
self.remote_https_url_label.setText(self.remote_url_label.text())
self.stage_https_url_label.setText(self.stage_url_label.text())
self.live_https_url_label.setText(self.live_url_label.text())
self.user_login_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'User Authentication'))
self.user_id_label.setText(translate('RemotePlugin.RemoteTab', 'User id:'))
self.password_label.setText(translate('RemotePlugin.RemoteTab', 'Password:'))
@ -203,10 +217,14 @@ class RemoteTab(SettingsTab):
https_url = u'https://%s:%s/' % (ip_address, self.https_port_spin_box.value())
self.remote_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
self.remote_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
http_url += u'stage'
https_url += u'stage'
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url, http_url))
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url, https_url))
http_url_temp = http_url + u'stage'
https_url_temp = https_url + u'stage'
self.stage_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
self.stage_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
http_url_temp = http_url + u'live'
https_url_temp = https_url + u'live'
self.live_url.setText(u'<a href="%s">%s</a>' % (http_url_temp, http_url_temp))
self.live_https_url.setText(u'<a href="%s">%s</a>' % (https_url_temp, https_url_temp))
def load(self):
"""

View File

@ -0,0 +1,358 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The duplicate song removal logic for OpenLP.
"""
from __future__ import division
import logging
import os
from PyQt4 import QtCore, QtGui
from openlp.core.lib import Registry, translate
from openlp.core.ui.wizard import OpenLPWizard, WizardStrings
from openlp.core.utils import AppLocation
from openlp.plugins.songs.lib import delete_song
from openlp.plugins.songs.lib.db import Song, MediaFile
from openlp.plugins.songs.forms.songreviewwidget import SongReviewWidget
from openlp.plugins.songs.lib.songcompare import songs_probably_equal
log = logging.getLogger(__name__)
class DuplicateSongRemovalForm(OpenLPWizard):
"""
This is the Duplicate Song Removal Wizard. It provides functionality to
search for and remove duplicate songs in the database.
"""
log.info(u'DuplicateSongRemovalForm loaded')
def __init__(self, plugin):
"""
Instantiate the wizard, and run any extra setup we need to.
``parent``
The QWidget-derived parent of the wizard.
``plugin``
The songs plugin.
"""
self.duplicate_song_list = []
self.review_current_count = 0
self.review_total_count = 0
# Used to interrupt ongoing searches when cancel is clicked.
self.break_search = False
OpenLPWizard.__init__(self, self.main_window, plugin, u'duplicateSongRemovalWizard',
u':/wizards/wizard_duplicateremoval.bmp', False)
self.setMinimumWidth(730)
def custom_signals(self):
"""
Song wizard specific signals.
"""
self.finish_button.clicked.connect(self.on_wizard_exit)
self.cancel_button.clicked.connect(self.on_wizard_exit)
def add_custom_pages(self):
"""
Add song wizard specific pages.
"""
# Add custom pages.
self.searching_page = QtGui.QWizardPage()
self.searching_page.setObjectName(u'searching_page')
self.searching_vertical_layout = QtGui.QVBoxLayout(self.searching_page)
self.searching_vertical_layout.setObjectName(u'searching_vertical_layout')
self.duplicate_search_progress_bar = QtGui.QProgressBar(self.searching_page)
self.duplicate_search_progress_bar.setObjectName(u'duplicate_search_progress_bar')
self.duplicate_search_progress_bar.setFormat(WizardStrings.PercentSymbolFormat)
self.searching_vertical_layout.addWidget(self.duplicate_search_progress_bar)
self.found_duplicates_edit = QtGui.QPlainTextEdit(self.searching_page)
self.found_duplicates_edit.setUndoRedoEnabled(False)
self.found_duplicates_edit.setReadOnly(True)
self.found_duplicates_edit.setObjectName(u'found_duplicates_edit')
self.searching_vertical_layout.addWidget(self.found_duplicates_edit)
self.searching_page_id = self.addPage(self.searching_page)
self.review_page = QtGui.QWizardPage()
self.review_page.setObjectName(u'review_page')
self.review_layout = QtGui.QVBoxLayout(self.review_page)
self.review_layout.setObjectName(u'review_layout')
self.review_scroll_area = QtGui.QScrollArea(self.review_page)
self.review_scroll_area.setObjectName(u'review_scroll_area')
self.review_scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.review_scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
self.review_scroll_area.setWidgetResizable(True)
self.review_scroll_area_widget = QtGui.QWidget(self.review_scroll_area)
self.review_scroll_area_widget.setObjectName(u'review_scroll_area_widget')
self.review_scroll_area_layout = QtGui.QHBoxLayout(self.review_scroll_area_widget)
self.review_scroll_area_layout.setObjectName(u'review_scroll_area_layout')
self.review_scroll_area_layout.setSizeConstraint(QtGui.QLayout.SetMinAndMaxSize)
self.review_scroll_area_layout.setMargin(0)
self.review_scroll_area_layout.setSpacing(0)
self.review_scroll_area.setWidget(self.review_scroll_area_widget)
self.review_layout.addWidget(self.review_scroll_area)
self.review_page_id = self.addPage(self.review_page)
# Add a dummy page to the end, to prevent the finish button to appear and the next button do disappear on the
#review page.
self.dummy_page = QtGui.QWizardPage()
self.dummy_page_id = self.addPage(self.dummy_page)
def retranslateUi(self):
"""
Song wizard localisation.
"""
self.setWindowTitle(translate(u'Wizard', u'Wizard'))
self.title_label.setText(WizardStrings.HeaderStyle % translate(u'OpenLP.Ui',
u'Welcome to the Duplicate Song Removal Wizard'))
self.information_label.setText(translate("Wizard",
u'This wizard will help you to remove duplicate songs from the song database. You will have a chance to '
u'review every potential duplicate song before it is deleted. So no songs will be deleted without your '
u'explicit approval.'))
self.searching_page.setTitle(translate(u'Wizard', u'Searching for duplicate songs.'))
self.searching_page.setSubTitle(translate(u'Wizard', u'Please wait while your songs database is analyzed.'))
self.update_review_counter_text()
self.review_page.setSubTitle(translate(u'Wizard',
u'Here you can decide which songs to remove and which ones to keep.'))
def update_review_counter_text(self):
"""
Set the wizard review page header text.
"""
self.review_page.setTitle(translate(u'Wizard', u'Review duplicate songs (%s/%s)') % \
(self.review_current_count, self.review_total_count))
def custom_page_changed(self, page_id):
"""
Called when changing the wizard page.
``page_id``
ID of the page the wizard changed to.
"""
# Hide back button.
self.button(QtGui.QWizard.BackButton).hide()
if page_id == self.searching_page_id:
self.application.set_busy_cursor()
try:
self.button(QtGui.QWizard.NextButton).hide()
# Search duplicate songs.
max_songs = self.plugin.manager.get_object_count(Song)
if max_songs == 0 or max_songs == 1:
self.duplicate_search_progress_bar.setMaximum(1)
self.duplicate_search_progress_bar.setValue(1)
self.notify_no_duplicates()
return
# With x songs we have x*(x - 1) / 2 comparisons.
max_progress_count = max_songs * (max_songs - 1) // 2
self.duplicate_search_progress_bar.setMaximum(max_progress_count)
songs = self.plugin.manager.get_all_objects(Song)
for outer_song_counter in range(max_songs - 1):
for inner_song_counter in range(outer_song_counter + 1, max_songs):
if songs_probably_equal(songs[outer_song_counter], songs[inner_song_counter]):
duplicate_added = self.add_duplicates_to_song_list(songs[outer_song_counter],
songs[inner_song_counter])
if duplicate_added:
self.found_duplicates_edit.appendPlainText(songs[outer_song_counter].title + " = " +
songs[inner_song_counter].title)
self.duplicate_search_progress_bar.setValue(self.duplicate_search_progress_bar.value() + 1)
# The call to process_events() will keep the GUI responsive.
self.application.process_events()
if self.break_search:
return
self.review_total_count = len(self.duplicate_song_list)
if self.review_total_count == 0:
self.notify_no_duplicates()
else:
self.button(QtGui.QWizard.NextButton).show()
finally:
self.application.set_normal_cursor()
elif page_id == self.review_page_id:
self.process_current_duplicate_entry()
def notify_no_duplicates(self):
"""
Notifies the user, that there were no duplicates found in the database.
"""
self.button(QtGui.QWizard.FinishButton).show()
self.button(QtGui.QWizard.FinishButton).setEnabled(True)
self.button(QtGui.QWizard.NextButton).hide()
self.button(QtGui.QWizard.CancelButton).hide()
QtGui.QMessageBox.information(self, translate(u'Wizard', u'Information'),
translate(u'Wizard', u'No duplicate songs have been found in the database.'),
QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok))
def add_duplicates_to_song_list(self, search_song, duplicate_song):
"""
Inserts a song duplicate (two similar songs) to the duplicate song list.
If one of the two songs is already part of the duplicate song list,
don't add another duplicate group but add the other song to that group.
Returns True if at least one of the songs was added, False if both were already
member of a group.
``search_song``
The song we searched the duplicate for.
``duplicate_song``
The duplicate song.
"""
duplicate_group_found = False
duplicate_added = False
for duplicate_group in self.duplicate_song_list:
# Skip the first song in the duplicate lists, since the first one has to be an earlier song.
if search_song in duplicate_group and not duplicate_song in duplicate_group:
duplicate_group.append(duplicate_song)
duplicate_group_found = True
duplicate_added = True
break
elif not search_song in duplicate_group and duplicate_song in duplicate_group:
duplicate_group.append(search_song)
duplicate_group_found = True
duplicate_added = True
break
elif search_song in duplicate_group and duplicate_song in duplicate_group:
duplicate_group_found = True
duplicate_added = False
break
if not duplicate_group_found:
self.duplicate_song_list.append([search_song, duplicate_song])
duplicate_added = True
return duplicate_added
def on_wizard_exit(self):
"""
Once the wizard is finished, refresh the song list,
since we potentially removed songs from it.
"""
self.break_search = True
self.plugin.media_item.on_search_text_button_clicked()
def setDefaults(self):
"""
Set default form values for the song import wizard.
"""
self.restart()
self.duplicate_search_progress_bar.setValue(0)
self.found_duplicates_edit.clear()
def validateCurrentPage(self):
"""
Controls whether we should switch to the next wizard page. This method loops
on the review page as long as there are more song duplicates to review.
"""
if self.currentId() == self.review_page_id:
# As long as it's not the last duplicate list entry we revisit the review page.
if len(self.duplicate_song_list) == 1:
return True
else:
self.proceed_to_next_review()
return False
return OpenLPWizard.validateCurrentPage(self)
def remove_button_clicked(self, song_review_widget):
"""
Removes a song from the database, removes the GUI element representing the
song on the review page, and disable the remove button if only one duplicate
is left.
``song_review_widget``
The SongReviewWidget whose song we should delete.
"""
# Remove song from duplicate song list.
self.duplicate_song_list[-1].remove(song_review_widget.song)
# Remove song from the database.
delete_song(song_review_widget.song.id, self.plugin)
# Remove GUI elements for the song.
self.review_scroll_area_layout.removeWidget(song_review_widget)
song_review_widget.setParent(None)
# Check if we only have one duplicate left:
# 2 stretches + 1 SongReviewWidget = 3
# The SongReviewWidget is then at position 1.
if len(self.duplicate_song_list[-1]) == 1:
self.review_scroll_area_layout.itemAt(1).widget().song_remove_button.setEnabled(False)
def proceed_to_next_review(self):
"""
Removes the previous review UI elements and calls process_current_duplicate_entry.
"""
# Remove last duplicate group.
self.duplicate_song_list.pop()
# Remove all previous elements.
for i in reversed(range(self.review_scroll_area_layout.count())):
item = self.review_scroll_area_layout.itemAt(i)
if isinstance(item, QtGui.QWidgetItem):
# The order is important here, if the .setParent(None) call is done
# before the .removeItem() call, a segfault occurs.
widget = item.widget()
self.review_scroll_area_layout.removeItem(item)
widget.setParent(None)
else:
self.review_scroll_area_layout.removeItem(item)
# Process next set of duplicates.
self.process_current_duplicate_entry()
def process_current_duplicate_entry(self):
"""
Update the review counter in the wizard header, add song widgets for
the current duplicate group to review, if it's the last
duplicate song group, hide the "next" button and show the "finish" button.
"""
# Update the counter.
self.review_current_count = self.review_total_count - (len(self.duplicate_song_list) - 1)
self.update_review_counter_text()
# Add song elements to the UI.
if len(self.duplicate_song_list) > 0:
self.review_scroll_area_layout.addStretch(1)
for duplicate in self.duplicate_song_list[-1]:
song_review_widget = SongReviewWidget(self.review_page, duplicate)
song_review_widget.song_remove_button_clicked.connect(self.remove_button_clicked)
self.review_scroll_area_layout.addWidget(song_review_widget)
self.review_scroll_area_layout.addStretch(1)
# Change next button to finish button on last review.
if len(self.duplicate_song_list) == 1:
self.button(QtGui.QWizard.FinishButton).show()
self.button(QtGui.QWizard.FinishButton).setEnabled(True)
self.button(QtGui.QWizard.NextButton).hide()
self.button(QtGui.QWizard.CancelButton).hide()
def _get_main_window(self):
"""
Adds the main window to the class dynamically.
"""
if not hasattr(self, u'_main_window'):
self._main_window = Registry().get(u'main_window')
return self._main_window
main_window = property(_get_main_window)
def _get_application(self):
"""
Adds the openlp to the class dynamically
"""
if not hasattr(self, u'_application'):
self._application = Registry().get(u'application')
return self._application
application = property(_get_application)

View File

@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
A widget representing a song in the duplicate song removal wizard review page.
"""
from PyQt4 import QtCore, QtGui
from openlp.core.lib import build_icon
from openlp.plugins.songs.lib import VerseType
from openlp.plugins.songs.lib.xml import SongXML
class SongReviewWidget(QtGui.QWidget):
"""
A widget representing a song on the duplicate song review page.
It displays most of the information a song contains and
provides a "remove" button to remove the song from the database.
The remove logic is not implemented here, but a signal is provided
when the remove button is clicked.
"""
# Signals have to be class variables and not instance variables. Otherwise
# they are not registered by Qt (missing emit and connect methods are artifacts of this).
# To use SongReviewWidget as a signal parameter one would have to assigning the class
# variable after the class is declared. While this is possible, it also messes Qts meta
# object system up. The result is an
# "Object::connect: Use the SIGNAL macro to bind SongReviewWidget::(QWidget*)" error on
# connect calls.
# That's why we cheat a little and use QWidget instead of SongReviewWidget as parameter.
# While not being entirely correct, it does work.
song_remove_button_clicked = QtCore.pyqtSignal(QtGui.QWidget)
def __init__(self, parent, song):
"""
``parent``
The QWidget-derived parent of the wizard.
``song``
The Song which this SongReviewWidget should represent.
"""
QtGui.QWidget.__init__(self, parent)
self.song = song
self.setupUi()
self.retranslateUi()
self.song_remove_button.clicked.connect(self.on_remove_button_clicked)
def setupUi(self):
self.song_vertical_layout = QtGui.QVBoxLayout(self)
self.song_vertical_layout.setObjectName(u'song_vertical_layout')
self.song_group_box = QtGui.QGroupBox(self)
self.song_group_box.setObjectName(u'song_group_box')
self.song_group_box.setFixedWidth(300)
self.song_group_box_layout = QtGui.QVBoxLayout(self.song_group_box)
self.song_group_box_layout.setObjectName(u'song_group_box_layout')
self.song_info_form_layout = QtGui.QFormLayout()
self.song_info_form_layout.setObjectName(u'song_info_form_layout')
# Add title widget.
self.song_title_label = QtGui.QLabel(self)
self.song_title_label.setObjectName(u'song_title_label')
self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.LabelRole, self.song_title_label)
self.song_title_content = QtGui.QLabel(self)
self.song_title_content.setObjectName(u'song_title_content')
self.song_title_content.setText(self.song.title)
self.song_title_content.setWordWrap(True)
self.song_info_form_layout.setWidget(0, QtGui.QFormLayout.FieldRole, self.song_title_content)
# Add alternate title widget.
self.song_alternate_title_label = QtGui.QLabel(self)
self.song_alternate_title_label.setObjectName(u'song_alternate_title_label')
self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.LabelRole, self.song_alternate_title_label)
self.song_alternate_title_content = QtGui.QLabel(self)
self.song_alternate_title_content.setObjectName(u'song_alternate_title_content')
self.song_alternate_title_content.setText(self.song.alternate_title)
self.song_alternate_title_content.setWordWrap(True)
self.song_info_form_layout.setWidget(1, QtGui.QFormLayout.FieldRole, self.song_alternate_title_content)
# Add CCLI number widget.
self.song_ccli_number_label = QtGui.QLabel(self)
self.song_ccli_number_label.setObjectName(u'song_ccli_number_label')
self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.LabelRole, self.song_ccli_number_label)
self.song_ccli_number_content = QtGui.QLabel(self)
self.song_ccli_number_content.setObjectName(u'song_ccli_number_content')
self.song_ccli_number_content.setText(self.song.ccli_number)
self.song_ccli_number_content.setWordWrap(True)
self.song_info_form_layout.setWidget(2, QtGui.QFormLayout.FieldRole, self.song_ccli_number_content)
# Add copyright widget.
self.song_copyright_label = QtGui.QLabel(self)
self.song_copyright_label.setObjectName(u'song_copyright_label')
self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.LabelRole, self.song_copyright_label)
self.song_copyright_content = QtGui.QLabel(self)
self.song_copyright_content.setObjectName(u'song_copyright_content')
self.song_copyright_content.setWordWrap(True)
self.song_copyright_content.setText(self.song.copyright)
self.song_info_form_layout.setWidget(3, QtGui.QFormLayout.FieldRole, self.song_copyright_content)
# Add comments widget.
self.song_comments_label = QtGui.QLabel(self)
self.song_comments_label.setObjectName(u'song_comments_label')
self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.LabelRole, self.song_comments_label)
self.song_comments_content = QtGui.QLabel(self)
self.song_comments_content.setObjectName(u'song_comments_content')
self.song_comments_content.setText(self.song.comments)
self.song_comments_content.setWordWrap(True)
self.song_info_form_layout.setWidget(4, QtGui.QFormLayout.FieldRole, self.song_comments_content)
# Add authors widget.
self.song_authors_label = QtGui.QLabel(self)
self.song_authors_label.setObjectName(u'song_authors_label')
self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.LabelRole, self.song_authors_label)
self.song_authors_content = QtGui.QLabel(self)
self.song_authors_content.setObjectName(u'song_authors_content')
self.song_authors_content.setWordWrap(True)
authors_text = u', '.join([author.display_name for author in self.song.authors])
self.song_authors_content.setText(authors_text)
self.song_info_form_layout.setWidget(5, QtGui.QFormLayout.FieldRole, self.song_authors_content)
# Add verse order widget.
self.song_verse_order_label = QtGui.QLabel(self)
self.song_verse_order_label.setObjectName(u'song_verse_order_label')
self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.LabelRole, self.song_verse_order_label)
self.song_verse_order_content = QtGui.QLabel(self)
self.song_verse_order_content.setObjectName(u'song_verse_order_content')
self.song_verse_order_content.setText(self.song.verse_order)
self.song_verse_order_content.setWordWrap(True)
self.song_info_form_layout.setWidget(6, QtGui.QFormLayout.FieldRole, self.song_verse_order_content)
self.song_group_box_layout.addLayout(self.song_info_form_layout)
# Add verses widget.
self.song_info_verse_list_widget = QtGui.QTableWidget(self.song_group_box)
self.song_info_verse_list_widget.setColumnCount(1)
self.song_info_verse_list_widget.horizontalHeader().setVisible(False)
self.song_info_verse_list_widget.setObjectName(u'song_info_verse_list_widget')
self.song_info_verse_list_widget.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
self.song_info_verse_list_widget.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.song_info_verse_list_widget.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.song_info_verse_list_widget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
self.song_info_verse_list_widget.setAlternatingRowColors(True)
song_xml = SongXML()
verses = song_xml.get_verses(self.song.lyrics)
self.song_info_verse_list_widget.setRowCount(len(verses))
song_tags = []
for verse_number, verse in enumerate(verses):
item = QtGui.QTableWidgetItem()
item.setText(verse[1])
self.song_info_verse_list_widget.setItem(verse_number, 0, item)
# We cannot use from_loose_input() here, because database
# is supposed to contain English lowercase singlechar tags.
verse_tag = verse[0][u'type']
verse_index = None
if len(verse_tag) > 1:
verse_index = VerseType.from_translated_string(verse_tag)
if verse_index is None:
verse_index = VerseType.from_string(verse_tag, None)
if verse_index is None:
verse_index = VerseType.from_tag(verse_tag)
verse_tag = VerseType.translated_tags[verse_index].upper()
song_tags.append(unicode(verse_tag + verse[0]['label']))
self.song_info_verse_list_widget.setVerticalHeaderLabels(song_tags)
# Resize table fields to content and table to columns
self.song_info_verse_list_widget.setColumnWidth(0, self.song_group_box.width())
self.song_info_verse_list_widget.resizeRowsToContents()
# The 6 is a trial and error value since verticalHeader().length() + offset() is a little bit to small.
# It seems there is no clean way to determine the real height of the table contents.
# The "correct" value slightly fluctuates depending on the theme used, in the worst case
# Some pixels are missing at the bottom of the table, but all themes I tried still allowed
# to read the last verse line, so I'll just leave it at that.
self.song_info_verse_list_widget.setFixedHeight(self.song_info_verse_list_widget.verticalHeader().length() +
self.song_info_verse_list_widget.verticalHeader().offset() + 6)
self.song_group_box_layout.addWidget(self.song_info_verse_list_widget)
self.song_group_box_layout.addStretch()
self.song_vertical_layout.addWidget(self.song_group_box)
self.song_remove_button = QtGui.QPushButton(self)
self.song_remove_button.setObjectName(u'song_remove_button')
self.song_remove_button.setIcon(build_icon(u':/songs/song_delete.png'))
self.song_remove_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed)
self.song_vertical_layout.addWidget(self.song_remove_button, alignment = QtCore.Qt.AlignHCenter)
def retranslateUi(self):
self.song_remove_button.setText(u'Remove')
self.song_title_label.setText(u'Title:')
self.song_alternate_title_label.setText(u'Alternate Title:')
self.song_ccli_number_label.setText(u'CCLI Number:')
self.song_verse_order_label.setText(u'Verse Order:')
self.song_copyright_label.setText(u'Copyright:')
self.song_comments_label.setText(u'Comments:')
self.song_authors_label.setText(u'Authors:')
def on_remove_button_clicked(self):
"""
Signal emitted when the "remove" button is clicked.
"""
self.song_remove_button_clicked.emit(self)

View File

@ -29,15 +29,21 @@
"""
The :mod:`~openlp.plugins.songs.lib` module contains a number of library functions and classes used in the Songs plugin.
"""
import logging
import os
import re
from PyQt4 import QtGui
from openlp.core.lib import translate
from openlp.core.utils import CONTROL_CHARS
from openlp.core.utils import AppLocation, CONTROL_CHARS
from openlp.plugins.songs.lib.db import MediaFile, Song
from db import Author
from ui import SongStrings
log = logging.getLogger(__name__)
WHITESPACE = re.compile(r'[\W_]+', re.UNICODE)
APOSTROPHE = re.compile(u'[\'`ʻ]', re.UNICODE)
PATTERN = re.compile(r"\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-f]{2})|\\([^a-z])|([{}])|[\r\n]+|(.)", re.I)
@ -593,3 +599,29 @@ def strip_rtf(text, default_encoding=None):
text = u''.join(out)
return text, default_encoding
def delete_song(song_id, song_plugin):
"""
Deletes a song from the database. Media files associated to the song
are removed prior to the deletion of the song.
``song_id``
The ID of the song to delete.
``song_plugin``
The song plugin instance.
"""
media_files = song_plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == song_id)
for media_file in media_files:
try:
os.remove(media_file.file_name)
except:
log.exception('Could not remove file: %s', media_file.file_name)
try:
save_path = os.path.join(AppLocation.get_section_data_path(song_plugin.name), 'audio', str(song_id))
if os.path.exists(save_path):
os.rmdir(save_path)
except OSError:
log.exception(u'Could not remove directory: %s', save_path)
song_plugin.manager.delete_object(Song, song_id)

View File

@ -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
from openlp.plugins.songs.lib import VerseType, clean_string, delete_song
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
@ -368,19 +368,7 @@ class SongMediaItem(MediaManagerItem):
self.main_window.display_progress_bar(len(items))
for item in items:
item_id = item.data(QtCore.Qt.UserRole)
media_files = self.plugin.manager.get_all_objects(MediaFile, MediaFile.song_id == item_id)
for media_file in media_files:
try:
os.remove(media_file.file_name)
except:
log.exception('Could not remove file: %s', media_file.file_name)
try:
save_path = os.path.join(AppLocation.get_section_data_path(self.plugin.name), 'audio', str(item_id))
if os.path.exists(save_path):
os.rmdir(save_path)
except OSError:
log.exception(u'Could not remove directory: %s', save_path)
self.plugin.manager.delete_object(Song, item_id)
delete_song(item_id, self.plugin)
self.main_window.increment_progress_bar()
self.main_window.finished_progress_bar()
self.application.set_normal_cursor()

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`songcompare` module provides functionality to search for
duplicate songs. It has one single :function:`songs_probably_equal`.
The algorithm is based on the diff algorithm.
First a diffset is calculated for two songs.
To compensate for typos all differences that are smaller than a
limit (<max_typo_size) and are surrounded by larger equal blocks
(>min_fragment_size) are removed and the surrounding equal parts are merged.
Finally two conditions can qualify a song tuple to be a duplicate:
1. There is a block of equal content that is at least min_block_size large.
This condition should hit for all larger songs that have a long enough
equal part. Even if only one verse is equal this condition should still hit.
2. Two thirds of the smaller song is contained in the larger song.
This condition should hit if one of the two songs (or both) is small (smaller
than the min_block_size), but most of the song is contained in the other song.
"""
from __future__ import division
import difflib
MIN_FRAGMENT_SIZE = 5
MIN_BLOCK_SIZE = 70
MAX_TYPO_SIZE = 3
def songs_probably_equal(song1, song2):
"""
Calculate and return whether two songs are probably equal.
``song1``
The first song to compare.
``song2``
The second song to compare.
"""
if len(song1.search_lyrics) < len(song2.search_lyrics):
small = song1.search_lyrics
large = song2.search_lyrics
else:
small = song2.search_lyrics
large = song1.search_lyrics
differ = difflib.SequenceMatcher(a=large, b=small)
diff_tuples = differ.get_opcodes()
diff_no_typos = _remove_typos(diff_tuples)
# Check 1: Similarity based on the absolute length of equal parts.
# Calculate the total length of all equal blocks of the set.
# Blocks smaller than min_block_size are not counted.
length_of_equal_blocks = 0
for element in diff_no_typos:
if element[0] == "equal" and _op_length(element) >= MIN_BLOCK_SIZE:
length_of_equal_blocks += _op_length(element)
if length_of_equal_blocks >= MIN_BLOCK_SIZE:
return True
# Check 2: Similarity based on the relative length of the longest equal block.
# Calculate the length of the largest equal block of the diff set.
length_of_longest_equal_block = 0
for element in diff_no_typos:
if element[0] == "equal" and _op_length(element) > length_of_longest_equal_block:
length_of_longest_equal_block = _op_length(element)
if length_of_equal_blocks >= MIN_BLOCK_SIZE or length_of_longest_equal_block > len(small) * 2 // 3:
return True
# Both checks failed. We assume the songs are not equal.
return False
def _op_length(opcode):
"""
Return the length of a given difference.
``opcode``
The difference.
"""
return max(opcode[2] - opcode[1], opcode[4] - opcode[3])
def _remove_typos(diff):
"""
Remove typos from a diff set. A typo is a small difference (<max_typo_size)
surrounded by larger equal passages (>min_fragment_size).
``diff``
The diff set to remove the typos from.
"""
# Remove typo at beginning of the string.
if len(diff) >= 2:
if diff[0][0] != "equal" and _op_length(diff[0]) <= MAX_TYPO_SIZE and \
_op_length(diff[1]) >= MIN_FRAGMENT_SIZE:
del diff[0]
# Remove typos in the middle of the string.
if len(diff) >= 3:
for index in range(len(diff) - 3, -1, -1):
if _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \
diff[index + 1][0] != "equal" and _op_length(diff[index + 1]) <= MAX_TYPO_SIZE and \
_op_length(diff[index + 2]) >= MIN_FRAGMENT_SIZE:
del diff[index + 1]
# Remove typo at the end of the string.
if len(diff) >= 2:
if _op_length(diff[-2]) >= MIN_FRAGMENT_SIZE and \
diff[-1][0] != "equal" and _op_length(diff[-1]) <= MAX_TYPO_SIZE:
del diff[-1]
# Merge the bordering equal passages that occured by removing differences.
for index in range(len(diff) - 2, -1, -1):
if diff[index][0] == "equal" and _op_length(diff[index]) >= MIN_FRAGMENT_SIZE and \
diff[index + 1][0] == "equal" and _op_length(diff[index + 1]) >= MIN_FRAGMENT_SIZE:
diff[index] = ("equal", diff[index][1], diff[index + 1][2], diff[index][3],
diff[index + 1][4])
del diff[index + 1]
return diff

View File

@ -50,6 +50,8 @@ from openlp.plugins.songs.lib.importer import SongFormat
from openlp.plugins.songs.lib.olpimport import OpenLPSongImport
from openlp.plugins.songs.lib.mediaitem import SongMediaItem
from openlp.plugins.songs.lib.songstab import SongsTab
from openlp.plugins.songs.forms.duplicatesongremovalform import DuplicateSongRemovalForm
log = logging.getLogger(__name__)
__default_settings__ = {
@ -97,10 +99,12 @@ class SongsPlugin(Plugin):
self.song_import_item.setVisible(True)
self.song_export_item.setVisible(True)
self.tools_reindex_item.setVisible(True)
self.tools_find_duplicates.setVisible(True)
action_list = ActionList.get_instance()
action_list.add_action(self.song_import_item, UiStrings().Import)
action_list.add_action(self.song_export_item, UiStrings().Export)
action_list.add_action(self.tools_reindex_item, UiStrings().Tools)
action_list.add_action(self.tools_find_duplicates, UiStrings().Tools)
def add_import_menu_item(self, import_menu):
"""
@ -136,7 +140,7 @@ class SongsPlugin(Plugin):
def add_tools_menu_item(self, tools_menu):
"""
Give the alerts plugin the opportunity to add items to the
Give the Songs plugin the opportunity to add items to the
**Tools** menu.
``tools_menu``
@ -150,6 +154,12 @@ class SongsPlugin(Plugin):
statustip=translate('SongsPlugin', 'Re-index the songs database to improve searching and ordering.'),
visible=False, triggers=self.on_tools_reindex_item_triggered)
tools_menu.addAction(self.tools_reindex_item)
self.tools_find_duplicates = create_action(tools_menu, u'toolsFindDuplicates',
text=translate('SongsPlugin', 'Find &Duplicate Songs'),
statustip=translate('SongsPlugin',
'Find and remove duplicate songs in the song database.'),
visible=False, triggers=self.on_tools_find_duplicates_triggered, can_shortcuts=True)
tools_menu.addAction(self.tools_find_duplicates)
def on_tools_reindex_item_triggered(self):
"""
@ -169,6 +179,12 @@ class SongsPlugin(Plugin):
self.manager.save_objects(songs)
self.media_item.on_search_text_button_clicked()
def on_tools_find_duplicates_triggered(self):
"""
Search for duplicates in the song database.
"""
DuplicateSongRemovalForm(self).exec_()
def on_song_import_item_clicked(self):
if self.media_item:
self.media_item.on_import_click()
@ -287,10 +303,12 @@ class SongsPlugin(Plugin):
self.song_import_item.setVisible(False)
self.song_export_item.setVisible(False)
self.tools_reindex_item.setVisible(False)
self.tools_find_duplicates.setVisible(False)
action_list = ActionList.get_instance()
action_list.remove_action(self.song_import_item, UiStrings().Import)
action_list.remove_action(self.song_export_item, UiStrings().Export)
action_list.remove_action(self.tools_reindex_item, UiStrings().Tools)
action_list.remove_action(self.tools_find_duplicates, UiStrings().Tools)
Plugin.finalise(self)
def new_service_created(self):

View File

@ -19,6 +19,7 @@
<file>topic_maintenance.png</file>
<file>song_author_edit.png</file>
<file>song_book_edit.png</file>
<file>song_delete.png</file>
</qresource>
<qresource prefix="images">
<file>image_group.png</file>
@ -101,6 +102,7 @@
<file>wizard_importbible.bmp</file>
<file>wizard_firsttime.bmp</file>
<file>wizard_createtheme.bmp</file>
<file>wizard_duplicateremoval.bmp</file>
</qresource>
<qresource prefix="services">
<file>service_collapse_all.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

View File

@ -27,12 +27,15 @@
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
from setuptools import setup, find_packages
import re
from setuptools import setup, find_packages
from subprocess import Popen, PIPE
VERSION_FILE = 'openlp/.version'
SPLIT_ALPHA_DIGITS = re.compile(r'(\d+|\D+)')
def try_int(s):
"""
Convert string s to an integer if possible. Fail silently and return
@ -46,6 +49,7 @@ def try_int(s):
except Exception:
return s
def natural_sort_key(s):
"""
Return a tuple by which s is sorted.
@ -55,6 +59,7 @@ def natural_sort_key(s):
"""
return map(try_int, SPLIT_ALPHA_DIGITS.findall(s))
def natural_compare(a, b):
"""
Compare two strings naturally and return the result.
@ -67,6 +72,7 @@ def natural_compare(a, b):
"""
return cmp(natural_sort_key(a), natural_sort_key(b))
def natural_sort(seq, compare=natural_compare):
"""
Returns a copy of seq, sorted by natural string sort.
@ -76,38 +82,50 @@ def natural_sort(seq, compare=natural_compare):
temp.sort(compare)
return temp
# NOTE: The following code is a duplicate of the code in openlp/core/utils/__init__.py. Any fix applied here should also
# be applied there.
try:
# Try to import Bazaar
from bzrlib.branch import Branch
b = Branch.open_containing('.')[0]
b.lock_read()
try:
# Get the branch's latest revision number.
revno = b.revno()
# Convert said revision number into a bzr revision id.
revision_id = b.dotted_revno_to_revision_id((revno,))
# Get a dict of tags, with the revision id as the key.
tags = b.tags.get_reverse_tag_dict()
# Check if the latest
if revision_id in tags:
version = u'%s' % tags[revision_id][0]
else:
version = '%s-bzr%s' % \
(natural_sort(b.tags.get_tag_dict().keys())[-1], revno)
ver_file = open(VERSION_FILE, u'w')
ver_file.write(version)
ver_file.close()
finally:
b.unlock()
# Get the revision of this tree.
bzr = Popen((u'bzr', u'revno'), stdout=PIPE)
tree_revision, error = bzr.communicate()
code = bzr.wait()
if code != 0:
raise Exception(u'Error running bzr log')
# Get all tags.
bzr = Popen((u'bzr', u'tags'), stdout=PIPE)
output, error = bzr.communicate()
code = bzr.wait()
if code != 0:
raise Exception(u'Error running bzr tags')
tags = output.splitlines()
if not tags:
tag_version = u'0.0.0'
tag_revision = u'0'
else:
# Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
# another series.
tags = [tag for tag in tags if tag.split()[-1].strip() != u'?']
# Get the last tag and split it in a revision and tag name.
tag_version, tag_revision = tags[-1].split()
# If they are equal, then this tree is tarball with the source for the release. We do not want the revision number
# in the version string.
if tree_revision == tag_revision:
version_string = tag_version
else:
version_string = u'%s-bzr%s' % (tag_version, tree_revision)
ver_file = open(VERSION_FILE, u'w')
ver_file.write(version_string)
except:
ver_file = open(VERSION_FILE, u'r')
version = ver_file.read().strip()
version_string = ver_file.read().strip()
finally:
ver_file.close()
setup(
name='OpenLP',
version=version,
version=version_string,
description="Open source Church presentation and lyrics projection application.",
long_description="""\
OpenLP (previously openlp.org) is free church presentation software, or lyrics projection software, used to display slides of songs, Bible verses, videos, images, and even presentations (if PowerPoint is installed) for church worship using a computer and a data projector.""",

0
tests/__init__.py Normal file
View File

View File

@ -2,11 +2,12 @@
Package to test the openlp.core.lib package.
"""
import os
import cPickle
from unittest import TestCase
from mock import MagicMock, patch
from openlp.core.lib import ItemCapabilities, ServiceItem, Registry
from tests.utils.osdinteraction import read_service_from_file
from tests.utils.constants import TEST_RESOURCES_PATH
VERSE = u'The Lord said to {r}Noah{/r}: \n'\
@ -18,8 +19,6 @@ VERSE = u'The Lord said to {r}Noah{/r}: \n'\
'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
FOOTER = [u'Arky Arky (Unknown)', u'Public Domain', u'CCLI 123456']
TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..', u'resources'))
class TestServiceItem(TestCase):
@ -78,7 +77,7 @@ class TestServiceItem(TestCase):
service_item.name = u'test'
# WHEN: adding image to a service item
test_image = os.path.join(TEST_PATH, u'church.jpg')
test_image = os.path.join(TEST_RESOURCES_PATH, u'church.jpg')
service_item.add_from_image(test_image, u'Image Title')
# THEN: We should get back a valid service item
@ -133,8 +132,8 @@ class TestServiceItem(TestCase):
service_item.name = u'test'
# WHEN: adding image to a service item
test_file = os.path.join(TEST_PATH, u'church.jpg')
service_item.add_from_command(TEST_PATH, u'church.jpg', test_file)
test_file = os.path.join(TEST_RESOURCES_PATH, u'church.jpg')
service_item.add_from_command(TEST_RESOURCES_PATH, u'church.jpg', test_file)
# THEN: We should get back a valid service item
assert service_item.is_valid is True, u'The new service item should be valid'
@ -151,7 +150,7 @@ class TestServiceItem(TestCase):
assert len(service) == 2, u'The saved service should have two parts'
assert service[u'header'][u'name'] == u'test', u'A test plugin should be returned'
assert service[u'data'][0][u'title'] == u'church.jpg', u'The first title name should be "church,jpg"'
assert service[u'data'][0][u'path'] == TEST_PATH, u'The path should match the input path'
assert service[u'data'][0][u'path'] == TEST_RESOURCES_PATH, u'The path should match the input path'
assert service[u'data'][0][u'image'] == test_file, u'The image should match the full path to image'
# WHEN validating a service item
@ -170,13 +169,12 @@ class TestServiceItem(TestCase):
"""
Test the Service Item - adding a custom slide from a saved service
"""
# GIVEN: A new service item and a mocked add icon function
# GIVEN: A new service item
service_item = ServiceItem(None)
service_item.add_icon = MagicMock()
# WHEN: adding a custom from a saved Service
line = self.convert_file_service_item(u'serviceitem_custom_1.osd')
service_item.set_from_service(line)
service = read_service_from_file(u'serviceitem_custom_1.osd')
service_item.set_from_service(service[0])
# THEN: We should get back a valid service item
assert service_item.is_valid is True, u'The new service item should be valid'
@ -195,18 +193,17 @@ class TestServiceItem(TestCase):
"""
Test the Service Item - adding an image from a saved service
"""
# GIVEN: A new service item and a mocked add icon function
# GIVEN: A new service item
image_name = u'image_1.jpg'
test_file = os.path.join(TEST_PATH, image_name)
test_file = os.path.join(TEST_RESOURCES_PATH, image_name)
frame_array = {u'path': test_file, u'title': image_name}
service_item = ServiceItem(None)
service_item.add_icon = MagicMock()
# WHEN: adding an image from a saved Service and mocked exists
line = self.convert_file_service_item(u'serviceitem_image_1.osd')
service = read_service_from_file(u'serviceitem_image_1.osd')
with patch('os.path.exists'):
service_item.set_from_service(line, TEST_PATH)
service_item.set_from_service(service[0], TEST_RESOURCES_PATH)
# THEN: We should get back a valid service item
assert service_item.is_valid is True, u'The new service item should be valid'
@ -229,7 +226,7 @@ class TestServiceItem(TestCase):
"""
Test the Service Item - adding an image from a saved local service
"""
# GIVEN: A new service item and a mocked add icon function
# GIVEN: A new service item
image_name1 = u'image_1.jpg'
image_name2 = u'image_2.jpg'
test_file1 = os.path.join(u'/home/openlp', image_name1)
@ -238,12 +235,11 @@ class TestServiceItem(TestCase):
frame_array2 = {u'path': test_file2, u'title': image_name2}
service_item = ServiceItem(None)
service_item.add_icon = MagicMock()
# WHEN: adding an image from a saved Service and mocked exists
line = self.convert_file_service_item(u'serviceitem_image_2.osd')
service = read_service_from_file(u'serviceitem_image_2.osd')
with patch('os.path.exists'):
service_item.set_from_service(line)
service_item.set_from_service(service[0])
# THEN: We should get back a valid service item
assert service_item.is_valid is True, u'The new service item should be valid'
@ -286,16 +282,3 @@ class TestServiceItem(TestCase):
assert service_item.title is None, u'The title should be set to a value'
assert service_item.is_capable(ItemCapabilities.HasDetailedTitleDisplay) is False, \
u'The Capability should have been removed'
def convert_file_service_item(self, name):
service_file = os.path.join(TEST_PATH, name)
try:
open_file = open(service_file, u'r')
items = cPickle.load(open_file)
first_line = items[0]
except IOError:
first_line = u''
finally:
open_file.close()
return first_line

View File

@ -4,15 +4,37 @@ This module contains tests for the lib submodule of the Songs plugin.
from unittest import TestCase
from mock import patch
from mock import patch, MagicMock
from openlp.plugins.songs.lib import VerseType, clean_string, clean_title
from openlp.plugins.songs.lib.songcompare import songs_probably_equal, _remove_typos, _op_length
class TestLib(TestCase):
"""
Test the functions in the :mod:`lib` module.
"""
def setUp(self):
"""
Mock up two songs and provide a set of lyrics for the songs_probably_equal tests.
"""
self.full_lyrics =u'''amazing grace how sweet the sound that saved a wretch like me i once was lost but now am
found was blind but now i see twas grace that taught my heart to fear and grace my fears relieved how
precious did that grace appear the hour i first believed through many dangers toils and snares i have already
come tis grace that brought me safe thus far and grace will lead me home'''
self.short_lyrics =u'''twas grace that taught my heart to fear and grace my fears relieved how precious did that
grace appear the hour i first believed'''
self.error_lyrics =u'''amazing how sweet the trumpet that saved a wrench like me i once was losst but now am
found waf blind but now i see it was grace that taught my heart to fear and grace my fears relieved how
precious did that grace appppppppear the hour i first believedxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx snares i have
already come to this grace that brought me safe so far and grace will lead me home'''
self.different_lyrics=u'''on a hill far away stood an old rugged cross the emblem of suffering and shame and i love
that old cross where the dearest and best for a world of lost sinners was slain so ill cherish the old rugged
cross till my trophies at last i lay down i will cling to the old rugged cross and exchange it some day for a
crown'''
self.song1 = MagicMock()
self.song2 = MagicMock()
def clean_string_test(self):
"""
Test the clean_string() function
@ -39,6 +61,160 @@ class TestLib(TestCase):
# THEN: The string should be cleaned up
self.assertEqual(result, u'This is a dirty string', u'The title should be cleaned up properly: "%s"' % result)
def songs_probably_equal_same_song_test(self):
"""
Test the songs_probably_equal function with twice the same song.
"""
# GIVEN: Two equal songs.
self.song1.search_lyrics = self.full_lyrics
self.song2.search_lyrics = self.full_lyrics
# WHEN: We compare those songs for equality.
result = songs_probably_equal(self.song1, self.song2)
# THEN: The result should be True.
assert result == True, u'The result should be True'
def songs_probably_equal_short_song_test(self):
"""
Test the songs_probably_equal function with a song and a shorter version of the same song.
"""
# GIVEN: A song and a short version of the same song.
self.song1.search_lyrics = self.full_lyrics
self.song2.search_lyrics = self.short_lyrics
# WHEN: We compare those songs for equality.
result = songs_probably_equal(self.song1, self.song2)
# THEN: The result should be True.
assert result == True, u'The result should be True'
def songs_probably_equal_error_song_test(self):
"""
Test the songs_probably_equal function with a song and a very erroneous version of the same song.
"""
# GIVEN: A song and the same song with lots of errors.
self.song1.search_lyrics = self.full_lyrics
self.song2.search_lyrics = self.error_lyrics
# WHEN: We compare those songs for equality.
result = songs_probably_equal(self.song1, self.song2)
# THEN: The result should be True.
assert result == True, u'The result should be True'
def songs_probably_equal_different_song_test(self):
"""
Test the songs_probably_equal function with two different songs.
"""
# GIVEN: Two different songs.
self.song1.search_lyrics = self.full_lyrics
self.song2.search_lyrics = self.different_lyrics
# WHEN: We compare those songs for equality.
result = songs_probably_equal(self.song1, self.song2)
# THEN: The result should be False.
assert result == False, u'The result should be False'
def remove_typos_beginning_test(self):
"""
Test the _remove_typos function with a typo at the beginning.
"""
# GIVEN: A diffset with a difference at the beginning.
diff = [('replace', 0, 2, 0, 1), ('equal', 2, 11, 1, 10)]
# WHEN: We remove the typos in there.
result = _remove_typos(diff)
# THEN: There should be no typos at the beginning anymore.
assert len(result) == 1, u'The result should contain only one element.'
assert result[0][0] == 'equal', u'The result should contain an equal element.'
def remove_typos_beginning_negated_test(self):
"""
Test the _remove_typos function with a large difference at the beginning.
"""
# GIVEN: A diffset with a large difference at the beginning.
diff = [('replace', 0, 20, 0, 1), ('equal', 20, 29, 1, 10)]
# WHEN: We remove the typos in there.
result = _remove_typos(list(diff))
# THEN: There diff should not have changed.
assert result == diff
def remove_typos_end_test(self):
"""
Test the _remove_typos function with a typo at the end.
"""
# GIVEN: A diffset with a difference at the end.
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11)]
# WHEN: We remove the typos in there.
result = _remove_typos(diff)
# THEN: There should be no typos at the end anymore.
assert len(result) == 1, u'The result should contain only one element.'
assert result[0][0] == 'equal', u'The result should contain an equal element.'
def remove_typos_end_negated_test(self):
"""
Test the _remove_typos function with a large difference at the end.
"""
# GIVEN: A diffset with a large difference at the end.
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 1)]
# WHEN: We remove the typos in there.
result = _remove_typos(list(diff))
# THEN: There diff should not have changed.
assert result == diff
def remove_typos_middle_test(self):
"""
Test the _remove_typos function with a typo in the middle.
"""
# GIVEN: A diffset with a difference in the middle.
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 12, 10, 11), ('equal', 12, 22, 11, 21)]
# WHEN: We remove the typos in there.
result = _remove_typos(diff)
# THEN: There should be no typos in the middle anymore. The remaining equals should have been merged.
assert len(result) is 1, u'The result should contain only one element.'
assert result[0][0] == 'equal', u'The result should contain an equal element.'
assert result[0][1] == 0, u'The start indices should be kept.'
assert result[0][2] == 22, u'The stop indices should be kept.'
assert result[0][3] == 0, u'The start indices should be kept.'
assert result[0][4] == 21, u'The stop indices should be kept.'
def remove_typos_beginning_negated_test(self):
"""
Test the _remove_typos function with a large difference in the middle.
"""
# GIVEN: A diffset with a large difference in the middle.
diff = [('equal', 0, 10, 0, 10), ('replace', 10, 20, 10, 11), ('equal', 20, 30, 11, 21)]
# WHEN: We remove the typos in there.
result = _remove_typos(list(diff))
# THEN: There diff should not have changed.
assert result == diff
def op_length_test(self):
"""
Test the _op_length function.
"""
# GIVEN: A diff entry.
diff_entry = ('replace', 0, 2, 4, 14)
# WHEN: We calculate the length of that diff.
result = _op_length(diff_entry)
# THEN: The maximum length should be returned.
assert result == 10, u'The length should be 10.'
class TestVerseType(TestCase):
"""

View File

@ -0,0 +1,88 @@
"""
Package to test the openlp.core.ui.listpreviewwidget.
"""
from unittest import TestCase
from mock import MagicMock, patch
from PyQt4 import QtGui
from openlp.core.lib import Registry, ServiceItem
from openlp.core.ui import listpreviewwidget
from tests.utils.osdinteraction import read_service_from_file
class TestListPreviewWidget(TestCase):
def setUp(self):
"""
Create the UI.
"""
Registry.create()
self.app = QtGui.QApplication([])
self.main_window = QtGui.QMainWindow()
self.image = QtGui.QImage(1, 1, QtGui.QImage.Format_RGB32)
self.image_manager = MagicMock()
self.image_manager.get_image.return_value = self.image
Registry().register(u'image_manager', self.image_manager)
self.preview_widget = listpreviewwidget.ListPreviewWidget(self.main_window, 2)
def tearDown(self):
"""
Delete all the C++ objects at the end so that we don't have a segfault.
"""
del self.preview_widget
del self.main_window
del self.app
def initial_slide_count_test(self):
"""
Test the inital slide count.
"""
# GIVEN: A new ListPreviewWidget instance.
# WHEN: No SlideItem has been added yet.
# THEN: The count of items should be zero.
self.assertEqual(self.preview_widget.slide_count(), 0,
u'The slide list should be empty.')
def initial_slide_number_test(self):
"""
Test the inital slide number.
"""
# GIVEN: A new ListPreviewWidget instance.
# WHEN: No SlideItem has been added yet.
# THEN: The number of the current item should be -1.
self.assertEqual(self.preview_widget.current_slide_number(), -1,
u'The slide number should be -1.')
def replace_service_item_test(self):
"""
Test item counts and current number with a service item.
"""
# GIVEN: A ServiceItem with two frames.
service_item = ServiceItem(None)
service = read_service_from_file(u'serviceitem_image_2.osd')
with patch('os.path.exists'):
service_item.set_from_service(service[0])
# WHEN: Added to the preview widget.
self.preview_widget.replace_service_item(service_item, 1, 1)
# THEN: The slide count and number should fit.
self.assertEqual(self.preview_widget.slide_count(), 2,
u'The slide count should be 2.')
self.assertEqual(self.preview_widget.current_slide_number(), 1,
u'The current slide number should be 1.')
def change_slide_test(self):
"""
Test the change_slide method.
"""
# GIVEN: A ServiceItem with two frames content.
service_item = ServiceItem(None)
service = read_service_from_file(u'serviceitem_image_2.osd')
with patch('os.path.exists'):
service_item.set_from_service(service[0])
# WHEN: Added to the preview widget and switched to the second frame.
self.preview_widget.replace_service_item(service_item, 1, 0)
self.preview_widget.change_slide(1)
# THEN: The current_slide_number should reflect the change.
self.assertEqual(self.preview_widget.current_slide_number(), 1,
u'The current slide number should be 1.')

0
tests/utils/__init__.py Normal file
View File

5
tests/utils/constants.py Normal file
View File

@ -0,0 +1,5 @@
import os
OPENLP_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), u'..', u'..'))
TEST_RESOURCES_PATH = os.path.join(OPENLP_PATH, u'tests', u'resources')

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
###############################################################################
# OpenLP - Open Source Lyrics Projection #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2013 Raoul Snyman #
# Portions copyright (c) 2008-2013 Tim Bentley, Gerald Britton, Jonathan #
# Corwin, Samuel Findlay, Michael Gorven, Scott Guerrieri, Matthias Hub, #
# Meinert Jordan, Armin Köhler, Erik Lundin, Edwin Lunando, Brian T. Meyer. #
# Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias Põldaru, #
# Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, #
# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Dave Warnock, #
# Frode Woldsund, Martin Zibricky, Patrick Zimmermann #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The :mod:`osdinteraction` provides miscellaneous functions for interacting with
OSD files.
"""
import os
import cPickle
from tests.utils.constants import TEST_RESOURCES_PATH
def read_service_from_file(file_name):
"""
Reads an OSD file and returns the first service item found therein.
@param file_name: File name of an OSD file residing in the tests/resources folder.
@return: The service contained in the file.
"""
service_file = os.path.join(TEST_RESOURCES_PATH, file_name)
with open(service_file, u'r') as open_file:
service = cPickle.load(open_file)
return service