From 365a4c62cf41a6760a3960d9cd07c755d7138b94 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 24 Jan 2014 19:43:02 +0000 Subject: [PATCH 01/66] Created GUI for mediaclip selection from DVD. --- .../media/forms/mediaclipselectordialog.py | 190 +++++++++ .../media/forms/mediaclipselectorform.py | 276 ++++++++++++++ openlp/plugins/media/lib/mediaitem.py | 8 + resources/forms/mediaclipselector.ui | 359 ++++++++++++++++++ 4 files changed, 833 insertions(+) create mode 100644 openlp/plugins/media/forms/mediaclipselectordialog.py create mode 100644 openlp/plugins/media/forms/mediaclipselectorform.py create mode 100644 resources/forms/mediaclipselector.ui diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py new file mode 100644 index 000000000..a6718fbf2 --- /dev/null +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -0,0 +1,190 @@ +from PyQt4 import QtCore, QtGui +from openlp.core.common import translate + +class Ui_MediaClipSelector(object): + def setupUi(self, MediaClipSelector): + MediaClipSelector.setObjectName("MediaClipSelector") + MediaClipSelector.resize(683, 739) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(MediaClipSelector.sizePolicy().hasHeightForWidth()) + MediaClipSelector.setSizePolicy(sizePolicy) + MediaClipSelector.setMinimumSize(QtCore.QSize(683, 686)) + MediaClipSelector.setFocusPolicy(QtCore.Qt.NoFocus) + MediaClipSelector.setAutoFillBackground(False) + MediaClipSelector.setInputMethodHints(QtCore.Qt.ImhNone) + self.centralwidget = QtGui.QWidget(MediaClipSelector) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) + self.centralwidget.setSizePolicy(sizePolicy) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout = QtGui.QGridLayout(self.centralwidget) + self.gridLayout.setObjectName("gridLayout") + self.close_pushbutton = QtGui.QPushButton(self.centralwidget) + self.close_pushbutton.setEnabled(True) + self.close_pushbutton.setObjectName("close_pushbutton") + self.gridLayout.addWidget(self.close_pushbutton, 10, 4, 1, 1) + self.pause_pushbutton = QtGui.QPushButton(self.centralwidget) + self.pause_pushbutton.setEnabled(True) + self.pause_pushbutton.setText("") + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.pause_pushbutton.setIcon(icon) + self.pause_pushbutton.setObjectName("pause_pushbutton") + self.gridLayout.addWidget(self.pause_pushbutton, 6, 1, 1, 1) + self.play_pushbutton = QtGui.QPushButton(self.centralwidget) + self.play_pushbutton.setEnabled(True) + self.play_pushbutton.setText("") + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.play_pushbutton.setIcon(icon1) + self.play_pushbutton.setObjectName("play_pushbutton") + self.gridLayout.addWidget(self.play_pushbutton, 6, 0, 1, 1) + self.media_path_label = QtGui.QLabel(self.centralwidget) + self.media_path_label.setEnabled(True) + self.media_path_label.setObjectName("media_path_label") + self.gridLayout.addWidget(self.media_path_label, 0, 0, 1, 2) + self.preview_pushbutton = QtGui.QPushButton(self.centralwidget) + self.preview_pushbutton.setEnabled(True) + self.preview_pushbutton.setObjectName("preview_pushbutton") + self.gridLayout.addWidget(self.preview_pushbutton, 10, 2, 1, 1) + self.start_point_label = QtGui.QLabel(self.centralwidget) + self.start_point_label.setEnabled(True) + self.start_point_label.setObjectName("start_point_label") + self.gridLayout.addWidget(self.start_point_label, 7, 0, 1, 2) + spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 9, 3, 1, 1) + self.start_timeedit = QtGui.QTimeEdit(self.centralwidget) + self.start_timeedit.setEnabled(True) + self.start_timeedit.setObjectName("start_timeedit") + self.gridLayout.addWidget(self.start_timeedit, 7, 2, 1, 1) + self.jump_end_pushbutton = QtGui.QPushButton(self.centralwidget) + self.jump_end_pushbutton.setEnabled(True) + self.jump_end_pushbutton.setObjectName("jump_end_pushbutton") + self.gridLayout.addWidget(self.jump_end_pushbutton, 8, 4, 1, 1) + self.subtitle_track_label = QtGui.QLabel(self.centralwidget) + self.subtitle_track_label.setEnabled(True) + self.subtitle_track_label.setObjectName("subtitle_track_label") + self.gridLayout.addWidget(self.subtitle_track_label, 4, 0, 1, 2) + self.set_end_pushbutton = QtGui.QPushButton(self.centralwidget) + self.set_end_pushbutton.setEnabled(True) + self.set_end_pushbutton.setObjectName("set_end_pushbutton") + self.gridLayout.addWidget(self.set_end_pushbutton, 8, 3, 1, 1) + self.set_start_pushbutton = QtGui.QPushButton(self.centralwidget) + self.set_start_pushbutton.setEnabled(True) + self.set_start_pushbutton.setObjectName("set_start_pushbutton") + self.gridLayout.addWidget(self.set_start_pushbutton, 7, 3, 1, 1) + self.audio_track_label = QtGui.QLabel(self.centralwidget) + self.audio_track_label.setEnabled(True) + self.audio_track_label.setObjectName("audio_track_label") + self.gridLayout.addWidget(self.audio_track_label, 3, 0, 1, 2) + self.load_disc_pushbutton = QtGui.QPushButton(self.centralwidget) + self.load_disc_pushbutton.setEnabled(True) + self.load_disc_pushbutton.setObjectName("load_disc_pushbutton") + self.gridLayout.addWidget(self.load_disc_pushbutton, 0, 4, 1, 1) + self.media_position_timeedit = QtGui.QTimeEdit(self.centralwidget) + self.media_position_timeedit.setEnabled(True) + self.media_position_timeedit.setObjectName("media_position_timeedit") + self.gridLayout.addWidget(self.media_position_timeedit, 6, 4, 1, 1) + self.end_point_label = QtGui.QLabel(self.centralwidget) + self.end_point_label.setEnabled(True) + self.end_point_label.setObjectName("end_point_label") + self.gridLayout.addWidget(self.end_point_label, 8, 0, 1, 1) + self.jump_start_pushbutton = QtGui.QPushButton(self.centralwidget) + self.jump_start_pushbutton.setEnabled(True) + self.jump_start_pushbutton.setObjectName("jump_start_pushbutton") + self.gridLayout.addWidget(self.jump_start_pushbutton, 7, 4, 1, 1) + self.end_timeedit = QtGui.QTimeEdit(self.centralwidget) + self.end_timeedit.setEnabled(True) + self.end_timeedit.setObjectName("end_timeedit") + self.gridLayout.addWidget(self.end_timeedit, 8, 2, 1, 1) + self.title_label = QtGui.QLabel(self.centralwidget) + self.title_label.setEnabled(True) + self.title_label.setObjectName("title_label") + self.gridLayout.addWidget(self.title_label, 2, 0, 1, 1) + self.save_pushbutton = QtGui.QPushButton(self.centralwidget) + self.save_pushbutton.setEnabled(True) + self.save_pushbutton.setObjectName("save_pushbutton") + self.gridLayout.addWidget(self.save_pushbutton, 10, 3, 1, 1) + self.media_path_combobox = QtGui.QComboBox(self.centralwidget) + self.media_path_combobox.setEnabled(True) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.media_path_combobox.sizePolicy().hasHeightForWidth()) + self.media_path_combobox.setSizePolicy(sizePolicy) + self.media_path_combobox.setEditable(True) + self.media_path_combobox.setObjectName("media_path_combobox") + self.gridLayout.addWidget(self.media_path_combobox, 0, 2, 1, 2) + self.position_horizontalslider = QtGui.QSlider(self.centralwidget) + self.position_horizontalslider.setEnabled(True) + self.position_horizontalslider.setTracking(False) + self.position_horizontalslider.setOrientation(QtCore.Qt.Horizontal) + self.position_horizontalslider.setInvertedAppearance(False) + self.position_horizontalslider.setObjectName("position_horizontalslider") + self.gridLayout.addWidget(self.position_horizontalslider, 6, 2, 1, 2) + self.title_combo_box = QtGui.QComboBox(self.centralwidget) + self.title_combo_box.setEnabled(True) + self.title_combo_box.setProperty("currentText", "") + self.title_combo_box.setObjectName("title_combo_box") + self.gridLayout.addWidget(self.title_combo_box, 2, 2, 1, 2) + self.audio_tracks_combobox = QtGui.QComboBox(self.centralwidget) + self.audio_tracks_combobox.setEnabled(True) + self.audio_tracks_combobox.setObjectName("audio_tracks_combobox") + self.gridLayout.addWidget(self.audio_tracks_combobox, 3, 2, 1, 2) + self.subtitle_tracks_combobox = QtGui.QComboBox(self.centralwidget) + self.subtitle_tracks_combobox.setEnabled(True) + self.subtitle_tracks_combobox.setObjectName("subtitle_tracks_combobox") + self.gridLayout.addWidget(self.subtitle_tracks_combobox, 4, 2, 1, 2) + self.media_view_frame = QtGui.QFrame(self.centralwidget) + self.media_view_frame.setMinimumSize(QtCore.QSize(665, 375)) + self.media_view_frame.setStyleSheet("background-color:black;") + self.media_view_frame.setFrameShape(QtGui.QFrame.StyledPanel) + self.media_view_frame.setFrameShadow(QtGui.QFrame.Raised) + self.media_view_frame.setObjectName("media_view_frame") + self.gridLayout.addWidget(self.media_view_frame, 5, 0, 1, 5) + #MediaClipSelector.setCentralWidget(self.centralwidget) + + self.retranslateUi(MediaClipSelector) + QtCore.QMetaObject.connectSlotsByName(MediaClipSelector) + MediaClipSelector.setTabOrder(self.media_path_combobox, self.load_disc_pushbutton) + MediaClipSelector.setTabOrder(self.load_disc_pushbutton, self.title_combo_box) + MediaClipSelector.setTabOrder(self.title_combo_box, self.audio_tracks_combobox) + MediaClipSelector.setTabOrder(self.audio_tracks_combobox, self.subtitle_tracks_combobox) + MediaClipSelector.setTabOrder(self.subtitle_tracks_combobox, self.play_pushbutton) + MediaClipSelector.setTabOrder(self.play_pushbutton, self.pause_pushbutton) + MediaClipSelector.setTabOrder(self.pause_pushbutton, self.position_horizontalslider) + MediaClipSelector.setTabOrder(self.position_horizontalslider, self.media_position_timeedit) + MediaClipSelector.setTabOrder(self.media_position_timeedit, self.start_timeedit) + MediaClipSelector.setTabOrder(self.start_timeedit, self.set_start_pushbutton) + MediaClipSelector.setTabOrder(self.set_start_pushbutton, self.jump_start_pushbutton) + MediaClipSelector.setTabOrder(self.jump_start_pushbutton, self.end_timeedit) + MediaClipSelector.setTabOrder(self.end_timeedit, self.set_end_pushbutton) + MediaClipSelector.setTabOrder(self.set_end_pushbutton, self.jump_end_pushbutton) + MediaClipSelector.setTabOrder(self.jump_end_pushbutton, self.preview_pushbutton) + MediaClipSelector.setTabOrder(self.preview_pushbutton, self.save_pushbutton) + MediaClipSelector.setTabOrder(self.save_pushbutton, self.close_pushbutton) + + def retranslateUi(self, MediaClipSelector): + MediaClipSelector.setWindowTitle(translate("MediaPlugin.MediaClipSelector", "Select media clip", None)) + self.close_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Close", None)) + self.media_path_label.setText(translate("MediaPlugin.MediaClipSelector", "Media path", None)) + self.preview_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Preview current clip", None)) + self.start_point_label.setText(translate("MediaPlugin.MediaClipSelector", "Start point", None)) + self.start_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) + self.jump_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to end point", None)) + self.subtitle_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Subtitle track", None)) + self.set_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as end point", None)) + self.set_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as start point", None)) + self.audio_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Audio track", None)) + self.load_disc_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Load disc", None)) + self.media_position_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) + self.end_point_label.setText(translate("MediaPlugin.MediaClipSelector", "End point", None)) + self.jump_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to start point", None)) + self.end_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) + self.title_label.setText(translate("MediaPlugin.MediaClipSelector", "Title", None)) + self.save_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Save current clip", None)) + diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py new file mode 100644 index 000000000..6f7f22c57 --- /dev/null +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 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 # +############################################################################### + +import os +import sys +import logging +import time + +from PyQt4 import QtCore, QtGui + +from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector +from openlp.core.ui.media.vendor import vlc + +log = logging.getLogger(__name__) + + +class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): + """ + Class to manage the clip selection + """ + log.info('%s MediaClipSelectorForm loaded', __name__) + + def __init__(self, media_item, parent, manager): + """ + Constructor + """ + super(MediaClipSelectorForm, self).__init__(parent) + self.media_item = media_item + self.setupUi(self) + self.playback_length = 0 + self.position_horizontalslider.setMinimum(0) + self.disable_all() + self.toggle_disable_load_media(False) + # most actions auto-connect due to the functions name, so only a few left to do + self.close_pushbutton.clicked.connect(self.reject) + #self.load_disc_pushbutton.clicked.connect(self.on_load_disc_pushbutton_clicked) + #self.pause_pushbutton.clicked.connect(self.on_pause_pushbutton_clicked) + #self.play_pushbutton.clicked.connect(self.on_play_pushbutton_clicked) + + def reject(self): + """ + Exit Dialog and do not save + """ + log.debug ('MediaClipSelectorForm.reject') + self.vlc_media_player.stop() + QtGui.QDialog.reject(self) + + def exec_(self): + self.setup_vlc() + return QtGui.QDialog.exec_(self) + + def setup_vlc(self): + self.vlc_instance = vlc.Instance() + # creating an empty vlc media player + self.vlc_media_player = self.vlc_instance.media_player_new() + # The media player has to be 'connected' to the QFrame. + # (otherwise a video would be displayed in it's own window) + # This is platform specific! + # You have to give the id of the QFrame (or similar object) + # to vlc, different platforms have different functions for this. + win_id = int(self.media_view_frame.winId()) + if sys.platform == "win32": + self.vlc_media_player.set_hwnd(win_id) + elif sys.platform == "darwin": + # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa + # framework and not the old Carbon. + self.vlc_media_player.set_nsobject(win_id) + else: + # for Linux using the X Server + self.vlc_media_player.set_xwindow(win_id) + self.vlc_media = None + # Setup timer every 100 ms to update position + self.timer = QtCore.QTimer(self) + self.timer.timeout.connect(self.update_position) + self.timer.start(100) + + @QtCore.pyqtSlot(bool) + def on_load_disc_pushbutton_clicked(self, clicked): + self.disable_all() + path = self.media_path_combobox.currentText() + if path == '': + print('no given path') + # TODO: Error message + self.toggle_disable_load_media(False) + return + self.vlc_media = self.vlc_instance.media_new_path(path) + if not self.vlc_media: + print('media player is none') + # TODO: Error message + self.toggle_disable_load_media(False) + return + # put the media in the media player + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.audio_set_mute(True) + # start playback to get vlc to parse the media + if self.vlc_media_player.play() < 0: + print('play returned error') + # TODO: Error message + self.toggle_disable_load_media(False) + return + self.vlc_media_player.audio_set_mute(True) + while self.vlc_media_player.get_time() == 0: + if self.vlc_media_player.get_state() == vlc.State.Error: + print('player in error state') + self.toggle_disable_load_media(False) + return + time.sleep(0.1) + self.vlc_media_player.pause() + self.vlc_media_player.set_time(0) + # Get titles, insert in combobox + titles = self.vlc_media_player.video_get_title_description() + self.title_combo_box.clear() + for title in titles: + self.title_combo_box.addItem(title[1].decode(), title[0]) + # Main title is usually title #1 + if len(titles) > 1: + self.title_combo_box.setCurrentIndex(1) + else: + self.title_combo_box.setCurrentIndex(0) + # Enable audio track combobox if anything is in it + if len(titles) > 0: + self.title_combo_box.setDisabled(False) + self.toggle_disable_load_media(False) + + def on_pause_pushbutton_clicked(self): + self.vlc_media_player.pause() + + def on_play_pushbutton_clicked(self): + self.vlc_media_player.play() + + def on_set_start_pushbutton_clicked(self): + vlc_ms_pos = self.vlc_media_player.get_time() + time = QtCore.QTime() + new_pos_time = time.addMSecs(vlc_ms_pos) + self.start_timeedit.setTime(new_pos_time) + + def on_set_end_pushbutton_clicked(self): + vlc_ms_pos = self.vlc_media_player.get_time() + time = QtCore.QTime() + new_pos_time = time.addMSecs(vlc_ms_pos) + self.end_timeedit.setTime(new_pos_time) + + def on_jump_end_pushbutton_clicked(self): + end_time = self.end_timeedit.time() + end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() + self.vlc_media_player.set_time(end_time_ms) + + def on_jump_start_pushbutton_clicked(self): + start_time = self.start_timeedit.time() + start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() + self.vlc_media_player.set_time(start_time_ms) + + @QtCore.pyqtSlot(int) + def on_title_combo_box_currentIndexChanged(self, index): + print('in on_title_combo_box_changed, index: ', str(index)) + self.vlc_media_player.set_title(index) + self.vlc_media_player.set_time(0) + self.vlc_media_player.play() + self.vlc_media_player.audio_set_mute(True) + while self.vlc_media_player.get_time() == 0: + time.sleep(0.1) + # pause + self.vlc_media_player.pause() + self.vlc_media_player.set_time(0) + # Get audio tracks, insert in combobox + audio_tracks = self.vlc_media_player.audio_get_track_description() + self.audio_tracks_combobox.clear() + for audio_track in audio_tracks: + self.audio_tracks_combobox.addItem(audio_track[1].decode(),audio_track[0]) + # Enable audio track combobox if anything is in it + if len(audio_tracks) > 0: + self.audio_tracks_combobox.setDisabled(False) + # First track is "deactivated", so set to next if it exists + if len(audio_tracks) > 1: + self.audio_tracks_combobox.setCurrentIndex(1) + # Get subtitle tracks, insert in combobox + subtitles_tracks = self.vlc_media_player.video_get_spu_description() + self.subtitle_tracks_combobox.clear() + for subtitle_track in subtitles_tracks: + self.subtitle_tracks_combobox.addItem(subtitle_track[1].decode(), subtitle_track[0]) + # Enable subtitle track combobox is anything in it + if len(subtitles_tracks) > 0: + self.subtitle_tracks_combobox.setDisabled(False) + # First track is "deactivated", so set to next if it exists + if len(subtitles_tracks) > 1: + self.subtitle_tracks_combobox.setCurrentIndex(1) + self.vlc_media_player.audio_set_mute(False) + self.playback_length = self.vlc_media_player.get_length() + self.position_horizontalslider.setMaximum(self.playback_length) + # If a title or audio track is available the player is enabled + if self.title_combo_box.count() > 0 or len(audio_tracks) > 0: + self.toggle_disable_player(False) + + @QtCore.pyqtSlot(int) + def on_audio_tracks_combobox_currentIndexChanged(self, index): + audio_track = self.audio_tracks_combobox.itemData(index) + print('in on_audio_tracks_combobox_currentIndexChanged, index: ', str(index), ' audio_track: ', audio_track) + if audio_track and int(audio_track) > 0: + self.vlc_media_player.audio_set_track(int(audio_track)) + + @QtCore.pyqtSlot(int) + def on_subtitle_tracks_combobox_currentIndexChanged(self, index): + subtitle_track = self.subtitle_tracks_combobox.itemData(index) + print('in on_subtitle_tracks_combobox_currentIndexChanged, index: ', str(index), ' subtitle_track: ', subtitle_track) + if subtitle_track: + self.vlc_media_player.video_set_spu(int(subtitle_track)) + + def on_position_horizontalslider_sliderMoved(self, position): + self.vlc_media_player.set_time(position) + + def update_position(self): + if self.vlc_media_player: + vlc_ms_pos = self.vlc_media_player.get_time() + #print('in update_position, time: ', vlc_ms_pos) + rounded_vlc_ms_pos = int(round(vlc_ms_pos / 100.0) * 100.0) + time = QtCore.QTime() + new_pos_time = time.addMSecs(rounded_vlc_ms_pos) + self.media_position_timeedit.setTime(new_pos_time) + self.position_horizontalslider.setSliderPosition(vlc_ms_pos) + + def disable_all(self): + self.toggle_disable_load_media(True) + self.title_combo_box.setDisabled(True) + self.audio_tracks_combobox.setDisabled(True) + self.subtitle_tracks_combobox.setDisabled(True) + self.toggle_disable_player(True) + + def toggle_disable_load_media(self, action): + self.media_path_combobox.setDisabled(action) + self.load_disc_pushbutton.setDisabled(action) + + def toggle_disable_player(self, action): + self.play_pushbutton.setDisabled(action) + self.pause_pushbutton.setDisabled(action) + self.position_horizontalslider.setDisabled(action) + self.media_position_timeedit.setDisabled(action) + self.start_timeedit.setDisabled(action) + self.set_start_pushbutton.setDisabled(action) + self.jump_start_pushbutton.setDisabled(action) + self.end_timeedit.setDisabled(action) + self.set_end_pushbutton.setDisabled(action) + self.jump_end_pushbutton.setDisabled(action) + self.preview_pushbutton.setDisabled(action) + self.save_pushbutton.setDisabled(action) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index c9ba6a47d..58dec0dac 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -39,6 +39,8 @@ from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adj from openlp.core.ui import DisplayController, Display, DisplayControllerType from openlp.core.ui.media import get_media_players, set_media_players from openlp.core.utils import get_locale_key +from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm + log = logging.getLogger(__name__) @@ -115,6 +117,7 @@ class MediaMediaItem(MediaManagerItem): triggers=self.onReplaceClick) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', visible=False, triggers=self.onResetClick) + self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=':/songs/song_maintenance.png', triggers=self.on_load_optical) self.media_widget = QtGui.QWidget(self) self.media_widget.setObjectName('media_widget') self.display_layout = QtGui.QFormLayout(self.media_widget) @@ -215,6 +218,7 @@ class MediaMediaItem(MediaManagerItem): check_directory_exists(self.servicePath) self.load_list(Settings().value(self.settings_section + '/media files')) self.populateDisplayTypes() + self.media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) def rebuild_players(self): """ @@ -311,3 +315,7 @@ class MediaMediaItem(MediaManagerItem): if filename.lower().find(string) > -1: results.append([file, filename]) return results + + def on_load_optical(self): + log.debug('in on_load_optical') + self.media_clip_selector_form.exec_() diff --git a/resources/forms/mediaclipselector.ui b/resources/forms/mediaclipselector.ui new file mode 100644 index 000000000..4cd8f518d --- /dev/null +++ b/resources/forms/mediaclipselector.ui @@ -0,0 +1,359 @@ + + + MediaClipSelector + + + + 0 + 0 + 683 + 739 + + + + + 0 + 0 + + + + + 683 + 686 + + + + Qt::NoFocus + + + Select media clip + + + false + + + Qt::ImhNone + + + + + 0 + 0 + + + + + + + true + + + Close + + + + + + + true + + + + + + + ../images/media_playback_pause.png../images/media_playback_pause.png + + + + + + + true + + + + + + + ../images/media_playback_start.png../images/media_playback_start.png + + + + + + + true + + + Media path + + + + + + + true + + + Preview current clip + + + + + + + true + + + Start point + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 40 + + + + + + + + true + + + HH:mm:ss.z + + + + + + + true + + + Jump to end point + + + + + + + true + + + Subtitle track + + + + + + + true + + + Set current position as end point + + + + + + + true + + + Set current position as start point + + + + + + + true + + + Audio track + + + + + + + true + + + Load disc + + + + + + + true + + + HH:mm:ss.z + + + + + + + true + + + End point + + + + + + + true + + + Jump to start point + + + + + + + true + + + HH:mm:ss.z + + + + + + + true + + + Title + + + + + + + true + + + Save current clip + + + + + + + true + + + + 0 + 0 + + + + true + + + + + + + true + + + false + + + Qt::Horizontal + + + false + + + + + + + true + + + + + + + + + + true + + + + + + + true + + + + + + + + 665 + 375 + + + + background-color:black; + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + media_path_combobox + load_disc_pushbutton + title_combo_box + audio_tracks_combobox + subtitle_tracks_combobox + play_pushbutton + pause_pushbutton + position_horizontalslider + media_position_timeedit + start_timeedit + set_start_pushbutton + jump_start_pushbutton + end_timeedit + set_end_pushbutton + jump_end_pushbutton + preview_pushbutton + save_pushbutton + close_pushbutton + + + + From 6d21a326edd01f7568456049cebd095541b0c280 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 17 Feb 2014 23:29:52 +0100 Subject: [PATCH 02/66] Made it possible to save a selected clip to the mediamanager. --- .../media/forms/mediaclipselectorform.py | 124 +++++++++++++++--- openlp/plugins/media/lib/mediaitem.py | 48 +++++-- resources/images/media_optical.png | Bin 0 -> 2056 bytes resources/images/openlp-2.qrc | 1 + 4 files changed, 143 insertions(+), 30 deletions(-) create mode 100644 resources/images/media_optical.png diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 6f7f22c57..688d879d4 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -35,6 +35,7 @@ import time from PyQt4 import QtCore, QtGui from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector +from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.media.vendor import vlc log = logging.getLogger(__name__) @@ -59,9 +60,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.toggle_disable_load_media(False) # most actions auto-connect due to the functions name, so only a few left to do self.close_pushbutton.clicked.connect(self.reject) - #self.load_disc_pushbutton.clicked.connect(self.on_load_disc_pushbutton_clicked) - #self.pause_pushbutton.clicked.connect(self.on_pause_pushbutton_clicked) - #self.play_pushbutton.clicked.connect(self.on_play_pushbutton_clicked) def reject(self): """ @@ -72,10 +70,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): QtGui.QDialog.reject(self) def exec_(self): + """ + Start dialog + """ self.setup_vlc() return QtGui.QDialog.exec_(self) def setup_vlc(self): + """ + Setup VLC instance and mediaplayer + """ self.vlc_instance = vlc.Instance() # creating an empty vlc media player self.vlc_media_player = self.vlc_instance.media_player_new() @@ -102,17 +106,20 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): @QtCore.pyqtSlot(bool) def on_load_disc_pushbutton_clicked(self, clicked): + """ + Load the media when the load-button has been clicked + """ self.disable_all() path = self.media_path_combobox.currentText() if path == '': - print('no given path') - # TODO: Error message + log.debug('no given path') + critical_error_message_box('Error', 'No path was given') self.toggle_disable_load_media(False) return self.vlc_media = self.vlc_instance.media_new_path(path) if not self.vlc_media: - print('media player is none') - # TODO: Error message + log.debug('vlc media player is none') + critical_error_message_box('Error', 'An error happened during initialization of VLC player') self.toggle_disable_load_media(False) return # put the media in the media player @@ -120,14 +127,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.audio_set_mute(True) # start playback to get vlc to parse the media if self.vlc_media_player.play() < 0: - print('play returned error') - # TODO: Error message + log.debug('vlc play returned error') + critical_error_message_box('Error', 'An error happen when starting VLC player') self.toggle_disable_load_media(False) return self.vlc_media_player.audio_set_mute(True) while self.vlc_media_player.get_time() == 0: if self.vlc_media_player.get_state() == vlc.State.Error: - print('player in error state') + log.debug('player in error state') self.toggle_disable_load_media(False) return time.sleep(0.1) @@ -148,25 +155,45 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.title_combo_box.setDisabled(False) self.toggle_disable_load_media(False) - def on_pause_pushbutton_clicked(self): + @QtCore.pyqtSlot(bool) + def on_pause_pushbutton_clicked(self, clicked): + """ + Pause the playback + """ self.vlc_media_player.pause() - def on_play_pushbutton_clicked(self): + @QtCore.pyqtSlot(bool) + def on_play_pushbutton_clicked(self, clicked): + """ + Start the playback + """ self.vlc_media_player.play() - def on_set_start_pushbutton_clicked(self): + @QtCore.pyqtSlot(bool) + def on_set_start_pushbutton_clicked(self, clicked): + """ + Copy the current player position to start_timeedit + """ vlc_ms_pos = self.vlc_media_player.get_time() time = QtCore.QTime() new_pos_time = time.addMSecs(vlc_ms_pos) self.start_timeedit.setTime(new_pos_time) - def on_set_end_pushbutton_clicked(self): + @QtCore.pyqtSlot(bool) + def on_set_end_pushbutton_clicked(self, clicked): + """ + Copy the current player position to end_timeedit + """ vlc_ms_pos = self.vlc_media_player.get_time() time = QtCore.QTime() new_pos_time = time.addMSecs(vlc_ms_pos) self.end_timeedit.setTime(new_pos_time) - def on_jump_end_pushbutton_clicked(self): + @QtCore.pyqtSlot(bool) + def on_jump_end_pushbutton_clicked(self, clicked): + """ + Set the player position to the position stored in end_timeedit + """ end_time = self.end_timeedit.time() end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ end_time.minute() * 60 * 1000 + \ @@ -174,7 +201,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): end_time.msec() self.vlc_media_player.set_time(end_time_ms) - def on_jump_start_pushbutton_clicked(self): + @QtCore.pyqtSlot(bool) + def on_jump_start_pushbutton_clicked(self, clicked): + """ + Set the player position to the position stored in start_timeedit + """ start_time = self.start_timeedit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ start_time.minute() * 60 * 1000 + \ @@ -184,12 +215,18 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): @QtCore.pyqtSlot(int) def on_title_combo_box_currentIndexChanged(self, index): - print('in on_title_combo_box_changed, index: ', str(index)) + """ + When a new title is chosen, it is loaded by VLC and info about audio and subtitle tracks is reloaded + """ + log.debug('in on_title_combo_box_changed, index: ', str(index)) self.vlc_media_player.set_title(index) self.vlc_media_player.set_time(0) self.vlc_media_player.play() self.vlc_media_player.audio_set_mute(True) while self.vlc_media_player.get_time() == 0: + if self.vlc_media_player.get_state() == vlc.State.Error: + log.debug('player in error state') + return time.sleep(0.1) # pause self.vlc_media_player.pause() @@ -225,25 +262,36 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): @QtCore.pyqtSlot(int) def on_audio_tracks_combobox_currentIndexChanged(self, index): + """ + When a new audio track is chosen update audio track bing played by VLC + """ audio_track = self.audio_tracks_combobox.itemData(index) - print('in on_audio_tracks_combobox_currentIndexChanged, index: ', str(index), ' audio_track: ', audio_track) + log.debug('in on_audio_tracks_combobox_currentIndexChanged, index: ', str(index), ' audio_track: ', audio_track) if audio_track and int(audio_track) > 0: self.vlc_media_player.audio_set_track(int(audio_track)) @QtCore.pyqtSlot(int) def on_subtitle_tracks_combobox_currentIndexChanged(self, index): + """ + When a new subtitle track is chosen update subtitle track bing played by VLC + """ subtitle_track = self.subtitle_tracks_combobox.itemData(index) - print('in on_subtitle_tracks_combobox_currentIndexChanged, index: ', str(index), ' subtitle_track: ', subtitle_track) + log.debug('in on_subtitle_tracks_combobox_currentIndexChanged, index: ', str(index), ' subtitle_track: ', subtitle_track) if subtitle_track: self.vlc_media_player.video_set_spu(int(subtitle_track)) def on_position_horizontalslider_sliderMoved(self, position): + """ + Set player position according to new slider position. + """ self.vlc_media_player.set_time(position) def update_position(self): + """ + Update slider position and displayed time according to VLC player position. + """ if self.vlc_media_player: vlc_ms_pos = self.vlc_media_player.get_time() - #print('in update_position, time: ', vlc_ms_pos) rounded_vlc_ms_pos = int(round(vlc_ms_pos / 100.0) * 100.0) time = QtCore.QTime() new_pos_time = time.addMSecs(rounded_vlc_ms_pos) @@ -251,6 +299,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.position_horizontalslider.setSliderPosition(vlc_ms_pos) def disable_all(self): + """ + Disable all elements in the dialog + """ self.toggle_disable_load_media(True) self.title_combo_box.setDisabled(True) self.audio_tracks_combobox.setDisabled(True) @@ -258,10 +309,20 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.toggle_disable_player(True) def toggle_disable_load_media(self, action): + """ + Enable/disable load media combobox and button. + + @param action: If True elements are disabled, if False they are enabled. + """ self.media_path_combobox.setDisabled(action) self.load_disc_pushbutton.setDisabled(action) def toggle_disable_player(self, action): + """ + Enable/disable player elementa. + + @param action: If True elements are disabled, if False they are enabled. + """ self.play_pushbutton.setDisabled(action) self.pause_pushbutton.setDisabled(action) self.position_horizontalslider.setDisabled(action) @@ -274,3 +335,26 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.jump_end_pushbutton.setDisabled(action) self.preview_pushbutton.setDisabled(action) self.save_pushbutton.setDisabled(action) + + @QtCore.pyqtSlot(bool) + def on_save_pushbutton_clicked(self, checked): + """ + Saves the current media and trackinfo as a clip to the mediamanager + """ + log.debug('in on_save_pushbutton_clicked') + start_time = self.start_timeedit.time() + start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() + end_time = self.end_timeedit.time() + end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() + title = self.title_combo_box.itemData(self.title_combo_box.currentIndex()) + audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) + subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) + path = self.media_path_combobox.currentText() + optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str(start_time_ms) + ':' + str(end_time_ms) + ':' + path + self.media_item.add_optical_clip(optical) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 58dec0dac..33b983dad 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -40,6 +40,7 @@ from openlp.core.ui import DisplayController, Display, DisplayControllerType from openlp.core.ui.media import get_media_players, set_media_players from openlp.core.utils import get_locale_key from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm +from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE @@ -49,7 +50,7 @@ log = logging.getLogger(__name__) CLAPPERBOARD = ':/media/slidecontroller_multimedia.png' VIDEO_ICON = build_icon(':/media/media_video.png') AUDIO_ICON = build_icon(':/media/media_audio.png') -DVD_ICON = build_icon(':/media/media_video.png') +OPTICAL_ICON = build_icon(':/media/media_optical.png') ERROR_ICON = build_icon(':/general/general_delete.png') @@ -111,13 +112,18 @@ class MediaMediaItem(MediaManagerItem): MediaManagerItem.add_list_view_to_toolbar(self) self.list_view.addAction(self.replace_action) + def add_start_header_bar(self): + self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text='Load optical disc', + tooltip='Load optical disc', triggers=self.on_load_optical) + if not VLC_AVAILABLE: + self.load_optical.setDisabled(True) + def add_end_header_bar(self): # Replace backgrounds do not work at present so remove functionality. self.replace_action = self.toolbar.add_toolbar_action('replace_action', icon=':/slides/slide_blank.png', triggers=self.onReplaceClick) self.reset_action = self.toolbar.add_toolbar_action('reset_action', icon=':/system/system_close.png', visible=False, triggers=self.onResetClick) - self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=':/songs/song_maintenance.png', triggers=self.on_load_optical) self.media_widget = QtGui.QWidget(self) self.media_widget.setObjectName('media_widget') self.display_layout = QtGui.QFormLayout(self.media_widget) @@ -269,15 +275,23 @@ class MediaMediaItem(MediaManagerItem): Settings().setValue(self.settings_section + '/media files', self.get_file_list()) def load_list(self, media, target_group=None): - # Sort the media by its filename considering language specific characters. + # Sort the media by its filename considering language specific characters. media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) for track in media: track_info = QtCore.QFileInfo(track) - if not os.path.exists(track): + if track.startswith('optical:'): + (filename, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(track) + optical = filename + '@' + str(title) + ':' + str(start) + '-' + str(end) + item_name = QtGui.QListWidgetItem(optical) + item_name.setIcon(build_icon(OPTICAL_ICON)) + item_name.setData(QtCore.Qt.UserRole, track) + item_name.setToolTip(optical) + elif not os.path.exists(track): filename = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(filename) item_name.setIcon(ERROR_ICON) item_name.setData(QtCore.Qt.UserRole, track) + item_name.setToolTip(track) elif track_info.isFile(): filename = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(filename) @@ -286,12 +300,7 @@ class MediaMediaItem(MediaManagerItem): else: item_name.setIcon(VIDEO_ICON) item_name.setData(QtCore.Qt.UserRole, track) - else: - filename = os.path.split(str(track))[1] - item_name = QtGui.QListWidgetItem(filename) - item_name.setIcon(build_icon(DVD_ICON)) - item_name.setData(QtCore.Qt.UserRole, track) - item_name.setToolTip(track) + item_name.setToolTip(track) self.list_view.addItem(item_name) def get_list(self, type=MediaType.Audio): @@ -319,3 +328,22 @@ class MediaMediaItem(MediaManagerItem): def on_load_optical(self): log.debug('in on_load_optical') self.media_clip_selector_form.exec_() + + def parse_optical_path(self, input): + # split the clip info + clip_info = input.split(sep=':') + title = int(clip_info[1]) + audio_track = int(clip_info[2]) + subtitle_track = int(clip_info[3]) + start = float(clip_info[4]) + end = float(clip_info[5]) + filename = clip_info[6] + if len(clip_info) > 7: + filename += clip_info[7] + return filename, title, audio_track, subtitle_track, start, end + + def add_optical_clip(self, optical): + full_list = self.get_file_list() + full_list.append(optical) + self.load_list([optical]) + Settings().setValue(self.settings_section + '/media files', self.get_file_list()) diff --git a/resources/images/media_optical.png b/resources/images/media_optical.png new file mode 100644 index 0000000000000000000000000000000000000000..033b9847299474a3e3d6f6076a5c1b874b070705 GIT binary patch literal 2056 zcmV+j2>17iP)Px#32;bRa{vGa>;M1;>;WEiI5hwO00(qQO+^RU1qTr>5T}NKfdBvi24YJ`L;(K) z{{a7>y{D4^00)RkL_t(|+SQg>ZyVK{1&_RH!W@79Q@Dj!LN=z{lA==;c}eK2T@Ex5;JNvnnf&@Lo&I5 zOs0s1g^F4#tstJr(lzqJjNbe!cCXH8{m%t_+HwDTBctY&(-nnZh^fI~9N{ntkTU!5 zL}CHybP>5+8HGXxi;GoMDyvvsU03V%2Bc^_Gcm3E{+XZv_K&xFhtG{@{otK3)$LW_ z54;J!jHRU&tgO_rw${ML#wND6chuF@8f?7t1NO|8mX_0rcrtSDw$o*U!7ix=dmJ7i zfk2SK(VYQ1iI=){k7yrw9g(YDly|jPMAG z0a!?YAm$KNUJtL;)(_x2JG+PATU$GHeoL)XmNBb0zr)SDju$c4KUR@=y($G~5RI51 z%XY*RH$+*2J&=c-C?cIb4!(i)^-Tt0aDpC!)9#SCC&QtNC1k%8^4pUm!76G#Z;KrTtj= zT8+WiP^~i2HFc@7yn6`#`R#|_antT2t`KB*7;&$Nls}A|Cy1iYjbf5Vspvte6oC*? z5Q-*h)#}Q#&CShr+If4ew)U)8EY{Z6)(+r}Mib65#cWB}QU+EbhGXcy-}mhU0sRBx z7b2k~A^`=lpn^mwhBQS^jG+*TVks0xHSR?&X{AhvK>&ZJxI}TC;u^&#>|CX?NIhS}mqcTfM-0|C7c2Yy&Cudoy|C_x%5n4j71*-U5kr{CLZp|D8STFf^Zjk^Fo zqWGAmT(1<8^L}>_2|1|7V;tnD0ilo{Ucm$>=YrkngGJ|oVaA222^&7S^7-3`reQ8u z*rl|q^j2dvKMZfaQfZXcb-Pc7 z-y>4E;PX-jyc2M`#$e;0!aO$y{j?c3KkNCk=C~y*iFGQ0dLq0+@KW`t&n1EfXvlaX z@Hs+o+5B+YdCc=O;2qDX{!GBIIyBtX&>XwIb%U~C`e}WA<4o}8cqN|;5`3;OTwDN- zIWIV)3l^gl7V{*`<`HOT`|-t>SKib7;n`7sb0)a2sZh@XZw?s``P@Npix)P73v;s$ z7^m&fO`D*fouJOYbyIVi;4xND$XLVdvJ8Q)hHQ4C5zB)8KCEn?I(sAkXjdwNw mt97oc>y6j^;9S?4p7}2y|J`56Sngr~0000media_stop.png media_audio.png media_video.png + media_optical.png slidecontroller_multimedia.png auto-start_active.png auto-start_inactive.png From 67acb9d8247d5dc48d4aacbda47d0438e7c8c4d8 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 4 Mar 2014 22:33:50 +0000 Subject: [PATCH 03/66] Implemented DVD clip selection and playback, still needs a lot of cleanups. --- openlp/core/lib/serviceitem.py | 5 +- openlp/core/ui/media/__init__.py | 3 + openlp/core/ui/media/mediacontroller.py | 74 ++++++++++++++++++- openlp/core/ui/media/vlcplayer.py | 30 +++++++- .../media/forms/mediaclipselectorform.py | 41 +++++++--- openlp/plugins/media/lib/mediaitem.py | 71 ++++++++++++------ 6 files changed, 181 insertions(+), 43 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 738e23464..1849c3dca 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -108,6 +108,8 @@ class ItemCapabilities(object): ``CanAutoStartForLive`` The capability to ignore the do not play if display blank flag. + ``IsOptical`` + .Determines is the service_item is based on an optical device """ CanPreview = 1 CanEdit = 2 @@ -125,6 +127,7 @@ class ItemCapabilities(object): CanWordSplit = 14 HasBackgroundAudio = 15 CanAutoStartForLive = 16 + IsOptical = 17 class ServiceItem(object): @@ -573,7 +576,7 @@ class ServiceItem(object): frame = self._raw_frames[row] except IndexError: return '' - if self.is_image(): + if self.is_image() or self.is_capable(ItemCapabilities.IsOptical): path_from = frame['path'] else: path_from = os.path.join(frame['path'], frame['title']) diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 31a27b620..4c6ede2e8 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -72,6 +72,9 @@ class MediaInfo(object): length = 0 start_time = 0 end_time = 0 + title_track = 0 + audio_track = 0 + subtitle_track = 0 media_type = MediaType() diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 50db35602..a886ac1b3 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -36,7 +36,7 @@ import datetime from PyQt4 import QtCore, QtGui from openlp.core.common import Registry, Settings, UiStrings, translate -from openlp.core.lib import OpenLPToolbar +from openlp.core.lib import OpenLPToolbar, ItemCapabilities from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players from openlp.core.ui.media.mediaplayer import MediaPlayer @@ -387,7 +387,15 @@ class MediaController(object): controller.media_info.file_info = QtCore.QFileInfo(service_item.get_frame_path()) display = self._define_display(controller) if controller.is_live: - is_valid = self._check_file_type(controller, display, service_item) + # if this is an optical device use special handling + if service_item.is_capable(ItemCapabilities.IsOptical): + log.debug('video is optical and live') + path = service_item.get_frame_path() + (name, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(path) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) + else : + log.debug('video is not optical and live') + is_valid = self._check_file_type(controller, display, service_item) display.override['theme'] = '' display.override['video'] = True if controller.media_info.is_background: @@ -398,12 +406,20 @@ class MediaController(object): controller.media_info.start_time = service_item.start_time controller.media_info.end_time = service_item.end_time elif controller.preview_display: - is_valid = self._check_file_type(controller, display, service_item) + if service_item.is_capable(ItemCapabilities.IsOptical): + log.debug('video is optical and preview') + path = service_item.get_frame_path() + (name, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(path) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) + else : + log.debug('video is not optical and preview') + is_valid = self._check_file_type(controller, display, service_item) if not is_valid: # Media could not be loaded correctly critical_error_message_box(translate('MediaPlugin.MediaItem', 'Unsupported File'), translate('MediaPlugin.MediaItem', 'Unsupported File')) return False + log.debug('video mediatype: ' +str(controller.media_info.media_type)) # dont care about actual theme, set a black background if controller.is_live and not controller.media_info.is_background: display.frame.evaluateJavaScript('show_video( "setBackBoard", null, null, null,"visible");') @@ -456,6 +472,44 @@ class MediaController(object): log.debug('use %s controller' % self.current_media_players[controller.controller_type]) return True + def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller): + log.debug('media_setup_optical') + if controller is None: + controller = self.display_controllers[DisplayControllerType.Plugin] + # stop running videos + self.media_reset(controller) + # Setup media info + controller.media_info = MediaInfo() + #controller.media_info.volume = 0 + controller.media_info.file_info = QtCore.QFileInfo(filename) + controller.media_info.media_type = MediaType.DVD + controller.media_info.start_time = start/1000 + controller.media_info.end_time = end/1000 + controller.media_info.length = (end - start)/1000 + controller.media_info.title_track = title + controller.media_info.audio_track = audio_track + controller.media_info.subtitle_track = subtitle_track + # When called from mediaitem display is None + if display is None: + display = controller.preview_display + # Find vlc player + used_players = get_media_players()[0] + vlc_player = None + for title in used_players: + player = self.media_players[title] + if player.name == 'vlc': + vlc_player = player + if vlc_player is None: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'VLC player required'), + translate('MediaPlugin.MediaItem', 'VLC player required for playback of optical devices')) + return False + vlc_player.load(display) + self.resize(display, vlc_player) + self.current_media_players[controller.controller_type] = vlc_player + controller.media_info.media_type = MediaType.DVD + return True + + def _check_file_type(self, controller, display, service_item): """ Select the correct media Player type from the prioritized Player list @@ -758,3 +812,17 @@ class MediaController(object): return self._live_controller live_controller = property(_get_live_controller) + + @staticmethod + def parse_optical_path(input): + # split the clip info + clip_info = input.split(sep=':') + title = int(clip_info[1]) + audio_track = int(clip_info[2]) + subtitle_track = int(clip_info[3]) + start = float(clip_info[4]) + end = float(clip_info[5]) + filename = clip_info[6] + if len(clip_info) > 7: + filename += clip_info[7] + return filename, title, audio_track, subtitle_track, start, end diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index bf6374473..4e1654c31 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -39,7 +39,7 @@ from PyQt4 import QtGui from openlp.core.common import Settings from openlp.core.lib import translate -from openlp.core.ui.media import MediaState +from openlp.core.ui.media import MediaState, MediaType from openlp.core.ui.media.mediaplayer import MediaPlayer log = logging.getLogger(__name__) @@ -205,14 +205,35 @@ class VlcPlayer(MediaPlayer): """ controller = display.controller start_time = 0 - if self.state != MediaState.Paused and controller.media_info.start_time > 0: - start_time = controller.media_info.start_time + log.debug('vlc play') display.vlcMediaPlayer.play() if not self.media_state_wait(display, vlc.State.Playing): return False + if self.state != MediaState.Paused and controller.media_info.start_time > 0: + log.debug('vlc play, starttime set') + start_time = controller.media_info.start_time + log.debug('mediatype: ' +str(controller.media_info.media_type)) + # Set tracks for the optical device + if controller.media_info.media_type == MediaType.DVD: + log.debug('vlc play, playing started') + if controller.media_info.title_track > 0: + log.debug('vlc play, title_track set: ' + str(controller.media_info.title_track)) + display.vlcMediaPlayer.set_title(controller.media_info.title_track) + display.vlcMediaPlayer.play() + if not self.media_state_wait(display, vlc.State.Playing): + return False + if controller.media_info.audio_track > 0: + display.vlcMediaPlayer.audio_set_track(controller.media_info.audio_track) + log.debug('vlc play, audio_track set: ' + str(controller.media_info.audio_track)) + if controller.media_info.subtitle_track > 0: + display.vlcMediaPlayer.video_set_spu(controller.media_info.subtitle_track) + log.debug('vlc play, subtitle_track set: ' + str(controller.media_info.subtitle_track)) + if controller.media_info.start_time > 0: + log.debug('vlc play, starttime set: ' + str(controller.media_info.start_time)) + start_time = controller.media_info.start_time self.volume(display, controller.media_info.volume) if start_time > 0: - self.seek(display, controller.media_info.start_time * 1000) + self.seek(display, int(start_time * 1000)) controller.media_info.length = int(display.vlcMediaPlayer.get_media().get_duration() / 1000) controller.seek_slider.setMaximum(controller.media_info.length * 1000) self.state = MediaState.Playing @@ -248,6 +269,7 @@ class VlcPlayer(MediaPlayer): Go to a particular position """ if display.vlcMediaPlayer.is_seekable(): + log.debug('vlc seek to: ' + str(seek_value)) display.vlcMediaPlayer.set_time(seek_value) def reset(self, display): diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 688d879d4..115e6690b 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -31,6 +31,8 @@ import os import sys import logging import time +from datetime import datetime + from PyQt4 import QtCore, QtGui @@ -132,12 +134,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.toggle_disable_load_media(False) return self.vlc_media_player.audio_set_mute(True) - while self.vlc_media_player.get_time() == 0: - if self.vlc_media_player.get_state() == vlc.State.Error: - log.debug('player in error state') - self.toggle_disable_load_media(False) - return - time.sleep(0.1) + #while self.vlc_media_player.get_time() == 0: + # if self.vlc_media_player.get_state() == vlc.State.Error: + # log.debug('player in error state') + # self.toggle_disable_load_media(False) + # return + # time.sleep(0.1) + if not self.media_state_wait(vlc.State.Playing): + return self.vlc_media_player.pause() self.vlc_media_player.set_time(0) # Get titles, insert in combobox @@ -223,11 +227,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.set_time(0) self.vlc_media_player.play() self.vlc_media_player.audio_set_mute(True) - while self.vlc_media_player.get_time() == 0: - if self.vlc_media_player.get_state() == vlc.State.Error: - log.debug('player in error state') - return - time.sleep(0.1) + #while self.vlc_media_player.get_time() == 0: + # if self.vlc_media_player.get_state() == vlc.State.Error: + # log.debug('player in error state') + # return + # time.sleep(0.1) + if not self.media_state_wait(vlc.State.Playing): + return # pause self.vlc_media_player.pause() self.vlc_media_player.set_time(0) @@ -358,3 +364,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): path = self.media_path_combobox.currentText() optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str(start_time_ms) + ':' + str(end_time_ms) + ':' + path self.media_item.add_optical_clip(optical) + + def media_state_wait(self, media_state): + """ + Wait for the video to change its state + Wait no longer than 15 seconds. (loading an iso file needs a long time) + """ + start = datetime.now() + while not media_state == self.vlc_media.get_state(): + if self.vlc_media.get_state() == vlc.State.Error: + return False + if (datetime.now() - start).seconds > 15: + return False + return True diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 33b983dad..80e96f08b 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -33,7 +33,7 @@ import os from PyQt4 import QtCore, QtGui from openlp.core.common import Registry, AppLocation, Settings, check_directory_exists, UiStrings, translate -from openlp.core.lib import ItemCapabilities, MediaManagerItem,MediaType, ServiceItem, ServiceItemContext, \ +from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, ServiceItem, ServiceItemContext, \ build_icon, check_item_selected from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType @@ -43,7 +43,6 @@ from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorFo from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE - log = logging.getLogger(__name__) @@ -193,22 +192,45 @@ class MediaMediaItem(MediaManagerItem): if item is None: return False filename = item.data(QtCore.Qt.UserRole) - if not os.path.exists(filename): - if not remote: - # File is no longer present - critical_error_message_box( - translate('MediaPlugin.MediaItem', 'Missing Media File'), - translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename) - return False - (path, name) = os.path.split(filename) - service_item.title = name - service_item.processor = self.display_type_combo_box.currentText() - service_item.add_from_command(path, name, CLAPPERBOARD) - # Only get start and end times if going to a service - if context == ServiceItemContext.Service: - # Start media and obtain the length - if not self.media_controller.media_length(service_item): + log.debug('generate_slide_data, filename: ' + filename) + if filename.startswith('optical:'): + (name, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(filename) + log.debug('generate_slide_data, optical name: ' + name) + if not os.path.exists(name): + if not remote: + # Optical disc is no longer present + critical_error_message_box( + translate('MediaPlugin.MediaItem', 'Missing Media File'), + translate('MediaPlugin.MediaItem', 'The optical disc %s is no longer available.') % name) return False + service_item.title = name + service_item.processor = self.display_type_combo_box.currentText() + service_item.add_from_command(filename, name, OPTICAL_ICON) + # Only set start and end times if going to a service + #if context == ServiceItemContext.Service: + # Set the length + self.media_controller.media_setup_optical(name, title, audio_track, subtitle_track, start, end, None, None) + service_item.set_media_length((end - start)/1000) + service_item.start_time = start/1000 + service_item.end_time = end/1000 + service_item.add_capability(ItemCapabilities.IsOptical) + else: + if not os.path.exists(filename): + if not remote: + # File is no longer present + critical_error_message_box( + translate('MediaPlugin.MediaItem', 'Missing Media File'), + translate('MediaPlugin.MediaItem', 'The file %s no longer exists.') % filename) + return False + (path, name) = os.path.split(filename) + service_item.title = name + service_item.processor = self.display_type_combo_box.currentText() + service_item.add_from_command(path, name, CLAPPERBOARD) + # Only get start and end times if going to a service + if context == ServiceItemContext.Service: + # Start media and obtain the length + if not self.media_controller.media_length(service_item): + return False service_item.add_capability(ItemCapabilities.CanAutoStartForLive) service_item.add_capability(ItemCapabilities.RequiresMedia) if Settings().value(self.settings_section + '/media auto start') == QtCore.Qt.Checked: @@ -329,7 +351,14 @@ class MediaMediaItem(MediaManagerItem): log.debug('in on_load_optical') self.media_clip_selector_form.exec_() - def parse_optical_path(self, input): + def add_optical_clip(self, optical): + full_list = self.get_file_list() + full_list.append(optical) + self.load_list([optical]) + Settings().setValue(self.settings_section + '/media files', self.get_file_list()) + + @staticmethod + def parse_optical_path(input): # split the clip info clip_info = input.split(sep=':') title = int(clip_info[1]) @@ -341,9 +370,3 @@ class MediaMediaItem(MediaManagerItem): if len(clip_info) > 7: filename += clip_info[7] return filename, title, audio_track, subtitle_track, start, end - - def add_optical_clip(self, optical): - full_list = self.get_file_list() - full_list.append(optical) - self.load_list([optical]) - Settings().setValue(self.settings_section + '/media files', self.get_file_list()) From 3988aacfbd7d12c50d20ea42b8685c9c80986719 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 8 Mar 2014 21:14:04 +0000 Subject: [PATCH 04/66] Made it possible to save and load optical clip to servicefile. Also some pep8 and general cleanup. --- openlp/core/lib/serviceitem.py | 29 +++-- openlp/core/ui/media/__init__.py | 25 +++- openlp/core/ui/media/mediacontroller.py | 19 +-- .../media/forms/mediaclipselectorform.py | 62 +++++++--- openlp/plugins/media/lib/mediaitem.py | 114 +++++++++++++----- 5 files changed, 167 insertions(+), 82 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index cb03d981f..694bb139f 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -435,7 +435,10 @@ class ServiceItem(object): for text_image in serviceitem['serviceitem']['data']: if not self.title: self.title = text_image['title'] - if path: + if self.is_capable(ItemCapabilities.IsOptical): + self.has_original_files = False + self.add_from_command(text_image['path'], text_image['title'], text_image['image']) + elif path: self.has_original_files = False self.add_from_command(path, text_image['title'], text_image['image']) else: @@ -446,7 +449,7 @@ class ServiceItem(object): """ Returns the title of the service item. """ - if self.is_text(): + if self.is_text() or self.is_capable(ItemCapabilities.IsOptical): return self.title else: if len(self._raw_frames) > 1: @@ -516,7 +519,8 @@ class ServiceItem(object): """ Confirms if the ServiceItem uses a file """ - return self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command + return self.service_item_type == ServiceItemType.Image or \ + (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical)) def is_text(self): """ @@ -646,15 +650,20 @@ class ServiceItem(object): self.is_valid = False break elif self.is_command(): - file_name = os.path.join(frame['path'], frame['title']) - if not os.path.exists(file_name): - self.is_valid = False - break - if suffix_list and not self.is_text(): - file_suffix = frame['title'].split('.')[-1] - if file_suffix.lower() not in suffix_list: + if self.is_capable(ItemCapabilities.IsOptical): + if not os.path.exists(frame['title']): self.is_valid = False break + else: + file_name = os.path.join(frame['path'], frame['title']) + if not os.path.exists(file_name): + self.is_valid = False + break + if suffix_list and not self.is_text(): + file_suffix = frame['title'].split('.')[-1] + if file_suffix.lower() not in suffix_list: + self.is_valid = False + break def _get_renderer(self): """ diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index aa2819450..8223ed694 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -102,11 +102,8 @@ def set_media_players(players_list, overridden_player='auto'): This method saves the configured media players and overridden player to the settings - ``players_list`` - A list with all active media players. - - ``overridden_player`` - Here an special media player is chosen for all media actions. + :param players_list: A list with all active media players. + :param overridden_player: Here an special media player is chosen for all media actions. """ log.debug('set_media_players') players = ','.join(players_list) @@ -114,6 +111,24 @@ def set_media_players(players_list, overridden_player='auto'): players = players.replace(overridden_player, '[%s]' % overridden_player) Settings().setValue('media/players', players) +def parse_optical_path(input): + """ + Split the optical path info. + + :param input: The string to parse + :return: The elements extracted from the string: filename, title, audio_track, subtitle_track, start, end + """ + clip_info = input.split(sep=':') + title = int(clip_info[1]) + audio_track = int(clip_info[2]) + subtitle_track = int(clip_info[3]) + start = float(clip_info[4]) + end = float(clip_info[5]) + filename = clip_info[6] + if len(clip_info) > 7: + filename += clip_info[7] + return filename, title, audio_track, subtitle_track, start, end + from .mediacontroller import MediaController from .playertab import PlayerTab diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 26e153503..8f782ce24 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -38,7 +38,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import Registry, RegistryMixin, Settings, UiStrings, translate from openlp.core.lib import OpenLPToolbar, ItemCapabilities from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players +from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players, parse_optical_path from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.common import AppLocation from openlp.core.ui import DisplayControllerType @@ -397,7 +397,7 @@ class MediaController(object): if service_item.is_capable(ItemCapabilities.IsOptical): log.debug('video is optical and live') path = service_item.get_frame_path() - (name, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(path) + (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path) is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) else : log.debug('video is not optical and live') @@ -415,7 +415,7 @@ class MediaController(object): if service_item.is_capable(ItemCapabilities.IsOptical): log.debug('video is optical and preview') path = service_item.get_frame_path() - (name, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(path) + (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path) is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) else : log.debug('video is not optical and preview') @@ -819,16 +819,3 @@ class MediaController(object): live_controller = property(_get_live_controller) - @staticmethod - def parse_optical_path(input): - # split the clip info - clip_info = input.split(sep=':') - title = int(clip_info[1]) - audio_track = int(clip_info[2]) - subtitle_track = int(clip_info[3]) - start = float(clip_info[4]) - end = float(clip_info[5]) - filename = clip_info[6] - if len(clip_info) > 7: - filename += clip_info[7] - return filename, title, audio_track, subtitle_track, start, end diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 115e6690b..34a228136 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -67,7 +67,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ Exit Dialog and do not save """ - log.debug ('MediaClipSelectorForm.reject') + log.debug('MediaClipSelectorForm.reject') self.vlc_media_player.stop() QtGui.QDialog.reject(self) @@ -110,6 +110,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_load_disc_pushbutton_clicked(self, clicked): """ Load the media when the load-button has been clicked + + :param clicked: Given from signal, not used. """ self.disable_all() path = self.media_path_combobox.currentText() @@ -134,12 +136,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.toggle_disable_load_media(False) return self.vlc_media_player.audio_set_mute(True) - #while self.vlc_media_player.get_time() == 0: - # if self.vlc_media_player.get_state() == vlc.State.Error: - # log.debug('player in error state') - # self.toggle_disable_load_media(False) - # return - # time.sleep(0.1) if not self.media_state_wait(vlc.State.Playing): return self.vlc_media_player.pause() @@ -163,6 +159,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_pause_pushbutton_clicked(self, clicked): """ Pause the playback + + :param clicked: Given from signal, not used. """ self.vlc_media_player.pause() @@ -170,6 +168,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_play_pushbutton_clicked(self, clicked): """ Start the playback + + :param clicked: Given from signal, not used. """ self.vlc_media_player.play() @@ -177,26 +177,40 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_set_start_pushbutton_clicked(self, clicked): """ Copy the current player position to start_timeedit + + :param clicked: Given from signal, not used. """ vlc_ms_pos = self.vlc_media_player.get_time() time = QtCore.QTime() new_pos_time = time.addMSecs(vlc_ms_pos) self.start_timeedit.setTime(new_pos_time) + # If start time is after end time, update end time. + end_time = self.end_timeedit.time() + if end_time < new_pos_time: + self.end_timeedit.setTime(new_pos_time) @QtCore.pyqtSlot(bool) def on_set_end_pushbutton_clicked(self, clicked): """ Copy the current player position to end_timeedit + + :param clicked: Given from signal, not used. """ vlc_ms_pos = self.vlc_media_player.get_time() time = QtCore.QTime() new_pos_time = time.addMSecs(vlc_ms_pos) self.end_timeedit.setTime(new_pos_time) + # If start time is after end time, update end time. + start_time = self.start_timeedit.time() + if start_time > new_pos_time: + self.start_timeedit.setTime(new_pos_time) @QtCore.pyqtSlot(bool) def on_jump_end_pushbutton_clicked(self, clicked): """ Set the player position to the position stored in end_timeedit + + :param clicked: Given from signal, not used. """ end_time = self.end_timeedit.time() end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ @@ -209,6 +223,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_jump_start_pushbutton_clicked(self, clicked): """ Set the player position to the position stored in start_timeedit + + :param clicked: Given from signal, not used. """ start_time = self.start_timeedit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ @@ -221,17 +237,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_title_combo_box_currentIndexChanged(self, index): """ When a new title is chosen, it is loaded by VLC and info about audio and subtitle tracks is reloaded + + :param index: The index of the newly chosen title track. """ log.debug('in on_title_combo_box_changed, index: ', str(index)) self.vlc_media_player.set_title(index) self.vlc_media_player.set_time(0) self.vlc_media_player.play() self.vlc_media_player.audio_set_mute(True) - #while self.vlc_media_player.get_time() == 0: - # if self.vlc_media_player.get_state() == vlc.State.Error: - # log.debug('player in error state') - # return - # time.sleep(0.1) if not self.media_state_wait(vlc.State.Playing): return # pause @@ -241,7 +254,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): audio_tracks = self.vlc_media_player.audio_get_track_description() self.audio_tracks_combobox.clear() for audio_track in audio_tracks: - self.audio_tracks_combobox.addItem(audio_track[1].decode(),audio_track[0]) + self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) # Enable audio track combobox if anything is in it if len(audio_tracks) > 0: self.audio_tracks_combobox.setDisabled(False) @@ -270,6 +283,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_audio_tracks_combobox_currentIndexChanged(self, index): """ When a new audio track is chosen update audio track bing played by VLC + + :param index: The index of the newly chosen audio track. """ audio_track = self.audio_tracks_combobox.itemData(index) log.debug('in on_audio_tracks_combobox_currentIndexChanged, index: ', str(index), ' audio_track: ', audio_track) @@ -280,15 +295,18 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def on_subtitle_tracks_combobox_currentIndexChanged(self, index): """ When a new subtitle track is chosen update subtitle track bing played by VLC + + :param index: The index of the newly chosen subtitle. """ subtitle_track = self.subtitle_tracks_combobox.itemData(index) - log.debug('in on_subtitle_tracks_combobox_currentIndexChanged, index: ', str(index), ' subtitle_track: ', subtitle_track) if subtitle_track: self.vlc_media_player.video_set_spu(int(subtitle_track)) def on_position_horizontalslider_sliderMoved(self, position): """ Set player position according to new slider position. + + :param position: Position to seek to. """ self.vlc_media_player.set_time(position) @@ -318,16 +336,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ Enable/disable load media combobox and button. - @param action: If True elements are disabled, if False they are enabled. + :param action: If True elements are disabled, if False they are enabled. """ self.media_path_combobox.setDisabled(action) self.load_disc_pushbutton.setDisabled(action) def toggle_disable_player(self, action): """ - Enable/disable player elementa. + Enable/disable player elements. - @param action: If True elements are disabled, if False they are enabled. + :param action: If True elements are disabled, if False they are enabled. """ self.play_pushbutton.setDisabled(action) self.pause_pushbutton.setDisabled(action) @@ -343,9 +361,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.save_pushbutton.setDisabled(action) @QtCore.pyqtSlot(bool) - def on_save_pushbutton_clicked(self, checked): + def on_save_pushbutton_clicked(self, clicked): """ Saves the current media and trackinfo as a clip to the mediamanager + + :param clicked: Given from signal, not used. """ log.debug('in on_save_pushbutton_clicked') start_time = self.start_timeedit.time() @@ -362,13 +382,17 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) path = self.media_path_combobox.currentText() - optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str(start_time_ms) + ':' + str(end_time_ms) + ':' + path + optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str( + start_time_ms) + ':' + str(end_time_ms) + ':' + path self.media_item.add_optical_clip(optical) def media_state_wait(self, media_state): """ Wait for the video to change its state Wait no longer than 15 seconds. (loading an iso file needs a long time) + + :param media_state: VLC media state to wait for. + :return: True if state was reached within 15 seconds, False if not or error occurred. """ start = datetime.now() while not media_state == self.vlc_media.get_state(): diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 7f06a392f..86d6d38ac 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -29,6 +29,7 @@ import logging import os +from datetime import time from PyQt4 import QtCore, QtGui @@ -37,7 +38,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi build_icon, check_item_selected from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType -from openlp.core.ui.media import get_media_players, set_media_players +from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path from openlp.core.utils import get_locale_key from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE @@ -47,9 +48,10 @@ log = logging.getLogger(__name__) CLAPPERBOARD = ':/media/slidecontroller_multimedia.png' +OPTICAL = ':/media/media_optical.png' VIDEO_ICON = build_icon(':/media/media_video.png') AUDIO_ICON = build_icon(':/media/media_audio.png') -OPTICAL_ICON = build_icon(':/media/media_optical.png') +OPTICAL_ICON = build_icon(OPTICAL) ERROR_ICON = build_icon(':/general/general_delete.png') @@ -89,6 +91,10 @@ class MediaMediaItem(MediaManagerItem): self.list_view.activateDnD() def retranslateUi(self): + """ + This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem`` + to another language. + """ self.on_new_prompt = translate('MediaPlugin.MediaItem', 'Select Media') self.replace_action.setText(UiStrings().ReplaceBG) self.replace_action.setToolTip(UiStrings().ReplaceLiveBG) @@ -108,16 +114,25 @@ class MediaMediaItem(MediaManagerItem): self.has_edit_icon = False def add_list_view_to_toolbar(self): + """ + Creates the main widget for listing items. + """ MediaManagerItem.add_list_view_to_toolbar(self) self.list_view.addAction(self.replace_action) def add_start_header_bar(self): + """ + Adds buttons to the start of the header bar. + """ self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text='Load optical disc', tooltip='Load optical disc', triggers=self.on_load_optical) if not VLC_AVAILABLE: self.load_optical.setDisabled(True) def add_end_header_bar(self): + """ + Adds buttons to the end of the header bar. + """ # Replace backgrounds do not work at present so remove functionality. self.replace_action = self.toolbar.add_toolbar_action('replace_action', icon=':/slides/slide_blank.png', triggers=self.on_replace_click) @@ -139,6 +154,11 @@ class MediaMediaItem(MediaManagerItem): self.display_type_combo_box.currentIndexChanged.connect(self.override_player_changed) def override_player_changed(self, index): + """ + Change to the selected override media player + + :param index: Index of the new selected player. + """ player = get_media_players()[0] if index == 0: set_media_players(player) @@ -163,7 +183,7 @@ class MediaMediaItem(MediaManagerItem): Called to replace Live background with the media selected. """ if check_item_selected(self.list_view, - translate('MediaPlugin.MediaItem', + translate('MediaPlugin.MediaItem', 'You must select a media file to replace the background with.')): item = self.list_view.currentItem() filename = item.data(QtCore.Qt.UserRole) @@ -172,12 +192,12 @@ class MediaMediaItem(MediaManagerItem): service_item.title = 'webkit' service_item.processor = 'webkit' (path, name) = os.path.split(filename) - service_item.add_from_command(path, name,CLAPPERBOARD) + service_item.add_from_command(path, name, CLAPPERBOARD) if self.media_controller.video(DisplayControllerType.Live, service_item, video_behind_text=True): self.reset_action.setVisible(True) else: critical_error_message_box(UiStrings().LiveBGError, - translate('MediaPlugin.MediaItem', + translate('MediaPlugin.MediaItem', 'There was no display item to amend.')) else: critical_error_message_box(UiStrings().LiveBGError, @@ -195,10 +215,9 @@ class MediaMediaItem(MediaManagerItem): if item is None: return False filename = item.data(QtCore.Qt.UserRole) - log.debug('generate_slide_data, filename: ' + filename) + # Special handling if the filename is a optical clip if filename.startswith('optical:'): - (name, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(filename) - log.debug('generate_slide_data, optical name: ' + name) + (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(filename) if not os.path.exists(name): if not remote: # Optical disc is no longer present @@ -206,16 +225,16 @@ class MediaMediaItem(MediaManagerItem): translate('MediaPlugin.MediaItem', 'Missing Media File'), translate('MediaPlugin.MediaItem', 'The optical disc %s is no longer available.') % name) return False - service_item.title = name service_item.processor = self.display_type_combo_box.currentText() - service_item.add_from_command(filename, name, OPTICAL_ICON) + service_item.add_from_command(filename, name, CLAPPERBOARD) + service_item.title = name + '@' + self.format_milliseconds(start) + '-' + self.format_milliseconds(end) # Only set start and end times if going to a service #if context == ServiceItemContext.Service: # Set the length self.media_controller.media_setup_optical(name, title, audio_track, subtitle_track, start, end, None, None) - service_item.set_media_length((end - start)/1000) - service_item.start_time = start/1000 - service_item.end_time = end/1000 + service_item.set_media_length((end - start) / 1000) + service_item.start_time = start / 1000 + service_item.end_time = end / 1000 service_item.add_capability(ItemCapabilities.IsOptical) else: if not os.path.exists(filename): @@ -243,12 +262,15 @@ class MediaMediaItem(MediaManagerItem): return True def initialise(self): + """ + Initialize media item. + """ self.list_view.clear() self.list_view.setIconSize(QtCore.QSize(88, 50)) self.service_path = os.path.join(AppLocation.get_section_data_path(self.settings_section), 'thumbnails') check_directory_exists(self.service_path) self.load_list(Settings().value(self.settings_section + '/media files')) - self.populate_display_types() + self.rebuild_players() self.media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) def rebuild_players(self): @@ -261,6 +283,9 @@ class MediaMediaItem(MediaManagerItem): ' '.join(self.media_controller.audio_extensions_list), UiStrings().AllFiles) def display_setup(self): + """ + Setup media controller display. + """ self.media_controller.setup_display(self.display_controller.preview_display, False) def populate_display_types(self): @@ -300,24 +325,32 @@ class MediaMediaItem(MediaManagerItem): Settings().setValue(self.settings_section + '/media files', self.get_file_list()) def load_list(self, media, target_group=None): - # Sort the media by its filename considering language specific characters. + """ + Sort the media by its filename considering language specific characters. + + :param media: List if media to sort and list. + :param target_group: Not used in media. + """ media.sort(key=lambda file_name: get_locale_key(os.path.split(str(file_name))[1])) for track in media: track_info = QtCore.QFileInfo(track) if track.startswith('optical:'): - (file_name, title, audio_track, subtitle_track, start, end) = self.parse_optical_path(track) - optical = file_name + '@' + str(title) + ':' + str(start) + '-' + str(end) + # Handle optical based item + (file_name, title, audio_track, subtitle_track, start, end) = parse_optical_path(track) + optical = file_name + '@' + self.format_milliseconds(start) + '-' + self.format_milliseconds(end) item_name = QtGui.QListWidgetItem(optical) - item_name.setIcon(build_icon(OPTICAL_ICON)) + item_name.setIcon(OPTICAL_ICON) item_name.setData(QtCore.Qt.UserRole, track) item_name.setToolTip(optical) elif not os.path.exists(track): + # File doesn't exist, mark as error. file_name = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(file_name) item_name.setIcon(ERROR_ICON) item_name.setData(QtCore.Qt.UserRole, track) item_name.setToolTip(track) elif track_info.isFile(): + # Normal media file handling. file_name = os.path.split(str(track))[1] item_name = QtGui.QListWidgetItem(file_name) if '*.%s' % (file_name.split('.')[-1].lower()) in self.media_controller.audio_extensions_list: @@ -329,6 +362,12 @@ class MediaMediaItem(MediaManagerItem): self.list_view.addItem(item_name) def get_list(self, type=MediaType.Audio): + """ + Get the list of media, optional select media type. + + :param type: Type to get, defaults to audio. + :return: The media list + """ media = Settings().value(self.settings_section + '/media files') media.sort(key=lambda filename: get_locale_key(os.path.split(str(filename))[1])) extension = [] @@ -341,6 +380,13 @@ class MediaMediaItem(MediaManagerItem): return media def search(self, string, show_error): + """ + Performs a search for items containing ``string`` + + :param string: String to be displayed + :param show_error: Should the error be shown (True) + :return: The search result. + """ files = Settings().value(self.settings_section + '/media files') results = [] string = string.lower() @@ -351,25 +397,29 @@ class MediaMediaItem(MediaManagerItem): return results def on_load_optical(self): - log.debug('in on_load_optical') + """ + When the load optical button is clicked, open the clip selector window. + """ self.media_clip_selector_form.exec_() def add_optical_clip(self, optical): + """ + Add a optical based clip to the mediamanager, called from media_clip_selector_form. + + :param optical: The clip to add. + """ full_list = self.get_file_list() full_list.append(optical) self.load_list([optical]) Settings().setValue(self.settings_section + '/media files', self.get_file_list()) - @staticmethod - def parse_optical_path(input): - # split the clip info - clip_info = input.split(sep=':') - title = int(clip_info[1]) - audio_track = int(clip_info[2]) - subtitle_track = int(clip_info[3]) - start = float(clip_info[4]) - end = float(clip_info[5]) - filename = clip_info[6] - if len(clip_info) > 7: - filename += clip_info[7] - return filename, title, audio_track, subtitle_track, start, end + def format_milliseconds(self, milliseconds): + """ + Format milliseconds into a human readable time string. + :param milliseconds: Milliseconds to format + :return: Time string in format: hh.mm.ss,ttt + """ + seconds, millis = divmod(milliseconds, 1000) + minutes, seconds = divmod(millis, 60) + hours, minutes = divmod(minutes, 60) + return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis) From 51ea57f35e8053fe69f8e0f557f4254e2cc8351f Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 17 Mar 2014 21:59:58 +0100 Subject: [PATCH 05/66] Added a test for loading serviceitem with optical media item. --- .../openlp_core_lib/test_serviceitem.py | 21 +++++++++++++++++++ tests/resources/serviceitem-dvd.osj | 1 + 2 files changed, 22 insertions(+) create mode 100644 tests/resources/serviceitem-dvd.osj diff --git a/tests/functional/openlp_core_lib/test_serviceitem.py b/tests/functional/openlp_core_lib/test_serviceitem.py index d75e94040..4fd6f6b83 100644 --- a/tests/functional/openlp_core_lib/test_serviceitem.py +++ b/tests/functional/openlp_core_lib/test_serviceitem.py @@ -206,3 +206,24 @@ class TestServiceItem(TestCase): 'This service item should be able to be run in a can be made to Loop') self.assertTrue(service_item.is_capable(ItemCapabilities.CanAppend), 'This service item should be able to have new items added to it') + + def service_item_load_optical_media_from_service_test(self): + """ + Test the Service Item - load an optical media item + """ + # GIVEN: A new service item and a mocked add icon function + service_item = ServiceItem(None) + service_item.add_icon = MagicMock() + + # WHEN: We load a serviceitem with optical media + line = convert_file_service_item(TEST_PATH, 'serviceitem-dvd.osj') + with patch('openlp.core.ui.servicemanager.os.path.exists') as mocked_exists: + mocked_exists.return_value = True + service_item.set_from_service(line) + + # THEN: We should get back a valid service item with optical media info + self.assertTrue(service_item.is_valid, 'The service item should be valid') + self.assertTrue(service_item.is_capable(ItemCapabilities.IsOptical), 'The item should be Optical') + self.assertEqual(service_item.start_time, 654.375, 'Start time should be 654.375') + self.assertEqual(service_item.end_time, 672.069, 'End time should be 672.069') + self.assertEqual(service_item.media_length, 17.694, 'Media length should be 17.694') diff --git a/tests/resources/serviceitem-dvd.osj b/tests/resources/serviceitem-dvd.osj new file mode 100644 index 000000000..d3d224bff --- /dev/null +++ b/tests/resources/serviceitem-dvd.osj @@ -0,0 +1 @@ +[{"serviceitem": {"header": {"auto_play_slides_once": false, "data": "", "processor": "Automatic", "theme": -1, "theme_overwritten": false, "end_time": 672.069, "start_time": 654.375, "capabilities": [12, 17, 16, 4], "media_length": 17.694, "audit": "", "xml_version": null, "title": "/dev/sr0@00:06:15,375-00:01:09,069", "auto_play_slides_loop": false, "notes": "", "icon": ":/plugins/plugin_media.png", "type": 3, "background_audio": [], "plugin": "media", "from_plugin": false, "search": "", "will_auto_start": false, "name": "media", "footer": [], "timed_slide_interval": 0}, "data": [{"image": ":/media/slidecontroller_multimedia.png", "path": "optical:1:5:3:654375:672069:/dev/sr0", "title": "/dev/sr0"}]}}] From 3608ad4fcdddb8842034e31828af510d66dce3b1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 20 Mar 2014 22:52:53 +0100 Subject: [PATCH 06/66] Added first tests for mediaclipselectorform.py --- openlp/plugins/media/forms/__init__.py | 28 +++++ .../media/forms/mediaclipselectorform.py | 9 +- .../openlp_plugins/media/__init__.py | 0 .../media/forms/test_mediaclipselectorform.py | 102 ++++++++++++++++++ 4 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 openlp/plugins/media/forms/__init__.py create mode 100644 tests/interfaces/openlp_plugins/media/__init__.py create mode 100644 tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py diff --git a/openlp/plugins/media/forms/__init__.py b/openlp/plugins/media/forms/__init__.py new file mode 100644 index 000000000..6b241e7fc --- /dev/null +++ b/openlp/plugins/media/forms/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 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 # +############################################################################### diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 34a228136..d013cacd7 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -36,6 +36,7 @@ from datetime import datetime from PyQt4 import QtCore, QtGui +from openlp.core.common import translate from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box from openlp.core.ui.media.vendor import vlc @@ -117,13 +118,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): path = self.media_path_combobox.currentText() if path == '': log.debug('no given path') - critical_error_message_box('Error', 'No path was given') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) self.toggle_disable_load_media(False) return self.vlc_media = self.vlc_instance.media_new_path(path) if not self.vlc_media: log.debug('vlc media player is none') - critical_error_message_box('Error', 'An error happened during initialization of VLC player') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'An error happened during initialization of VLC player')) self.toggle_disable_load_media(False) return # put the media in the media player @@ -132,7 +134,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # start playback to get vlc to parse the media if self.vlc_media_player.play() < 0: log.debug('vlc play returned error') - critical_error_message_box('Error', 'An error happen when starting VLC player') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'An error happen when starting VLC player')) self.toggle_disable_load_media(False) return self.vlc_media_player.audio_set_mute(True) diff --git a/tests/interfaces/openlp_plugins/media/__init__.py b/tests/interfaces/openlp_plugins/media/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py new file mode 100644 index 000000000..b4dbf70cd --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 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 # +############################################################################### +""" +Module to test the MediaClipSelectorForm. +""" +from unittest import TestCase + +from PyQt4 import QtGui, QtTest, QtCore + +from openlp.core.common import Registry +from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm +from tests.interfaces import MagicMock, patch +from tests.helpers.testmixin import TestMixin + + +class TestMediaClipSelectorForm(TestCase, TestMixin): + """ + Test the EditCustomSlideForm. + """ + def setUp(self): + """ + Create the UI + """ + Registry.create() + self.get_application() + self.main_window = QtGui.QMainWindow() + Registry().register('main_window', self.main_window) + # Mock VLC so we don't actually use it + self.vlc_patcher = patch('openlp.plugins.media.forms.mediaclipselectorform.vlc') + self.vlc_patcher.start() + # Mock the media item + self.mock_media_item = MagicMock() + # Mock media_state_wait to avoid waiting for VLC playing state + #self.mock_media_state_wait = patch('openlp.plugins.media.forms.mediaclipselectorform.media_state_wait') + #self.mock_media_state_wait.return_value = True + #self.mock_media_state_wait.start() + # create form to test + self.form = MediaClipSelectorForm(self.mock_media_item, self.main_window, None) + mock_media_state_wait = MagicMock() + mock_media_state_wait.return_value = True + self.form.media_state_wait = mock_media_state_wait + + def tearDown(self): + """ + Delete all the C++ objects at the end so that we don't have a segfault + """ + #self.mock_media_state_wait.stop() + self.vlc_patcher.stop() + del self.form + del self.main_window + + def basic_test(self): + """ + Test if the dialog is correctly set up. + """ + # GIVEN: A mocked QDialog.exec_() method + with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + # WHEN: Show the dialog. + self.form.exec_() + + # THEN: The media path should be empty. + assert self.form.media_path_combobox.currentText() == '', 'There should not be any text in the media path.' + + def click_load_button_test(self): + """ + Test that the correct function is called when load is clicked + """ + # GIVEN: Mocked methods. + with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ + mocked_critical_error_message_box: + + # WHEN: The load button is clicked with no path set + QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) + + # THEN: we should get an error + mocked_critical_error_message_box.assert_called_with(message='No path was given') From 4b83741b36d3869db977d506917d2d9b356e1347 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 30 Mar 2014 20:11:39 +0200 Subject: [PATCH 07/66] Added autodetection of optical drives and some small fixes. --- .../media/forms/mediaclipselectorform.py | 80 +++++++++++++++---- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index d013cacd7..f1c25b378 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -28,7 +28,12 @@ ############################################################################### import os +if os.name == 'nt': + from ctypes import windll + import string import sys +if sys.platform.startswith('linux'): + import dbus import logging import time from datetime import datetime @@ -106,6 +111,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.update_position) self.timer.start(100) + self.find_optical_devices() @QtCore.pyqtSlot(bool) def on_load_disc_pushbutton_clicked(self, clicked): @@ -140,6 +146,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): return self.vlc_media_player.audio_set_mute(True) if not self.media_state_wait(vlc.State.Playing): + self.toggle_disable_load_media(False) return self.vlc_media_player.pause() self.vlc_media_player.set_time(0) @@ -217,9 +224,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ end_time = self.end_timeedit.time() end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ - end_time.minute() * 60 * 1000 + \ - end_time.second() * 1000 + \ - end_time.msec() + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() self.vlc_media_player.set_time(end_time_ms) @QtCore.pyqtSlot(bool) @@ -231,9 +238,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ start_time = self.start_timeedit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ - start_time.minute() * 60 * 1000 + \ - start_time.second() * 1000 + \ - start_time.msec() + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() self.vlc_media_player.set_time(start_time_ms) @QtCore.pyqtSlot(int) @@ -373,14 +380,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): log.debug('in on_save_pushbutton_clicked') start_time = self.start_timeedit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ - start_time.minute() * 60 * 1000 + \ - start_time.second() * 1000 + \ - start_time.msec() + start_time.minute() * 60 * 1000 + \ + start_time.second() * 1000 + \ + start_time.msec() end_time = self.end_timeedit.time() end_time_ms = end_time.hour() * 60 * 60 * 1000 + \ - end_time.minute() * 60 * 1000 + \ - end_time.second() * 1000 + \ - end_time.msec() + end_time.minute() * 60 * 1000 + \ + end_time.second() * 1000 + \ + end_time.msec() title = self.title_combo_box.itemData(self.title_combo_box.currentIndex()) audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) @@ -392,15 +399,58 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def media_state_wait(self, media_state): """ Wait for the video to change its state - Wait no longer than 15 seconds. (loading an iso file needs a long time) + Wait no longer than 15 seconds. (loading an optical disc takes some time) :param media_state: VLC media state to wait for. :return: True if state was reached within 15 seconds, False if not or error occurred. """ start = datetime.now() - while not media_state == self.vlc_media.get_state(): - if self.vlc_media.get_state() == vlc.State.Error: + while media_state != self.vlc_media_player.get_state(): + if self.vlc_media_player.get_state() == vlc.State.Error: return False if (datetime.now() - start).seconds > 15: return False return True + + def find_optical_devices(self): + """ + Attempt to autodetect optical devices on the computer, and add them to the media-dropdown + :return: + """ + # Clear list first + self.media_path_combobox.clear() + # insert empty string as first item + self.media_path_combobox.addItem('') + if os.name == 'nt': + # use win api to fine optical drives + bitmask = windll.kernel32.GetLogicalDrives() + for letter in string.uppercase: + if bitmask & 1: + try: + type = windll.kernel32.GetDriveTypeW('%s:\\' % letter) + # if type is 5, it is a cd-rom drive + if type == 5: + self.media_path_combobox.addItem('%s:\\' % letter) + except Exception as e: + log.debug("Exception while looking for optical drives: ", e) + bitmask >>= 1 + elif sys.platform.startswith('linux'): + # Get disc devices from dbus and find the ones that are optical + bus = dbus.SystemBus() + udev_manager_obj = bus.get_object("org.freedesktop.UDisks", "/org/freedesktop/UDisks") + udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.UDisks') + for dev in udev_manager.EnumerateDevices(): + device_obj = bus.get_object("org.freedesktop.UDisks", dev) + device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) + if device_props.Get('org.freedesktop.UDisks.Device', "DeviceIsOpticalDisc"): + self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', "DeviceFile")) + elif sys.platform.startswith('darwin'): + # Look for DVD folders in devices to find optical devices + volumes = os.listdir('/Volumes') + candidates = list() + for volume in volumes: + if volume.startswith('.'): + continue + dirs = os.listdir('/Volumes/' + volume) + if 'VIDEO_TS' in dirs: + self.media_path_combobox.addItem() From 73304d7ec372f39157fccfa7a432de40613e3876 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 30 Mar 2014 21:32:41 +0200 Subject: [PATCH 08/66] Added check to see if given path exists. --- .../plugins/media/forms/mediaclipselectorform.py | 15 ++++++++++++--- .../media/forms/test_mediaclipselectorform.py | 5 ----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index f1c25b378..1472052a1 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -122,16 +122,23 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ self.disable_all() path = self.media_path_combobox.currentText() + # Check if given path is non-empty and exists before starting VLC if path == '': log.debug('no given path') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) self.toggle_disable_load_media(False) return + if not os.path.exists(path): + log.debug('vlc media player is none') + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'Given path does not exists')) + self.toggle_disable_load_media(False) + return self.vlc_media = self.vlc_instance.media_new_path(path) if not self.vlc_media: log.debug('vlc media player is none') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', - 'An error happened during initialization of VLC player')) + 'An error happened during initialization of VLC player')) self.toggle_disable_load_media(False) return # put the media in the media player @@ -141,11 +148,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if self.vlc_media_player.play() < 0: log.debug('vlc play returned error') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', - 'An error happen when starting VLC player')) + 'VLC player failed playing the media')) self.toggle_disable_load_media(False) return self.vlc_media_player.audio_set_mute(True) if not self.media_state_wait(vlc.State.Playing): + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'VLC player failed playing the media')) self.toggle_disable_load_media(False) return self.vlc_media_player.pause() @@ -453,4 +462,4 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): continue dirs = os.listdir('/Volumes/' + volume) if 'VIDEO_TS' in dirs: - self.media_path_combobox.addItem() + self.media_path_combobox.addItem('/Volumes/' + volume) diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index b4dbf70cd..5939a5e05 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -56,10 +56,6 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.vlc_patcher.start() # Mock the media item self.mock_media_item = MagicMock() - # Mock media_state_wait to avoid waiting for VLC playing state - #self.mock_media_state_wait = patch('openlp.plugins.media.forms.mediaclipselectorform.media_state_wait') - #self.mock_media_state_wait.return_value = True - #self.mock_media_state_wait.start() # create form to test self.form = MediaClipSelectorForm(self.mock_media_item, self.main_window, None) mock_media_state_wait = MagicMock() @@ -70,7 +66,6 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): """ Delete all the C++ objects at the end so that we don't have a segfault """ - #self.mock_media_state_wait.stop() self.vlc_patcher.stop() del self.form del self.main_window From 6e90904d91606c62bec8bab2ae0d5424dd76506c Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 1 Apr 2014 21:48:46 +0200 Subject: [PATCH 09/66] Fix for autodetection of optical devices --- openlp/plugins/media/forms/mediaclipselectorform.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 1472052a1..005f821a4 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -441,18 +441,20 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if type == 5: self.media_path_combobox.addItem('%s:\\' % letter) except Exception as e: - log.debug("Exception while looking for optical drives: ", e) + log.debug('Exception while looking for optical drives: ', e) bitmask >>= 1 elif sys.platform.startswith('linux'): # Get disc devices from dbus and find the ones that are optical bus = dbus.SystemBus() - udev_manager_obj = bus.get_object("org.freedesktop.UDisks", "/org/freedesktop/UDisks") + udev_manager_obj = bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks') udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.UDisks') for dev in udev_manager.EnumerateDevices(): device_obj = bus.get_object("org.freedesktop.UDisks", dev) device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) - if device_props.Get('org.freedesktop.UDisks.Device', "DeviceIsOpticalDisc"): - self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', "DeviceFile")) + if device_props.Get('org.freedesktop.UDisks.Device', 'DeviceIsDrive'): + drive_props = device_props.Get('org.freedesktop.UDisks.Device', 'DriveMediaCompatibility') + if any('optical' in prop for prop in drive_props): + self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', 'DeviceFile')) elif sys.platform.startswith('darwin'): # Look for DVD folders in devices to find optical devices volumes = os.listdir('/Volumes') From 43f18ee2c9f8cfe0cb0ec99504042763363346c1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 5 Apr 2014 09:46:35 +0200 Subject: [PATCH 10/66] Added some tests. --- .../media/forms/mediaclipselectorform.py | 6 +- .../media/forms/test_mediaclipselectorform.py | 59 ++++++++++++++++++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 005f821a4..bbc525834 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -129,7 +129,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.toggle_disable_load_media(False) return if not os.path.exists(path): - log.debug('vlc media player is none') + log.debug('Given path does not exists') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'Given path does not exists')) self.toggle_disable_load_media(False) @@ -259,7 +259,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param index: The index of the newly chosen title track. """ - log.debug('in on_title_combo_box_changed, index: ', str(index)) + log.debug('in on_title_combo_box_changed, index: %d', index) self.vlc_media_player.set_title(index) self.vlc_media_player.set_time(0) self.vlc_media_player.play() @@ -306,7 +306,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param index: The index of the newly chosen audio track. """ audio_track = self.audio_tracks_combobox.itemData(index) - log.debug('in on_audio_tracks_combobox_currentIndexChanged, index: ', str(index), ' audio_track: ', audio_track) + log.debug('in on_audio_tracks_combobox_currentIndexChanged, index: %d audio_track: %s' % (index, audio_track)) if audio_track and int(audio_track) > 0: self.vlc_media_player.audio_set_track(int(audio_track)) diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index 5939a5e05..fb7519057 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -84,14 +84,69 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): def click_load_button_test(self): """ - Test that the correct function is called when load is clicked + Test that the correct function is called when load is clicked, and that it behaves as expected. """ # GIVEN: Mocked methods. with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ - mocked_critical_error_message_box: + mocked_critical_error_message_box,\ + patch('openlp.plugins.media.forms.mediaclipselectorform.os.path.exists') as mocked_os_path_exists,\ + patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() # WHEN: The load button is clicked with no path set QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) # THEN: we should get an error mocked_critical_error_message_box.assert_called_with(message='No path was given') + + # WHEN: The load button is clicked with a non-existing path + mocked_os_path_exists.return_value = False + self.form.media_path_combobox.insertItem (0, '/non-existing/test-path.test') + self.form.media_path_combobox.setCurrentIndex(0) + QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) + + # THEN: we should get an error + assert self.form.media_path_combobox.currentText() == '/non-existing/test-path.test',\ + 'The media path should be the given one.' + mocked_critical_error_message_box.assert_called_with(message='Given path does not exists') + + # WHEN: The load button is clicked with a mocked existing path + mocked_os_path_exists.return_value = True + self.form.vlc_media_player = MagicMock() + self.form.vlc_media_player.play.return_value = -1 + self.form.media_path_combobox.insertItem (0, '/existing/test-path.test') + self.form.media_path_combobox.setCurrentIndex(0) + QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) + + # THEN: we should get an error + assert self.form.media_path_combobox.currentText() == '/existing/test-path.test',\ + 'The media path should be the given one.' + mocked_critical_error_message_box.assert_called_with(message='VLC player failed playing the media') + + def title_combobox_test(self): + """ + Test the behavior when the title combobox is updated + """ + # GIVEN: Mocked methods and some entries in the title combobox. + with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + self.form.audio_tracks_combobox.itemData = MagicMock() + self.form.subtitle_tracks_combobox.itemData = MagicMock() + self.form.audio_tracks_combobox.itemData.return_value = None + self.form.subtitle_tracks_combobox.itemData.return_value = None + self.form.title_combo_box.insertItem(0, 'Test Title 0') + self.form.title_combo_box.insertItem(1, 'Test Title 1') + + + # WHEN: There exists audio and subtitle tracks and the index is updated. + self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), + (0, b'Audio Track 1')] + self.form.vlc_media_player.video_get_spu_description.return_value = [(-1, b'Disabled'), + (0, b'Subtitle Track 1')] + self.form.title_combo_box.setCurrentIndex(1) + + # THEN: The subtitle and audio track comboboxes should be updated and get signals and call itemData. + self.form.audio_tracks_combobox.itemData.assert_any_call(0) + self.form.audio_tracks_combobox.itemData.assert_any_call(1) + self.form.subtitle_tracks_combobox.itemData.assert_any_call(0) + From aefef04d97734a7d2c5c15656e1aced87c3a9031 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 7 Apr 2014 23:08:48 +0200 Subject: [PATCH 11/66] Small fixes. Added checks of start/end times. --- .../media/forms/mediaclipselectorform.py | 35 ++++++++++++++++--- openlp/plugins/media/lib/mediaitem.py | 4 +-- .../media/forms/test_mediaclipselectorform.py | 22 ++++++------ 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index bbc525834..7e912ab05 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -219,11 +219,35 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): time = QtCore.QTime() new_pos_time = time.addMSecs(vlc_ms_pos) self.end_timeedit.setTime(new_pos_time) - # If start time is after end time, update end time. + # If start time is after end time, update start time. start_time = self.start_timeedit.time() if start_time > new_pos_time: self.start_timeedit.setTime(new_pos_time) + @QtCore.pyqtSlot(QtCore.QTime) + def on_start_timeedit_timeChanged(self, new_time): + """ + Called when start_timeedit is changed manually + + :param new_time: The new time + """ + # If start time is after end time, update end time. + end_time = self.end_timeedit.time() + if end_time < new_time: + self.end_timeedit.setTime(new_time) + + @QtCore.pyqtSlot(QtCore.QTime) + def on_end_timeedit_timeChanged(self, new_time): + """ + Called when end_timeedit is changed manually + + :param new_time: The new time + """ + # If start time is after end time, update start time. + start_time = self.start_timeedit.time() + if start_time > new_time: + self.start_timeedit.setTime(new_time) + @QtCore.pyqtSlot(bool) def on_jump_end_pushbutton_clicked(self, clicked): """ @@ -288,12 +312,15 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # Enable subtitle track combobox is anything in it if len(subtitles_tracks) > 0: self.subtitle_tracks_combobox.setDisabled(False) - # First track is "deactivated", so set to next if it exists - if len(subtitles_tracks) > 1: - self.subtitle_tracks_combobox.setCurrentIndex(1) self.vlc_media_player.audio_set_mute(False) self.playback_length = self.vlc_media_player.get_length() self.position_horizontalslider.setMaximum(self.playback_length) + # setup start and end time + rounded_vlc_ms_length = int(round(self.playback_length / 100.0) * 100.0) + time = QtCore.QTime() + playback_length_time = time.addMSecs(rounded_vlc_ms_length) + self.start_timeedit.setMaximumTime(playback_length_time) + self.end_timeedit.setMaximumTime(playback_length_time) # If a title or audio track is available the player is enabled if self.title_combo_box.count() > 0 or len(audio_tracks) > 0: self.toggle_disable_player(False) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 1b91c1554..c2af902d4 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -125,8 +125,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ Adds buttons to the start of the header bar. """ - self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text='Load optical disc', - tooltip='Load optical disc', triggers=self.on_load_optical) + self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text='Load CD/DVD', + tooltip='Load CD/DVD', triggers=self.on_load_optical) if not VLC_AVAILABLE: self.load_optical.setDisabled(True) diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index fb7519057..16670232c 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -88,9 +88,9 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): """ # GIVEN: Mocked methods. with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ - mocked_critical_error_message_box,\ - patch('openlp.plugins.media.forms.mediaclipselectorform.os.path.exists') as mocked_os_path_exists,\ - patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + mocked_critical_error_message_box,\ + patch('openlp.plugins.media.forms.mediaclipselectorform.os.path.exists') as mocked_os_path_exists,\ + patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: self.form.exec_() # WHEN: The load button is clicked with no path set @@ -101,10 +101,10 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): # WHEN: The load button is clicked with a non-existing path mocked_os_path_exists.return_value = False - self.form.media_path_combobox.insertItem (0, '/non-existing/test-path.test') + self.form.media_path_combobox.insertItem(0, '/non-existing/test-path.test') self.form.media_path_combobox.setCurrentIndex(0) QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) - + # THEN: we should get an error assert self.form.media_path_combobox.currentText() == '/non-existing/test-path.test',\ 'The media path should be the given one.' @@ -114,10 +114,10 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): mocked_os_path_exists.return_value = True self.form.vlc_media_player = MagicMock() self.form.vlc_media_player.play.return_value = -1 - self.form.media_path_combobox.insertItem (0, '/existing/test-path.test') + self.form.media_path_combobox.insertItem(0, '/existing/test-path.test') self.form.media_path_combobox.setCurrentIndex(0) QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) - + # THEN: we should get an error assert self.form.media_path_combobox.currentText() == '/existing/test-path.test',\ 'The media path should be the given one.' @@ -136,17 +136,15 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.subtitle_tracks_combobox.itemData.return_value = None self.form.title_combo_box.insertItem(0, 'Test Title 0') self.form.title_combo_box.insertItem(1, 'Test Title 1') - # WHEN: There exists audio and subtitle tracks and the index is updated. - self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), - (0, b'Audio Track 1')] + self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), + (0, b'Audio Track 1')] self.form.vlc_media_player.video_get_spu_description.return_value = [(-1, b'Disabled'), - (0, b'Subtitle Track 1')] + (0, b'Subtitle Track 1')] self.form.title_combo_box.setCurrentIndex(1) # THEN: The subtitle and audio track comboboxes should be updated and get signals and call itemData. self.form.audio_tracks_combobox.itemData.assert_any_call(0) self.form.audio_tracks_combobox.itemData.assert_any_call(1) self.form.subtitle_tracks_combobox.itemData.assert_any_call(0) - From f16a781309f00872e20d1ec8e600f459ccb11347 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Wed, 9 Apr 2014 22:24:19 +0200 Subject: [PATCH 12/66] Changed the clipselector gui a bit. --- .../media/forms/mediaclipselectordialog.py | 251 ++++++++-------- .../media/forms/mediaclipselectorform.py | 26 +- resources/forms/mediaclipselector.ui | 280 ++++++++---------- .../media/forms/test_mediaclipselectorform.py | 3 +- 4 files changed, 274 insertions(+), 286 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index a6718fbf2..921c68a8d 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -1,3 +1,31 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 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 # +############################################################################### from PyQt4 import QtCore, QtGui from openlp.core.common import translate @@ -23,92 +51,6 @@ class Ui_MediaClipSelector(object): self.centralwidget.setObjectName("centralwidget") self.gridLayout = QtGui.QGridLayout(self.centralwidget) self.gridLayout.setObjectName("gridLayout") - self.close_pushbutton = QtGui.QPushButton(self.centralwidget) - self.close_pushbutton.setEnabled(True) - self.close_pushbutton.setObjectName("close_pushbutton") - self.gridLayout.addWidget(self.close_pushbutton, 10, 4, 1, 1) - self.pause_pushbutton = QtGui.QPushButton(self.centralwidget) - self.pause_pushbutton.setEnabled(True) - self.pause_pushbutton.setText("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.pause_pushbutton.setIcon(icon) - self.pause_pushbutton.setObjectName("pause_pushbutton") - self.gridLayout.addWidget(self.pause_pushbutton, 6, 1, 1, 1) - self.play_pushbutton = QtGui.QPushButton(self.centralwidget) - self.play_pushbutton.setEnabled(True) - self.play_pushbutton.setText("") - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.play_pushbutton.setIcon(icon1) - self.play_pushbutton.setObjectName("play_pushbutton") - self.gridLayout.addWidget(self.play_pushbutton, 6, 0, 1, 1) - self.media_path_label = QtGui.QLabel(self.centralwidget) - self.media_path_label.setEnabled(True) - self.media_path_label.setObjectName("media_path_label") - self.gridLayout.addWidget(self.media_path_label, 0, 0, 1, 2) - self.preview_pushbutton = QtGui.QPushButton(self.centralwidget) - self.preview_pushbutton.setEnabled(True) - self.preview_pushbutton.setObjectName("preview_pushbutton") - self.gridLayout.addWidget(self.preview_pushbutton, 10, 2, 1, 1) - self.start_point_label = QtGui.QLabel(self.centralwidget) - self.start_point_label.setEnabled(True) - self.start_point_label.setObjectName("start_point_label") - self.gridLayout.addWidget(self.start_point_label, 7, 0, 1, 2) - spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem, 9, 3, 1, 1) - self.start_timeedit = QtGui.QTimeEdit(self.centralwidget) - self.start_timeedit.setEnabled(True) - self.start_timeedit.setObjectName("start_timeedit") - self.gridLayout.addWidget(self.start_timeedit, 7, 2, 1, 1) - self.jump_end_pushbutton = QtGui.QPushButton(self.centralwidget) - self.jump_end_pushbutton.setEnabled(True) - self.jump_end_pushbutton.setObjectName("jump_end_pushbutton") - self.gridLayout.addWidget(self.jump_end_pushbutton, 8, 4, 1, 1) - self.subtitle_track_label = QtGui.QLabel(self.centralwidget) - self.subtitle_track_label.setEnabled(True) - self.subtitle_track_label.setObjectName("subtitle_track_label") - self.gridLayout.addWidget(self.subtitle_track_label, 4, 0, 1, 2) - self.set_end_pushbutton = QtGui.QPushButton(self.centralwidget) - self.set_end_pushbutton.setEnabled(True) - self.set_end_pushbutton.setObjectName("set_end_pushbutton") - self.gridLayout.addWidget(self.set_end_pushbutton, 8, 3, 1, 1) - self.set_start_pushbutton = QtGui.QPushButton(self.centralwidget) - self.set_start_pushbutton.setEnabled(True) - self.set_start_pushbutton.setObjectName("set_start_pushbutton") - self.gridLayout.addWidget(self.set_start_pushbutton, 7, 3, 1, 1) - self.audio_track_label = QtGui.QLabel(self.centralwidget) - self.audio_track_label.setEnabled(True) - self.audio_track_label.setObjectName("audio_track_label") - self.gridLayout.addWidget(self.audio_track_label, 3, 0, 1, 2) - self.load_disc_pushbutton = QtGui.QPushButton(self.centralwidget) - self.load_disc_pushbutton.setEnabled(True) - self.load_disc_pushbutton.setObjectName("load_disc_pushbutton") - self.gridLayout.addWidget(self.load_disc_pushbutton, 0, 4, 1, 1) - self.media_position_timeedit = QtGui.QTimeEdit(self.centralwidget) - self.media_position_timeedit.setEnabled(True) - self.media_position_timeedit.setObjectName("media_position_timeedit") - self.gridLayout.addWidget(self.media_position_timeedit, 6, 4, 1, 1) - self.end_point_label = QtGui.QLabel(self.centralwidget) - self.end_point_label.setEnabled(True) - self.end_point_label.setObjectName("end_point_label") - self.gridLayout.addWidget(self.end_point_label, 8, 0, 1, 1) - self.jump_start_pushbutton = QtGui.QPushButton(self.centralwidget) - self.jump_start_pushbutton.setEnabled(True) - self.jump_start_pushbutton.setObjectName("jump_start_pushbutton") - self.gridLayout.addWidget(self.jump_start_pushbutton, 7, 4, 1, 1) - self.end_timeedit = QtGui.QTimeEdit(self.centralwidget) - self.end_timeedit.setEnabled(True) - self.end_timeedit.setObjectName("end_timeedit") - self.gridLayout.addWidget(self.end_timeedit, 8, 2, 1, 1) - self.title_label = QtGui.QLabel(self.centralwidget) - self.title_label.setEnabled(True) - self.title_label.setObjectName("title_label") - self.gridLayout.addWidget(self.title_label, 2, 0, 1, 1) - self.save_pushbutton = QtGui.QPushButton(self.centralwidget) - self.save_pushbutton.setEnabled(True) - self.save_pushbutton.setObjectName("save_pushbutton") - self.gridLayout.addWidget(self.save_pushbutton, 10, 3, 1, 1) self.media_path_combobox = QtGui.QComboBox(self.centralwidget) self.media_path_combobox.setEnabled(True) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) @@ -119,26 +61,76 @@ class Ui_MediaClipSelector(object): self.media_path_combobox.setEditable(True) self.media_path_combobox.setObjectName("media_path_combobox") self.gridLayout.addWidget(self.media_path_combobox, 0, 2, 1, 2) - self.position_horizontalslider = QtGui.QSlider(self.centralwidget) - self.position_horizontalslider.setEnabled(True) - self.position_horizontalslider.setTracking(False) - self.position_horizontalslider.setOrientation(QtCore.Qt.Horizontal) - self.position_horizontalslider.setInvertedAppearance(False) - self.position_horizontalslider.setObjectName("position_horizontalslider") - self.gridLayout.addWidget(self.position_horizontalslider, 6, 2, 1, 2) - self.title_combo_box = QtGui.QComboBox(self.centralwidget) - self.title_combo_box.setEnabled(True) - self.title_combo_box.setProperty("currentText", "") - self.title_combo_box.setObjectName("title_combo_box") - self.gridLayout.addWidget(self.title_combo_box, 2, 2, 1, 2) - self.audio_tracks_combobox = QtGui.QComboBox(self.centralwidget) - self.audio_tracks_combobox.setEnabled(True) - self.audio_tracks_combobox.setObjectName("audio_tracks_combobox") - self.gridLayout.addWidget(self.audio_tracks_combobox, 3, 2, 1, 2) + self.start_timeedit = QtGui.QTimeEdit(self.centralwidget) + self.start_timeedit.setEnabled(True) + self.start_timeedit.setObjectName("start_timeedit") + self.gridLayout.addWidget(self.start_timeedit, 7, 2, 1, 1) + self.end_timeedit = QtGui.QTimeEdit(self.centralwidget) + self.end_timeedit.setEnabled(True) + self.end_timeedit.setObjectName("end_timeedit") + self.gridLayout.addWidget(self.end_timeedit, 8, 2, 1, 1) + self.set_start_pushbutton = QtGui.QPushButton(self.centralwidget) + self.set_start_pushbutton.setEnabled(True) + self.set_start_pushbutton.setObjectName("set_start_pushbutton") + self.gridLayout.addWidget(self.set_start_pushbutton, 7, 3, 1, 1) + self.load_disc_pushbutton = QtGui.QPushButton(self.centralwidget) + self.load_disc_pushbutton.setEnabled(True) + self.load_disc_pushbutton.setObjectName("load_disc_pushbutton") + self.gridLayout.addWidget(self.load_disc_pushbutton, 0, 4, 1, 1) + spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) + self.gridLayout.addItem(spacerItem, 9, 3, 1, 1) + self.play_pushbutton = QtGui.QPushButton(self.centralwidget) + self.play_pushbutton.setEnabled(True) + self.play_pushbutton.setText("") + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.play_pushbutton.setIcon(icon) + self.play_pushbutton.setObjectName("play_pushbutton") + self.gridLayout.addWidget(self.play_pushbutton, 6, 0, 1, 1) + self.end_point_label = QtGui.QLabel(self.centralwidget) + self.end_point_label.setEnabled(True) + self.end_point_label.setObjectName("end_point_label") + self.gridLayout.addWidget(self.end_point_label, 8, 0, 1, 1) self.subtitle_tracks_combobox = QtGui.QComboBox(self.centralwidget) self.subtitle_tracks_combobox.setEnabled(True) self.subtitle_tracks_combobox.setObjectName("subtitle_tracks_combobox") self.gridLayout.addWidget(self.subtitle_tracks_combobox, 4, 2, 1, 2) + self.title_label = QtGui.QLabel(self.centralwidget) + self.title_label.setEnabled(True) + self.title_label.setObjectName("title_label") + self.gridLayout.addWidget(self.title_label, 2, 0, 1, 1) + self.audio_tracks_combobox = QtGui.QComboBox(self.centralwidget) + self.audio_tracks_combobox.setEnabled(True) + self.audio_tracks_combobox.setObjectName("audio_tracks_combobox") + self.gridLayout.addWidget(self.audio_tracks_combobox, 3, 2, 1, 2) + self.set_end_pushbutton = QtGui.QPushButton(self.centralwidget) + self.set_end_pushbutton.setEnabled(True) + self.set_end_pushbutton.setObjectName("set_end_pushbutton") + self.gridLayout.addWidget(self.set_end_pushbutton, 8, 3, 1, 1) + self.save_pushbutton = QtGui.QPushButton(self.centralwidget) + self.save_pushbutton.setEnabled(True) + self.save_pushbutton.setObjectName("save_pushbutton") + self.gridLayout.addWidget(self.save_pushbutton, 10, 3, 1, 1) + self.close_pushbutton = QtGui.QPushButton(self.centralwidget) + self.close_pushbutton.setEnabled(True) + self.close_pushbutton.setObjectName("close_pushbutton") + self.gridLayout.addWidget(self.close_pushbutton, 10, 4, 1, 1) + self.start_point_label = QtGui.QLabel(self.centralwidget) + self.start_point_label.setEnabled(True) + self.start_point_label.setObjectName("start_point_label") + self.gridLayout.addWidget(self.start_point_label, 7, 0, 1, 2) + self.jump_start_pushbutton = QtGui.QPushButton(self.centralwidget) + self.jump_start_pushbutton.setEnabled(True) + self.jump_start_pushbutton.setObjectName("jump_start_pushbutton") + self.gridLayout.addWidget(self.jump_start_pushbutton, 7, 4, 1, 1) + self.audio_track_label = QtGui.QLabel(self.centralwidget) + self.audio_track_label.setEnabled(True) + self.audio_track_label.setObjectName("audio_track_label") + self.gridLayout.addWidget(self.audio_track_label, 3, 0, 1, 2) + self.media_position_timeedit = QtGui.QTimeEdit(self.centralwidget) + self.media_position_timeedit.setEnabled(True) + self.media_position_timeedit.setObjectName("media_position_timeedit") + self.gridLayout.addWidget(self.media_position_timeedit, 6, 4, 1, 1) self.media_view_frame = QtGui.QFrame(self.centralwidget) self.media_view_frame.setMinimumSize(QtCore.QSize(665, 375)) self.media_view_frame.setStyleSheet("background-color:black;") @@ -146,6 +138,30 @@ class Ui_MediaClipSelector(object): self.media_view_frame.setFrameShadow(QtGui.QFrame.Raised) self.media_view_frame.setObjectName("media_view_frame") self.gridLayout.addWidget(self.media_view_frame, 5, 0, 1, 5) + self.subtitle_track_label = QtGui.QLabel(self.centralwidget) + self.subtitle_track_label.setEnabled(True) + self.subtitle_track_label.setObjectName("subtitle_track_label") + self.gridLayout.addWidget(self.subtitle_track_label, 4, 0, 1, 2) + self.jump_end_pushbutton = QtGui.QPushButton(self.centralwidget) + self.jump_end_pushbutton.setEnabled(True) + self.jump_end_pushbutton.setObjectName("jump_end_pushbutton") + self.gridLayout.addWidget(self.jump_end_pushbutton, 8, 4, 1, 1) + self.media_path_label = QtGui.QLabel(self.centralwidget) + self.media_path_label.setEnabled(True) + self.media_path_label.setObjectName("media_path_label") + self.gridLayout.addWidget(self.media_path_label, 0, 0, 1, 2) + self.title_combo_box = QtGui.QComboBox(self.centralwidget) + self.title_combo_box.setEnabled(True) + self.title_combo_box.setProperty("currentText", "") + self.title_combo_box.setObjectName("title_combo_box") + self.gridLayout.addWidget(self.title_combo_box, 2, 2, 1, 2) + self.position_horizontalslider = QtGui.QSlider(self.centralwidget) + self.position_horizontalslider.setEnabled(True) + self.position_horizontalslider.setTracking(False) + self.position_horizontalslider.setOrientation(QtCore.Qt.Horizontal) + self.position_horizontalslider.setInvertedAppearance(False) + self.position_horizontalslider.setObjectName("position_horizontalslider") + self.gridLayout.addWidget(self.position_horizontalslider, 6, 1, 1, 3) #MediaClipSelector.setCentralWidget(self.centralwidget) self.retranslateUi(MediaClipSelector) @@ -155,8 +171,7 @@ class Ui_MediaClipSelector(object): MediaClipSelector.setTabOrder(self.title_combo_box, self.audio_tracks_combobox) MediaClipSelector.setTabOrder(self.audio_tracks_combobox, self.subtitle_tracks_combobox) MediaClipSelector.setTabOrder(self.subtitle_tracks_combobox, self.play_pushbutton) - MediaClipSelector.setTabOrder(self.play_pushbutton, self.pause_pushbutton) - MediaClipSelector.setTabOrder(self.pause_pushbutton, self.position_horizontalslider) + MediaClipSelector.setTabOrder(self.play_pushbutton, self.position_horizontalslider) MediaClipSelector.setTabOrder(self.position_horizontalslider, self.media_position_timeedit) MediaClipSelector.setTabOrder(self.media_position_timeedit, self.start_timeedit) MediaClipSelector.setTabOrder(self.start_timeedit, self.set_start_pushbutton) @@ -164,27 +179,25 @@ class Ui_MediaClipSelector(object): MediaClipSelector.setTabOrder(self.jump_start_pushbutton, self.end_timeedit) MediaClipSelector.setTabOrder(self.end_timeedit, self.set_end_pushbutton) MediaClipSelector.setTabOrder(self.set_end_pushbutton, self.jump_end_pushbutton) - MediaClipSelector.setTabOrder(self.jump_end_pushbutton, self.preview_pushbutton) - MediaClipSelector.setTabOrder(self.preview_pushbutton, self.save_pushbutton) + MediaClipSelector.setTabOrder(self.jump_end_pushbutton, self.save_pushbutton) MediaClipSelector.setTabOrder(self.save_pushbutton, self.close_pushbutton) def retranslateUi(self, MediaClipSelector): MediaClipSelector.setWindowTitle(translate("MediaPlugin.MediaClipSelector", "Select media clip", None)) - self.close_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Close", None)) - self.media_path_label.setText(translate("MediaPlugin.MediaClipSelector", "Media path", None)) - self.preview_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Preview current clip", None)) - self.start_point_label.setText(translate("MediaPlugin.MediaClipSelector", "Start point", None)) self.start_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) - self.jump_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to end point", None)) - self.subtitle_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Subtitle track", None)) - self.set_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as end point", None)) - self.set_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as start point", None)) - self.audio_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Audio track", None)) - self.load_disc_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Load disc", None)) - self.media_position_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) - self.end_point_label.setText(translate("MediaPlugin.MediaClipSelector", "End point", None)) - self.jump_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to start point", None)) self.end_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) + self.set_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as start point", None)) + self.load_disc_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Load disc", None)) + self.end_point_label.setText(translate("MediaPlugin.MediaClipSelector", "End point", None)) self.title_label.setText(translate("MediaPlugin.MediaClipSelector", "Title", None)) + self.set_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as end point", None)) self.save_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Save current clip", None)) + self.close_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Close", None)) + self.start_point_label.setText(translate("MediaPlugin.MediaClipSelector", "Start point", None)) + self.jump_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to start point", None)) + self.audio_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Audio track", None)) + self.media_position_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) + self.subtitle_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Subtitle track", None)) + self.jump_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to end point", None)) + self.media_path_label.setText(translate("MediaPlugin.MediaClipSelector", "Media path", None)) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 7e912ab05..6cda4a494 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -68,6 +68,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.toggle_disable_load_media(False) # most actions auto-connect due to the functions name, so only a few left to do self.close_pushbutton.clicked.connect(self.reject) + # setup play/pause icon + self.play_icon = QtGui.QIcon() + self.play_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.pause_icon = QtGui.QIcon() + self.pause_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) def reject(self): """ @@ -174,23 +179,20 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.title_combo_box.setDisabled(False) self.toggle_disable_load_media(False) - @QtCore.pyqtSlot(bool) - def on_pause_pushbutton_clicked(self, clicked): - """ - Pause the playback - - :param clicked: Given from signal, not used. - """ - self.vlc_media_player.pause() - @QtCore.pyqtSlot(bool) def on_play_pushbutton_clicked(self, clicked): """ - Start the playback + Toggle the playback :param clicked: Given from signal, not used. """ - self.vlc_media_player.play() + if self.vlc_media_player.get_state() == vlc.State.Playing: + self.vlc_media_player.pause() + self.play_pushbutton.setIcon(self.play_icon) + else: + self.vlc_media_player.play() + self.media_state_wait(vlc.State.Playing) + self.play_pushbutton.setIcon(self.pause_icon) @QtCore.pyqtSlot(bool) def on_set_start_pushbutton_clicked(self, clicked): @@ -394,7 +396,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param action: If True elements are disabled, if False they are enabled. """ self.play_pushbutton.setDisabled(action) - self.pause_pushbutton.setDisabled(action) self.position_horizontalslider.setDisabled(action) self.media_position_timeedit.setDisabled(action) self.start_timeedit.setDisabled(action) @@ -403,7 +404,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.end_timeedit.setDisabled(action) self.set_end_pushbutton.setDisabled(action) self.jump_end_pushbutton.setDisabled(action) - self.preview_pushbutton.setDisabled(action) self.save_pushbutton.setDisabled(action) @QtCore.pyqtSlot(bool) diff --git a/resources/forms/mediaclipselector.ui b/resources/forms/mediaclipselector.ui index 4cd8f518d..754161eae 100644 --- a/resources/forms/mediaclipselector.ui +++ b/resources/forms/mediaclipselector.ui @@ -42,71 +42,59 @@ - - + + true - - Close + + + 0 + 0 + + + + true - - + + true - - - - - - ../images/media_playback_pause.png../images/media_playback_pause.png + + HH:mm:ss.z - - + + true - - - - - - ../images/media_playback_start.png../images/media_playback_start.png + + HH:mm:ss.z - - + + true - Media path + Set current position as start point - - + + true - Preview current clip - - - - - - - true - - - Start point + Load disc @@ -126,83 +114,17 @@ - - - - true - - - HH:mm:ss.z - - - - - + + true - Jump to end point + - - - - - - true - - - Subtitle track - - - - - - - true - - - Set current position as end point - - - - - - - true - - - Set current position as start point - - - - - - - true - - - Audio track - - - - - - - true - - - Load disc - - - - - - - true - - - HH:mm:ss.z + + + ../images/media_playback_start.png../images/media_playback_start.png @@ -216,24 +138,11 @@ - - + + true - - Jump to start point - - - - - - - true - - - HH:mm:ss.z - @@ -246,6 +155,23 @@ + + + + true + + + + + + + true + + + Set current position as end point + + + @@ -256,60 +182,54 @@ - - + + true - - - 0 - 0 - - - - true + + Close - - + + true - - false - - - Qt::Horizontal - - - false + + Start point - - + + true - - + + Jump to start point - - + + true + + Audio track + - - + + true + + HH:mm:ss.z + @@ -331,6 +251,62 @@ + + + + true + + + Subtitle track + + + + + + + true + + + Jump to end point + + + + + + + true + + + Media path + + + + + + + true + + + + + + + + + + true + + + false + + + Qt::Horizontal + + + false + + + @@ -341,7 +317,6 @@ audio_tracks_combobox subtitle_tracks_combobox play_pushbutton - pause_pushbutton position_horizontalslider media_position_timeedit start_timeedit @@ -350,7 +325,6 @@ end_timeedit set_end_pushbutton jump_end_pushbutton - preview_pushbutton save_pushbutton close_pushbutton diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index 16670232c..9484e92a7 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -130,6 +130,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): # GIVEN: Mocked methods and some entries in the title combobox. with patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: self.form.exec_() + self.form.vlc_media_player.get_length.return_value = 1000 self.form.audio_tracks_combobox.itemData = MagicMock() self.form.subtitle_tracks_combobox.itemData = MagicMock() self.form.audio_tracks_combobox.itemData.return_value = None @@ -138,7 +139,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.title_combo_box.insertItem(1, 'Test Title 1') # WHEN: There exists audio and subtitle tracks and the index is updated. - self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), + self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), (0, b'Audio Track 1')] self.form.vlc_media_player.video_get_spu_description.return_value = [(-1, b'Disabled'), (0, b'Subtitle Track 1')] From a999e59a4f36503f36d51700042a4628ad2af9b1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 20 Apr 2014 16:36:56 +0200 Subject: [PATCH 13/66] Fixes for merge --- openlp/core/lib/serviceitem.py | 2 +- tests/resources/serviceitem-dvd.osj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index b1e254f82..7e9528503 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -435,7 +435,7 @@ class ServiceItem(RegistryProperties): Returns the title of the service item. """ if self.is_text() or self.is_capable(ItemCapabilities.IsOptical) \ - or self.is_capable(temCapabilities.CanEditTitle): + or self.is_capable(ItemCapabilities.CanEditTitle): return self.title else: if len(self._raw_frames) > 1: diff --git a/tests/resources/serviceitem-dvd.osj b/tests/resources/serviceitem-dvd.osj index d3d224bff..cda95e592 100644 --- a/tests/resources/serviceitem-dvd.osj +++ b/tests/resources/serviceitem-dvd.osj @@ -1 +1 @@ -[{"serviceitem": {"header": {"auto_play_slides_once": false, "data": "", "processor": "Automatic", "theme": -1, "theme_overwritten": false, "end_time": 672.069, "start_time": 654.375, "capabilities": [12, 17, 16, 4], "media_length": 17.694, "audit": "", "xml_version": null, "title": "/dev/sr0@00:06:15,375-00:01:09,069", "auto_play_slides_loop": false, "notes": "", "icon": ":/plugins/plugin_media.png", "type": 3, "background_audio": [], "plugin": "media", "from_plugin": false, "search": "", "will_auto_start": false, "name": "media", "footer": [], "timed_slide_interval": 0}, "data": [{"image": ":/media/slidecontroller_multimedia.png", "path": "optical:1:5:3:654375:672069:/dev/sr0", "title": "/dev/sr0"}]}}] +[{"serviceitem": {"header": {"auto_play_slides_once": false, "data": "", "processor": "Automatic", "theme": -1, "theme_overwritten": false, "end_time": 672.069, "start_time": 654.375, "capabilities": [12, 18, 16, 4], "media_length": 17.694, "audit": "", "xml_version": null, "title": "/dev/sr0@00:06:15,375-00:01:09,069", "auto_play_slides_loop": false, "notes": "", "icon": ":/plugins/plugin_media.png", "type": 3, "background_audio": [], "plugin": "media", "from_plugin": false, "search": "", "will_auto_start": false, "name": "media", "footer": [], "timed_slide_interval": 0}, "data": [{"image": ":/media/slidecontroller_multimedia.png", "path": "optical:1:5:3:654375:672069:/dev/sr0", "title": "/dev/sr0"}]}}] From 87e7310e2bf32c2e47236c32d8ce71afee94f9bb Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 22 Apr 2014 19:23:54 +0200 Subject: [PATCH 14/66] Reset the clipselector on each load. --- .../media/forms/mediaclipselectordialog.py | 3 ++ .../media/forms/mediaclipselectorform.py | 41 ++++++++++++++++--- resources/forms/mediaclipselector.ui | 3 ++ 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index 921c68a8d..beed9e8e3 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -26,6 +26,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., 59 # # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### + + from PyQt4 import QtCore, QtGui from openlp.core.common import translate @@ -129,6 +131,7 @@ class Ui_MediaClipSelector(object): self.gridLayout.addWidget(self.audio_track_label, 3, 0, 1, 2) self.media_position_timeedit = QtGui.QTimeEdit(self.centralwidget) self.media_position_timeedit.setEnabled(True) + self.media_position_timeedit.setReadOnly(True) self.media_position_timeedit.setObjectName("media_position_timeedit") self.gridLayout.addWidget(self.media_position_timeedit, 6, 4, 1, 1) self.media_view_frame = QtGui.QFrame(self.centralwidget) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 6cda4a494..11986acf5 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -35,7 +35,6 @@ import sys if sys.platform.startswith('linux'): import dbus import logging -import time from datetime import datetime @@ -62,10 +61,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): super(MediaClipSelectorForm, self).__init__(parent) self.media_item = media_item self.setupUi(self) - self.playback_length = 0 - self.position_horizontalslider.setMinimum(0) - self.disable_all() - self.toggle_disable_load_media(False) # most actions auto-connect due to the functions name, so only a few left to do self.close_pushbutton.clicked.connect(self.reject) # setup play/pause icon @@ -79,16 +74,43 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): Exit Dialog and do not save """ log.debug('MediaClipSelectorForm.reject') - self.vlc_media_player.stop() + # Tear down vlc + if self.vlc_media_player: + self.vlc_media_player.stop() + self.vlc_media_player.release() + self.vlc_media_player = None + if self.vlc_instance: + self.vlc_instance.release() + self.vlc_instance = None + if self.vlc_media: + self.vlc_media.release() + self.vlc_media = None QtGui.QDialog.reject(self) def exec_(self): """ Start dialog """ + self.reset_ui() self.setup_vlc() return QtGui.QDialog.exec_(self) + def reset_ui(self): + """ + Reset the UI to default values + """ + self.playback_length = 0 + self.position_horizontalslider.setMinimum(0) + self.disable_all() + self.toggle_disable_load_media(False) + self.subtitle_tracks_combobox.clear() + self.audio_tracks_combobox.clear() + self.title_combo_box.clear() + time = QtCore.QTime() + self.start_timeedit.setTime(time) + self.end_timeedit.setTime(time) + self.media_position_timeedit.setTime(time) + def setup_vlc(self): """ Setup VLC instance and mediaplayer @@ -286,6 +308,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param index: The index of the newly chosen title track. """ log.debug('in on_title_combo_box_changed, index: %d', index) + if not self.vlc_media_player: + return self.vlc_media_player.set_title(index) self.vlc_media_player.set_time(0) self.vlc_media_player.play() @@ -323,6 +347,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): playback_length_time = time.addMSecs(rounded_vlc_ms_length) self.start_timeedit.setMaximumTime(playback_length_time) self.end_timeedit.setMaximumTime(playback_length_time) + self.end_timeedit.setTime(playback_length_time) # If a title or audio track is available the player is enabled if self.title_combo_box.count() > 0 or len(audio_tracks) > 0: self.toggle_disable_player(False) @@ -334,6 +359,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param index: The index of the newly chosen audio track. """ + if not self.vlc_media_player: + return audio_track = self.audio_tracks_combobox.itemData(index) log.debug('in on_audio_tracks_combobox_currentIndexChanged, index: %d audio_track: %s' % (index, audio_track)) if audio_track and int(audio_track) > 0: @@ -346,6 +373,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param index: The index of the newly chosen subtitle. """ + if not self.vlc_media_player: + return subtitle_track = self.subtitle_tracks_combobox.itemData(index) if subtitle_track: self.vlc_media_player.video_set_spu(int(subtitle_track)) diff --git a/resources/forms/mediaclipselector.ui b/resources/forms/mediaclipselector.ui index 754161eae..b3f22e678 100644 --- a/resources/forms/mediaclipselector.ui +++ b/resources/forms/mediaclipselector.ui @@ -227,6 +227,9 @@ true + + true + HH:mm:ss.z From 7980231273ede4a4dfe50459885d80a96083459d Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 22 Apr 2014 22:13:36 +0200 Subject: [PATCH 15/66] Skip tests on windows. --- openlp/plugins/media/forms/mediaclipselectorform.py | 11 ++++++++++- openlp/plugins/media/lib/mediaitem.py | 3 ++- .../media/forms/test_mediaclipselectorform.py | 8 +++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 11986acf5..653cfdae2 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -43,7 +43,16 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import translate from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media.vendor import vlc +try: + from openlp.core.ui.media.vendor import vlc +except (ImportError, NameError, NotImplementedError): + pass +except OSError as e: + if sys.platform.startswith('win'): + if not isinstance(e, WindowsError) and e.winerror != 126: + raise + else: + raise log = logging.getLogger(__name__) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 74e6750d7..9be15dbb5 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -278,7 +278,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): check_directory_exists(self.service_path) self.load_list(Settings().value(self.settings_section + '/media files')) self.rebuild_players() - self.media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) + if VLC_AVAILABLE: + self.media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) def rebuild_players(self): """ diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index 9484e92a7..241ef26c3 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -29,7 +29,13 @@ """ Module to test the MediaClipSelectorForm. """ -from unittest import TestCase + +import os +from unittest import TestCase, SkipTest +from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE + +if os.name == 'nt' and not VLC_AVAILABLE: + raise SkipTest('Windows without VLC, skipping this test since it cannot run without vlc') from PyQt4 import QtGui, QtTest, QtCore From c6d85ff5236073ce2970bdcdc7dd8c29b6b3d279 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 22 Apr 2014 22:27:15 +0200 Subject: [PATCH 16/66] Fix for tests on windows --- openlp/plugins/media/lib/mediaitem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 9be15dbb5..c5510b7e4 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -41,8 +41,9 @@ from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adj from openlp.core.ui import DisplayController, Display, DisplayControllerType from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path from openlp.core.utils import get_locale_key -from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE +if VLC_AVAILABLE: + from openlp.plugins.media.forms.mediaclipselectorform import MediaClipSelectorForm log = logging.getLogger(__name__) From 817fe854e48cf14b9dc0efc961e30930f17e207c Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 22 Apr 2014 22:42:07 +0200 Subject: [PATCH 17/66] PEP8 fixes --- openlp/core/lib/serviceitem.py | 5 ++--- openlp/core/ui/media/mediacontroller.py | 10 ++++++---- openlp/core/ui/media/vlcplayer.py | 2 +- openlp/plugins/media/forms/mediaclipselectorform.py | 9 ++++++--- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 7e9528503..6aba66252 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -504,7 +504,7 @@ class ServiceItem(RegistryProperties): Confirms if the ServiceItem uses a file """ return self.service_item_type == ServiceItemType.Image or \ - (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical)) + (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical)) def is_text(self): """ @@ -645,5 +645,4 @@ class ServiceItem(RegistryProperties): file_suffix = frame['title'].split('.')[-1] if file_suffix.lower() not in suffix_list: self.is_valid = False - break - + break \ No newline at end of file diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index c720c22d0..993172354 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -38,7 +38,8 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import OpenLPMixin, Registry, RegistryMixin, RegistryProperties, Settings, UiStrings, translate from openlp.core.lib import OpenLPToolbar, ItemCapabilities from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players, parse_optical_path +from openlp.core.ui.media import MediaState, MediaInfo, MediaType, get_media_players, set_media_players,\ + parse_optical_path from openlp.core.ui.media.mediaplayer import MediaPlayer from openlp.core.common import AppLocation from openlp.core.ui import DisplayControllerType @@ -373,7 +374,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): log.debug('video is optical and live') path = service_item.get_frame_path() (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path) - is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, + controller) else: log.debug('video is not optical and live') is_valid = self._check_file_type(controller, display, service_item) @@ -391,7 +393,8 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): log.debug('video is optical and preview') path = service_item.get_frame_path() (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path) - is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) + is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, + controller) else: log.debug('video is not optical and preview') is_valid = self._check_file_type(controller, display, service_item) @@ -460,7 +463,6 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): self.media_reset(controller) # Setup media info controller.media_info = MediaInfo() - #controller.media_info.volume = 0 controller.media_info.file_info = QtCore.QFileInfo(filename) controller.media_info.media_type = MediaType.DVD controller.media_info.start_time = start/1000 diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 8f8aa1ac1..9f060a2dc 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -212,7 +212,7 @@ class VlcPlayer(MediaPlayer): if self.state != MediaState.Paused and controller.media_info.start_time > 0: log.debug('vlc play, starttime set') start_time = controller.media_info.start_time - log.debug('mediatype: ' +str(controller.media_info.media_type)) + log.debug('mediatype: ' + str(controller.media_info.media_type)) # Set tracks for the optical device if controller.media_info.media_type == MediaType.DVD: log.debug('vlc play, playing started') diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 653cfdae2..e9f46d9d1 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -74,9 +74,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.close_pushbutton.clicked.connect(self.reject) # setup play/pause icon self.play_icon = QtGui.QIcon() - self.play_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.play_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, + QtGui.QIcon.Off) self.pause_icon = QtGui.QIcon() - self.pause_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_pause.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self.pause_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_pause.png"), QtGui.QIcon.Normal, + QtGui.QIcon.Off) def reject(self): """ @@ -519,7 +521,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if device_props.Get('org.freedesktop.UDisks.Device', 'DeviceIsDrive'): drive_props = device_props.Get('org.freedesktop.UDisks.Device', 'DriveMediaCompatibility') if any('optical' in prop for prop in drive_props): - self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', 'DeviceFile')) + self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', + 'DeviceFile')) elif sys.platform.startswith('darwin'): # Look for DVD folders in devices to find optical devices volumes = os.listdir('/Volumes') From 06825e4aeb8810217f1587984c1f3d86854be94e Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 23 May 2014 15:39:25 +0200 Subject: [PATCH 18/66] Added support for audio cds and udisks2 --- openlp/core/ui/media/mediacontroller.py | 10 +- openlp/core/ui/media/vlcplayer.py | 14 +- .../media/forms/mediaclipselectorform.py | 202 ++++++++++++------ openlp/plugins/media/lib/mediaitem.py | 2 +- 4 files changed, 160 insertions(+), 68 deletions(-) diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 993172354..b25556e73 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -464,7 +464,10 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): # Setup media info controller.media_info = MediaInfo() controller.media_info.file_info = QtCore.QFileInfo(filename) - controller.media_info.media_type = MediaType.DVD + if audio_track == -1 and subtitle_track == -1: + controller.media_info.media_type = MediaType.CD + else: + controller.media_info.media_type = MediaType.DVD controller.media_info.start_time = start/1000 controller.media_info.end_time = end/1000 controller.media_info.length = (end - start)/1000 @@ -489,7 +492,10 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): vlc_player.load(display) self.resize(display, vlc_player) self.current_media_players[controller.controller_type] = vlc_player - controller.media_info.media_type = MediaType.DVD + if audio_track == -1 and subtitle_track == -1: + controller.media_info.media_type = MediaType.CD + else: + controller.media_info.media_type = MediaType.DVD return True def _check_file_type(self, controller, display, service_item): diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 6fd9bafd3..0c6d91078 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -166,7 +166,19 @@ class VlcPlayer(MediaPlayer): file_path = str(controller.media_info.file_info.absoluteFilePath()) path = os.path.normcase(file_path) # create the media - display.vlc_media = display.vlc_instance.media_new_path(path) + if controller.media_info.media_type == MediaType.CD: + display.vlc_media = display.vlc_instance.media_new_location('cdda://' + path) + display.vlc_media_player.set_media(display.vlc_media) + display.vlc_media_player.play() + # Wait for media to start playing. In this case VLC actually returns an error. + self.media_state_wait(display, vlc.State.Playing) + # If subitems exists, this is a CD + audio_cd_tracks = display.vlc_media.subitems() + if not audio_cd_tracks or audio_cd_tracks.count() < 1: + return False + display.vlc_media = audio_cd_tracks.item_at_index(controller.media_info.title_track) + else: + display.vlc_media = display.vlc_instance.media_new_path(path) # put the media in the media player display.vlc_media_player.set_media(display.vlc_media) # parse the metadata of the file diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index e9f46d9d1..82b3cd9ea 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -150,6 +150,40 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.timer.timeout.connect(self.update_position) self.timer.start(100) self.find_optical_devices() + self.audio_cd = False + self.audio_cd_tracks = None + + def detect_audio_cd(self, path): + """ + Detects is the given path is an audio CD + + :param path: Path to the device to be tested. + :return: True if it was an audio CD else False. + """ + # Detect by trying to play it as a CD + self.vlc_media = self.vlc_instance.media_new_location('cdda://' + path) + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.play() + # Wait for media to start playing. In this case VLC actually returns an error. + self.media_state_wait(vlc.State.Playing) + self.vlc_media_player.pause() + # If subitems exists, this is a CD + self.audio_cd_tracks = self.vlc_media.subitems() + if not self.audio_cd_tracks or self.audio_cd_tracks.count() < 1: + return False + # Insert into title_combo_box + self.title_combo_box.clear() + for i in range(self.audio_cd_tracks.count()): + item = self.audio_cd_tracks.item_at_index(i) + item_title = item.get_meta(vlc.Meta.Title) + self.title_combo_box.addItem(item_title, i) + self.vlc_media_player.set_media(self.audio_cd_tracks.item_at_index(0)) + self.audio_cd = True + self.title_combo_box.setDisabled(False) + self.title_combo_box.setCurrentIndex(0) + self.on_title_combo_box_currentIndexChanged(0) + + return True @QtCore.pyqtSlot(bool) def on_load_disc_pushbutton_clicked(self, clicked): @@ -172,7 +206,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): 'Given path does not exists')) self.toggle_disable_load_media(False) return - self.vlc_media = self.vlc_instance.media_new_path(path) + self.vlc_media = self.vlc_instance.media_new_location(path) if not self.vlc_media: log.debug('vlc media player is none') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', @@ -191,25 +225,28 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): return self.vlc_media_player.audio_set_mute(True) if not self.media_state_wait(vlc.State.Playing): - critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', - 'VLC player failed playing the media')) - self.toggle_disable_load_media(False) - return + # Tests if this is an audio CD + if not self.detect_audio_cd(path): + critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', + 'VLC player failed playing the media')) + self.toggle_disable_load_media(False) + return self.vlc_media_player.pause() self.vlc_media_player.set_time(0) - # Get titles, insert in combobox - titles = self.vlc_media_player.video_get_title_description() - self.title_combo_box.clear() - for title in titles: - self.title_combo_box.addItem(title[1].decode(), title[0]) - # Main title is usually title #1 - if len(titles) > 1: - self.title_combo_box.setCurrentIndex(1) - else: - self.title_combo_box.setCurrentIndex(0) - # Enable audio track combobox if anything is in it - if len(titles) > 0: - self.title_combo_box.setDisabled(False) + if not self.audio_cd: + # Get titles, insert in combobox + titles = self.vlc_media_player.video_get_title_description() + self.title_combo_box.clear() + for title in titles: + self.title_combo_box.addItem(title[1].decode(), title[0]) + # Main title is usually title #1 + if len(titles) > 1: + self.title_combo_box.setCurrentIndex(1) + else: + self.title_combo_box.setCurrentIndex(0) + # Enable audio track combobox if anything is in it + if len(titles) > 0: + self.title_combo_box.setDisabled(False) self.toggle_disable_load_media(False) @QtCore.pyqtSlot(bool) @@ -321,35 +358,53 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): log.debug('in on_title_combo_box_changed, index: %d', index) if not self.vlc_media_player: return - self.vlc_media_player.set_title(index) - self.vlc_media_player.set_time(0) - self.vlc_media_player.play() - self.vlc_media_player.audio_set_mute(True) - if not self.media_state_wait(vlc.State.Playing): - return - # pause - self.vlc_media_player.pause() - self.vlc_media_player.set_time(0) - # Get audio tracks, insert in combobox - audio_tracks = self.vlc_media_player.audio_get_track_description() - self.audio_tracks_combobox.clear() - for audio_track in audio_tracks: - self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) - # Enable audio track combobox if anything is in it - if len(audio_tracks) > 0: - self.audio_tracks_combobox.setDisabled(False) - # First track is "deactivated", so set to next if it exists - if len(audio_tracks) > 1: - self.audio_tracks_combobox.setCurrentIndex(1) - # Get subtitle tracks, insert in combobox - subtitles_tracks = self.vlc_media_player.video_get_spu_description() - self.subtitle_tracks_combobox.clear() - for subtitle_track in subtitles_tracks: - self.subtitle_tracks_combobox.addItem(subtitle_track[1].decode(), subtitle_track[0]) - # Enable subtitle track combobox is anything in it - if len(subtitles_tracks) > 0: - self.subtitle_tracks_combobox.setDisabled(False) - self.vlc_media_player.audio_set_mute(False) + if self.audio_cd: + self.vlc_media = self.audio_cd_tracks.item_at_index(index) + self.vlc_media_player.set_media(self.vlc_media) + self.vlc_media_player.set_time(0) + self.vlc_media_player.play() + self.vlc_media_player.audio_set_mute(True) + if not self.media_state_wait(vlc.State.Playing): + return + # pause + self.vlc_media_player.pause() + self.vlc_media_player.set_time(0) + self.vlc_media_player.audio_set_mute(False) + self.toggle_disable_player(False) + else: + self.vlc_media_player.set_title(index) + self.vlc_media_player.set_time(0) + self.vlc_media_player.play() + self.vlc_media_player.audio_set_mute(True) + if not self.media_state_wait(vlc.State.Playing): + return + # pause + self.vlc_media_player.pause() + self.vlc_media_player.set_time(0) + # Get audio tracks, insert in combobox + audio_tracks = self.vlc_media_player.audio_get_track_description() + self.audio_tracks_combobox.clear() + for audio_track in audio_tracks: + self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) + # Enable audio track combobox if anything is in it + if len(audio_tracks) > 0: + self.audio_tracks_combobox.setDisabled(False) + # First track is "deactivated", so set to next if it exists + if len(audio_tracks) > 1: + self.audio_tracks_combobox.setCurrentIndex(1) + # Get subtitle tracks, insert in combobox + subtitles_tracks = self.vlc_media_player.video_get_spu_description() + self.subtitle_tracks_combobox.clear() + for subtitle_track in subtitles_tracks: + self.subtitle_tracks_combobox.addItem(subtitle_track[1].decode(), subtitle_track[0]) + # Enable subtitle track combobox is anything in it + if len(subtitles_tracks) > 0: + self.subtitle_tracks_combobox.setDisabled(False) + self.vlc_media_player.audio_set_mute(False) + # If a title or audio track is available the player is enabled + if self.title_combo_box.count() > 0 or len(audio_tracks) > 0: + self.toggle_disable_player(False) + # Set media length info self.playback_length = self.vlc_media_player.get_length() self.position_horizontalslider.setMaximum(self.playback_length) # setup start and end time @@ -359,9 +414,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.start_timeedit.setMaximumTime(playback_length_time) self.end_timeedit.setMaximumTime(playback_length_time) self.end_timeedit.setTime(playback_length_time) - # If a title or audio track is available the player is enabled - if self.title_combo_box.count() > 0 or len(audio_tracks) > 0: - self.toggle_disable_player(False) @QtCore.pyqtSlot(int) def on_audio_tracks_combobox_currentIndexChanged(self, index): @@ -465,11 +517,15 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): end_time.second() * 1000 + \ end_time.msec() title = self.title_combo_box.itemData(self.title_combo_box.currentIndex()) - audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) - subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) path = self.media_path_combobox.currentText() - optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str( - start_time_ms) + ':' + str(end_time_ms) + ':' + path + optical = '' + if self.audio_cd: + optical = 'optical:' + str(title) + ':-1:-1:' + str(start_time_ms) + ':' + str(end_time_ms) + ':' + path + else: + audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) + subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) + optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str( + start_time_ms) + ':' + str(end_time_ms) + ':' + path self.media_item.add_optical_clip(optical) def media_state_wait(self, media_state): @@ -513,16 +569,34 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): elif sys.platform.startswith('linux'): # Get disc devices from dbus and find the ones that are optical bus = dbus.SystemBus() - udev_manager_obj = bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks') - udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.UDisks') - for dev in udev_manager.EnumerateDevices(): - device_obj = bus.get_object("org.freedesktop.UDisks", dev) - device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) - if device_props.Get('org.freedesktop.UDisks.Device', 'DeviceIsDrive'): - drive_props = device_props.Get('org.freedesktop.UDisks.Device', 'DriveMediaCompatibility') - if any('optical' in prop for prop in drive_props): - self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', - 'DeviceFile')) + try: + udev_manager_obj = bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks') + udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.UDisks') + for dev in udev_manager.EnumerateDevices(): + device_obj = bus.get_object("org.freedesktop.UDisks", dev) + device_props = dbus.Interface(device_obj, dbus.PROPERTIES_IFACE) + if device_props.Get('org.freedesktop.UDisks.Device', 'DeviceIsDrive'): + drive_props = device_props.Get('org.freedesktop.UDisks.Device', 'DriveMediaCompatibility') + if any('optical' in prop for prop in drive_props): + self.media_path_combobox.addItem(device_props.Get('org.freedesktop.UDisks.Device', + 'DeviceFile')) + return + except dbus.exceptions.DBusException: + log.debug('could not use udisks, will try udisks2') + udev_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2') + udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.DBus.ObjectManager') + for k,v in udev_manager.GetManagedObjects().items(): + drive_info = v.get('org.freedesktop.UDisks2.Drive', {}) + drive_props = drive_info.get('MediaCompatibility') + if drive_props and any('optical' in prop for prop in drive_props): + for device in udev_manager.GetManagedObjects().values(): + if dbus.String('org.freedesktop.UDisks2.Block') in device: + if device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('Drive')] == k: + block_file = '' + for c in device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('PreferredDevice')]: + if chr(c) != '\x00': + block_file += chr(c) + self.media_path_combobox.addItem(block_file) elif sys.platform.startswith('darwin'): # Look for DVD folders in devices to find optical devices volumes = os.listdir('/Volumes') diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 626ef36c9..ca8690d75 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -428,6 +428,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :return: Time string in format: hh.mm.ss,ttt """ seconds, millis = divmod(milliseconds, 1000) - minutes, seconds = divmod(millis, 60) + minutes, seconds = divmod(seconds, 60) hours, minutes = divmod(minutes, 60) return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis) From 2a1a93d319221f5335e7451d12b34400ae71e312 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 14 Jun 2014 23:48:07 +0200 Subject: [PATCH 19/66] Made it possible to name optical clips, and fixed some issues. --- openlp/core/ui/media/__init__.py | 23 ++++++-- openlp/core/ui/media/mediacontroller.py | 17 +++++- .../media/forms/mediaclipselectorform.py | 54 +++++++++++++++---- openlp/plugins/media/lib/mediaitem.py | 47 ++++++++-------- 4 files changed, 105 insertions(+), 36 deletions(-) diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 1d8ee3a4c..9248d03cf 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -110,6 +110,7 @@ def set_media_players(players_list, overridden_player='auto'): players = players.replace(overridden_player, '[%s]' % overridden_player) Settings().setValue('media/players', players) + def parse_optical_path(input): """ Split the optical path info. @@ -123,10 +124,24 @@ def parse_optical_path(input): subtitle_track = int(clip_info[3]) start = float(clip_info[4]) end = float(clip_info[5]) - filename = clip_info[6] - if len(clip_info) > 7: - filename += clip_info[7] - return filename, title, audio_track, subtitle_track, start, end + clip_name = clip_info[6] + filename = clip_info[7] + # Windows path usually contains a colon after the drive letter + if len(clip_info) > 8: + filename += clip_info[8] + return filename, title, audio_track, subtitle_track, start, end, clip_name + + +def format_milliseconds(milliseconds): + """ + Format milliseconds into a human readable time string. + :param milliseconds: Milliseconds to format + :return: Time string in format: hh.mm.ss,ttt + """ + seconds, millis = divmod(milliseconds, 1000) + minutes, seconds = divmod(seconds, 60) + hours, minutes = divmod(minutes, 60) + return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis) from .mediacontroller import MediaController from .playertab import PlayerTab diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index b25556e73..a62a9465c 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -373,7 +373,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): if service_item.is_capable(ItemCapabilities.IsOptical): log.debug('video is optical and live') path = service_item.get_frame_path() - (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path) + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path) is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) else: @@ -392,7 +392,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): if service_item.is_capable(ItemCapabilities.IsOptical): log.debug('video is optical and preview') path = service_item.get_frame_path() - (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(path) + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(path) is_valid = self.media_setup_optical(name, title, audio_track, subtitle_track, start, end, display, controller) else: @@ -456,6 +456,19 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): return True def media_setup_optical(self, filename, title, audio_track, subtitle_track, start, end, display, controller): + """ + Setup playback of optical media + + :param filename: Path of the optical device/drive. + :param title: The main/title track to play. + :param audio_track: The audio track to play. + :param subtitle_track: The subtitle track to play. + :param start: Start position in miliseconds. + :param end: End position in miliseconds. + :param display: The display to play the media. + :param controller: The media contraoller. + :return: True if setup succeded else False. + """ log.debug('media_setup_optical') if controller is None: controller = self.display_controllers[DisplayControllerType.Plugin] diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 82b3cd9ea..06f583a3e 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -43,6 +43,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import translate from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box +from openlp.core.ui.media import format_milliseconds try: from openlp.core.ui.media.vendor import vlc except (ImportError, NameError, NotImplementedError): @@ -156,7 +157,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): def detect_audio_cd(self, path): """ Detects is the given path is an audio CD - + :param path: Path to the device to be tested. :return: True if it was an audio CD else False. """ @@ -166,7 +167,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.play() # Wait for media to start playing. In this case VLC actually returns an error. self.media_state_wait(vlc.State.Playing) - self.vlc_media_player.pause() + self.vlc_media_player.set_pause(1) # If subitems exists, this is a CD self.audio_cd_tracks = self.vlc_media.subitems() if not self.audio_cd_tracks or self.audio_cd_tracks.count() < 1: @@ -231,7 +232,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): 'VLC player failed playing the media')) self.toggle_disable_load_media(False) return - self.vlc_media_player.pause() + self.vlc_media_player.set_pause(1) self.vlc_media_player.set_time(0) if not self.audio_cd: # Get titles, insert in combobox @@ -247,6 +248,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # Enable audio track combobox if anything is in it if len(titles) > 0: self.title_combo_box.setDisabled(False) + self.vlc_media_player.set_pause(1) self.toggle_disable_load_media(False) @QtCore.pyqtSlot(bool) @@ -367,7 +369,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if not self.media_state_wait(vlc.State.Playing): return # pause - self.vlc_media_player.pause() + self.vlc_media_player.set_pause(1) self.vlc_media_player.set_time(0) self.vlc_media_player.audio_set_mute(False) self.toggle_disable_player(False) @@ -379,7 +381,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if not self.media_state_wait(vlc.State.Playing): return # pause - self.vlc_media_player.pause() + self.vlc_media_player.set_pause(1) self.vlc_media_player.set_time(0) # Get audio tracks, insert in combobox audio_tracks = self.vlc_media_player.audio_get_track_description() @@ -520,12 +522,38 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): path = self.media_path_combobox.currentText() optical = '' if self.audio_cd: - optical = 'optical:' + str(title) + ':-1:-1:' + str(start_time_ms) + ':' + str(end_time_ms) + ':' + path + optical = 'optical:' + str(title) + ':-1:-1:' + str(start_time_ms) + ':' + str(end_time_ms) + ':' else: audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str( - start_time_ms) + ':' + str(end_time_ms) + ':' + path + start_time_ms) + ':' + str(end_time_ms) + ':' + # Ask for an alternative name for the mediaclip + while True: + new_optical_name, ok = QtGui.QInputDialog.getText(self, translate('MediaPlugin.MediaClipSelectorForm', + 'Set name of mediaclip'), + translate('MediaPlugin.MediaClipSelectorForm', + 'Name of mediaclip:'), + QtGui.QLineEdit.Normal) + # User pressed cancel, don't save the clip + if not ok: + return + # User pressed ok, but the input text is blank + if not new_optical_name: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', + 'Enter a valid name or cancel'), + translate('MediaPlugin.MediaClipSelectorForm', + 'Enter a valid name or cancel')) + # The entered new name contains a colon, which we don't allow because colons is used to seperate clip info + elif new_optical_name.find(':') >= 0: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'Invalid character'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The name of the mediaclip must not contain the character ":"')) + # New name entered and we use it + else: + break + # Append the new name to the optical string and the path + optical += new_optical_name + ':' + path self.media_item.add_optical_clip(optical) def media_state_wait(self, media_state): @@ -585,7 +613,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): log.debug('could not use udisks, will try udisks2') udev_manager_obj = bus.get_object('org.freedesktop.UDisks2', '/org/freedesktop/UDisks2') udev_manager = dbus.Interface(udev_manager_obj, 'org.freedesktop.DBus.ObjectManager') - for k,v in udev_manager.GetManagedObjects().items(): + for k, v in udev_manager.GetManagedObjects().items(): drive_info = v.get('org.freedesktop.UDisks2.Drive', {}) drive_props = drive_info.get('MediaCompatibility') if drive_props and any('optical' in prop for prop in drive_props): @@ -593,7 +621,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if dbus.String('org.freedesktop.UDisks2.Block') in device: if device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('Drive')] == k: block_file = '' - for c in device[dbus.String('org.freedesktop.UDisks2.Block')][dbus.String('PreferredDevice')]: + for c in device[dbus.String('org.freedesktop.UDisks2.Block')][ + dbus.String('PreferredDevice')]: if chr(c) != '\x00': block_file += chr(c) self.media_path_combobox.addItem(block_file) @@ -605,5 +634,12 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if volume.startswith('.'): continue dirs = os.listdir('/Volumes/' + volume) + # Detect DVD if 'VIDEO_TS' in dirs: self.media_path_combobox.addItem('/Volumes/' + volume) + # Detect audio cd + files = [f for f in dirs if os.path.isfile(f)] + for file in files: + if file.endswith('aiff'): + self.media_path_combobox.addItem('/Volumes/' + volume) + break diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index ca8690d75..a47609ea1 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -39,7 +39,7 @@ from openlp.core.lib import ItemCapabilities, MediaManagerItem, MediaType, Servi build_icon, check_item_selected from openlp.core.lib.ui import critical_error_message_box, create_horizontal_adjusting_combo_box from openlp.core.ui import DisplayController, Display, DisplayControllerType -from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path +from openlp.core.ui.media import get_media_players, set_media_players, parse_optical_path, format_milliseconds from openlp.core.utils import get_locale_key from openlp.core.ui.media.vlcplayer import VLC_AVAILABLE if VLC_AVAILABLE: @@ -125,9 +125,20 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ Adds buttons to the start of the header bar. """ - self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text='Load CD/DVD', - tooltip='Load CD/DVD', triggers=self.on_load_optical) - if not VLC_AVAILABLE: + print(get_media_players()[0]) + if 'vlc' in get_media_players()[0]: + diable_optical_button_text = False + optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + optical_button_tooltip = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + else: + diable_optical_button_text = True + optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD') + optical_button_tooltip = translate('MediaPlugin.MediaItem', + 'Load CD/DVD - only supported when VLC is installed and enabled') + self.load_optical = self.toolbar.add_toolbar_action('load_optical', icon=OPTICAL_ICON, text=optical_button_text, + tooltip=optical_button_tooltip, + triggers=self.on_load_optical) + if diable_optical_button_text: self.load_optical.setDisabled(True) def add_end_header_bar(self): @@ -224,7 +235,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): filename = item.data(QtCore.Qt.UserRole) # Special handling if the filename is a optical clip if filename.startswith('optical:'): - (name, title, audio_track, subtitle_track, start, end) = parse_optical_path(filename) + (name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(filename) if not os.path.exists(name): if not remote: # Optical disc is no longer present @@ -234,7 +245,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): return False service_item.processor = self.display_type_combo_box.currentText() service_item.add_from_command(filename, name, CLAPPERBOARD) - service_item.title = name + '@' + self.format_milliseconds(start) + '-' + self.format_milliseconds(end) + service_item.title = clip_name # Only set start and end times if going to a service #if context == ServiceItemContext.Service: # Set the length @@ -345,12 +356,11 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): track_info = QtCore.QFileInfo(track) if track.startswith('optical:'): # Handle optical based item - (file_name, title, audio_track, subtitle_track, start, end) = parse_optical_path(track) - optical = file_name + '@' + self.format_milliseconds(start) + '-' + self.format_milliseconds(end) - item_name = QtGui.QListWidgetItem(optical) + (file_name, title, audio_track, subtitle_track, start, end, clip_name) = parse_optical_path(track) + item_name = QtGui.QListWidgetItem(clip_name) item_name.setIcon(OPTICAL_ICON) item_name.setData(QtCore.Qt.UserRole, track) - item_name.setToolTip(optical) + item_name.setToolTip(file_name + '@' + format_milliseconds(start) + '-' + format_milliseconds(end)) elif not os.path.exists(track): # File doesn't exist, mark as error. file_name = os.path.split(str(track))[1] @@ -417,17 +427,12 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): :param optical: The clip to add. """ full_list = self.get_file_list() + # If the clip already is in the media list it isn't added and an error message is displayed. + if optical in full_list: + critical_error_message_box(translate('MediaPlugin.MediaItem', 'Mediaclip already saved'), + translate('MediaPlugin.MediaItem', 'This mediaclip has already been saved')) + return + # Append the optical string to the media list full_list.append(optical) self.load_list([optical]) Settings().setValue(self.settings_section + '/media files', self.get_file_list()) - - def format_milliseconds(self, milliseconds): - """ - Format milliseconds into a human readable time string. - :param milliseconds: Milliseconds to format - :return: Time string in format: hh.mm.ss,ttt - """ - seconds, millis = divmod(milliseconds, 1000) - minutes, seconds = divmod(seconds, 60) - hours, minutes = divmod(minutes, 60) - return "%02d:%02d:%02d,%03d" % (hours, minutes, seconds, millis) From 346c9e475a68bdfd51850174caa646fc41318ee1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 15 Jun 2014 21:33:05 +0200 Subject: [PATCH 20/66] updated test --- tests/resources/serviceitem-dvd.osj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/resources/serviceitem-dvd.osj b/tests/resources/serviceitem-dvd.osj index cda95e592..997dbd539 100644 --- a/tests/resources/serviceitem-dvd.osj +++ b/tests/resources/serviceitem-dvd.osj @@ -1 +1 @@ -[{"serviceitem": {"header": {"auto_play_slides_once": false, "data": "", "processor": "Automatic", "theme": -1, "theme_overwritten": false, "end_time": 672.069, "start_time": 654.375, "capabilities": [12, 18, 16, 4], "media_length": 17.694, "audit": "", "xml_version": null, "title": "/dev/sr0@00:06:15,375-00:01:09,069", "auto_play_slides_loop": false, "notes": "", "icon": ":/plugins/plugin_media.png", "type": 3, "background_audio": [], "plugin": "media", "from_plugin": false, "search": "", "will_auto_start": false, "name": "media", "footer": [], "timed_slide_interval": 0}, "data": [{"image": ":/media/slidecontroller_multimedia.png", "path": "optical:1:5:3:654375:672069:/dev/sr0", "title": "/dev/sr0"}]}}] +[{"serviceitem": {"header": {"auto_play_slides_once": false, "data": "", "processor": "Automatic", "theme": -1, "theme_overwritten": false, "end_time": 672.069, "start_time": 654.375, "capabilities": [12, 18, 16, 4], "media_length": 17.694, "audit": "", "xml_version": null, "title": "First DVD Clip", "auto_play_slides_loop": false, "notes": "", "icon": ":/plugins/plugin_media.png", "type": 3, "background_audio": [], "plugin": "media", "from_plugin": false, "search": "", "will_auto_start": false, "name": "media", "footer": [], "timed_slide_interval": 0}, "data": [{"image": ":/media/slidecontroller_multimedia.png", "path": "optical:1:5:3:654375:672069:First DVD Clip:/dev/sr0", "title": "/dev/sr0"}]}}] From 956af8706bde2fc24c4b4d92e3149947f7b56a56 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 17 Jun 2014 09:27:12 +0200 Subject: [PATCH 21/66] Small suggested changes --- openlp/core/lib/serviceitem.py | 2 +- openlp/core/ui/media/__init__.py | 1 + openlp/plugins/media/forms/mediaclipselectordialog.py | 9 +++++---- openlp/plugins/media/forms/mediaclipselectorform.py | 7 +++---- openlp/plugins/media/lib/mediaitem.py | 5 +---- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index 6aba66252..ecd6ca5bd 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -645,4 +645,4 @@ class ServiceItem(RegistryProperties): file_suffix = frame['title'].split('.')[-1] if file_suffix.lower() not in suffix_list: self.is_valid = False - break \ No newline at end of file + break diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 9248d03cf..2c422dbd0 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -118,6 +118,7 @@ def parse_optical_path(input): :param input: The string to parse :return: The elements extracted from the string: filename, title, audio_track, subtitle_track, start, end """ + log.debug('parse_optical_path, about to parse: "%s"' % input) clip_info = input.split(sep=':') title = int(clip_info[1]) audio_track = int(clip_info[2]) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index beed9e8e3..ad23aa83e 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -31,6 +31,7 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import translate + class Ui_MediaClipSelector(object): def setupUi(self, MediaClipSelector): MediaClipSelector.setObjectName("MediaClipSelector") @@ -165,7 +166,6 @@ class Ui_MediaClipSelector(object): self.position_horizontalslider.setInvertedAppearance(False) self.position_horizontalslider.setObjectName("position_horizontalslider") self.gridLayout.addWidget(self.position_horizontalslider, 6, 1, 1, 3) - #MediaClipSelector.setCentralWidget(self.centralwidget) self.retranslateUi(MediaClipSelector) QtCore.QMetaObject.connectSlotsByName(MediaClipSelector) @@ -189,11 +189,13 @@ class Ui_MediaClipSelector(object): MediaClipSelector.setWindowTitle(translate("MediaPlugin.MediaClipSelector", "Select media clip", None)) self.start_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) self.end_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) - self.set_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as start point", None)) + self.set_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", + "Set current position as start point", None)) self.load_disc_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Load disc", None)) self.end_point_label.setText(translate("MediaPlugin.MediaClipSelector", "End point", None)) self.title_label.setText(translate("MediaPlugin.MediaClipSelector", "Title", None)) - self.set_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Set current position as end point", None)) + self.set_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", + "Set current position as end point", None)) self.save_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Save current clip", None)) self.close_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Close", None)) self.start_point_label.setText(translate("MediaPlugin.MediaClipSelector", "Start point", None)) @@ -203,4 +205,3 @@ class Ui_MediaClipSelector(object): self.subtitle_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Subtitle track", None)) self.jump_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to end point", None)) self.media_path_label.setText(translate("MediaPlugin.MediaClipSelector", "Media path", None)) - diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 06f583a3e..045fb390d 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -196,7 +196,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.disable_all() path = self.media_path_combobox.currentText() # Check if given path is non-empty and exists before starting VLC - if path == '': + if not path: log.debug('no given path') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) self.toggle_disable_load_media(False) @@ -522,12 +522,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): path = self.media_path_combobox.currentText() optical = '' if self.audio_cd: - optical = 'optical:' + str(title) + ':-1:-1:' + str(start_time_ms) + ':' + str(end_time_ms) + ':' + optical = 'optical:%d:-1:-1:%d:%d:' % (title, start_time_ms, end_time_ms) else: audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) - optical = 'optical:' + str(title) + ':' + str(audio_track) + ':' + str(subtitle_track) + ':' + str( - start_time_ms) + ':' + str(end_time_ms) + ':' + optical = 'optical:%d:%d:%d:%d:%d:' % (title, audio_track, subtitle_track, start_time_ms, end_time_ms) # Ask for an alternative name for the mediaclip while True: new_optical_name, ok = QtGui.QInputDialog.getText(self, translate('MediaPlugin.MediaClipSelectorForm', diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index a47609ea1..5bdbe4d33 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -125,7 +125,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ Adds buttons to the start of the header bar. """ - print(get_media_players()[0]) if 'vlc' in get_media_players()[0]: diable_optical_button_text = False optical_button_text = translate('MediaPlugin.MediaItem', 'Load CD/DVD') @@ -246,8 +245,6 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): service_item.processor = self.display_type_combo_box.currentText() service_item.add_from_command(filename, name, CLAPPERBOARD) service_item.title = clip_name - # Only set start and end times if going to a service - #if context == ServiceItemContext.Service: # Set the length self.media_controller.media_setup_optical(name, title, audio_track, subtitle_track, start, end, None, None) service_item.set_media_length((end - start) / 1000) @@ -360,7 +357,7 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): item_name = QtGui.QListWidgetItem(clip_name) item_name.setIcon(OPTICAL_ICON) item_name.setData(QtCore.Qt.UserRole, track) - item_name.setToolTip(file_name + '@' + format_milliseconds(start) + '-' + format_milliseconds(end)) + item_name.setToolTip('%s@%s-%s' % (file_name, format_milliseconds(start), format_milliseconds(end))) elif not os.path.exists(track): # File doesn't exist, mark as error. file_name = os.path.split(str(track))[1] From 05f4f2d72e2fad9e63926d59762b97e9cc3f152e Mon Sep 17 00:00:00 2001 From: Phill Ridout Date: Mon, 30 Jun 2014 21:54:11 +0100 Subject: [PATCH 22/66] fixed tests --- tests/functional/openlp_core_lib/test_file_dialog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py index ab7663a83..875e422da 100644 --- a/tests/functional/openlp_core_lib/test_file_dialog.py +++ b/tests/functional/openlp_core_lib/test_file_dialog.py @@ -65,11 +65,11 @@ class TestFileDialog(TestCase): # THEN: os.path.exists should have been called with known args. QmessageBox.information should have been # called. The returned result should correlate with the input. - self.mocked_os.path.exists.assert_callde_with('/Valid File') - self.mocked_os.path.exists.assert_callde_with('/url%20encoded%20file%20%231') - self.mocked_os.path.exists.assert_callde_with('/url encoded file #1') - self.mocked_os.path.exists.assert_callde_with('/non-existing') - self.mocked_os.path.exists.assert_callde_with('/non-existing') + self.mocked_os.path.exists.assert_any_call('/Valid File') + self.mocked_os.path.exists.assert_any_call('/url%20encoded%20file%20%231') + self.mocked_os.path.exists.assert_any_call('/url encoded file #1') + self.mocked_os.path.exists.assert_any_call('/non-existing') + self.mocked_os.path.exists.assert_any_call('/non-existing') self.mocked_qt_gui.QmessageBox.information.called_with(self.mocked_parent, UiStrings().FileNotFound, UiStrings().FileNotFoundMessage % '/non-existing') self.assertEqual(result, ['/Valid File', '/url encoded file #1'], 'The returned file list is incorrect') From 9b6979b840f393a3f31f30213260147a68397c60 Mon Sep 17 00:00:00 2001 From: Phill Ridout Date: Mon, 30 Jun 2014 21:59:22 +0100 Subject: [PATCH 23/66] Changed log.warn to log.warning --- openlp/core/lib/toolbar.py | 2 +- openlp/core/lib/ui.py | 6 +++--- openlp/core/ui/media/mediacontroller.py | 2 +- openlp/core/utils/actions.py | 4 ++-- .../plugins/bibles/forms/bibleupgradeform.py | 12 ++++++------ openlp/plugins/bibles/lib/http.py | 4 ++-- openlp/plugins/bibles/lib/opensong.py | 2 +- .../presentations/lib/impresscontroller.py | 18 +++++++++--------- .../presentations/lib/messagelistener.py | 4 ++-- .../presentations/presentationplugin.py | 4 ++-- openlp/plugins/songs/lib/songshowplusimport.py | 2 +- .../songs/lib/worshipcenterproimport.py | 2 +- 12 files changed, 31 insertions(+), 31 deletions(-) diff --git a/openlp/core/lib/toolbar.py b/openlp/core/lib/toolbar.py index b1cc7b249..b24be89a8 100644 --- a/openlp/core/lib/toolbar.py +++ b/openlp/core/lib/toolbar.py @@ -81,4 +81,4 @@ class OpenLPToolbar(QtGui.QToolBar): if handle in self.actions: self.actions[handle].setVisible(visible) else: - log.warn('No handle "%s" in actions list.', str(handle)) + log.warning('No handle "%s" in actions list.', str(handle)) diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index a1e37abcf..cbc35e28d 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -172,7 +172,7 @@ def create_button(parent, name, **kwargs): kwargs.setdefault('icon', ':/services/service_down.png') kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.')) else: - log.warn('The role "%s" is not defined in create_push_button().', role) + log.warning('The role "%s" is not defined in create_push_button().', role) if kwargs.pop('btn_class', '') == 'toolbutton': button = QtGui.QToolButton(parent) else: @@ -190,7 +190,7 @@ def create_button(parent, name, **kwargs): button.clicked.connect(kwargs.pop('click')) for key in list(kwargs.keys()): if key not in ['text', 'icon', 'tooltip', 'click']: - log.warn('Parameter %s was not consumed in create_button().', key) + log.warning('Parameter %s was not consumed in create_button().', key) return button @@ -275,7 +275,7 @@ def create_action(parent, name, **kwargs): action.triggered.connect(kwargs.pop('triggers')) for key in list(kwargs.keys()): if key not in ['text', 'icon', 'tooltip', 'statustip', 'checked', 'can_shortcuts', 'category', 'triggers']: - log.warn('Parameter %s was not consumed in create_action().' % key) + log.warning('Parameter %s was not consumed in create_action().' % key) return action diff --git a/openlp/core/ui/media/mediacontroller.py b/openlp/core/ui/media/mediacontroller.py index 596b618cb..f086b80b2 100644 --- a/openlp/core/ui/media/mediacontroller.py +++ b/openlp/core/ui/media/mediacontroller.py @@ -175,7 +175,7 @@ class MediaController(RegistryMixin, OpenLPMixin, RegistryProperties): # On some platforms importing vlc.py might cause # also OSError exceptions. (e.g. Mac OS X) except (ImportError, OSError): - log.warn('Failed to import %s on path %s', module_name, path) + log.warning('Failed to import %s on path %s', module_name, path) player_classes = MediaPlayer.__subclasses__() for player_class in player_classes: player = player_class(self) diff --git a/openlp/core/utils/actions.py b/openlp/core/utils/actions.py index d81e16b2e..fb794866b 100644 --- a/openlp/core/utils/actions.py +++ b/openlp/core/utils/actions.py @@ -279,7 +279,7 @@ class ActionList(object): actions.append(action) ActionList.shortcut_map[shortcuts[1]] = actions else: - log.warn('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % + log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % (shortcuts[1], action.objectName())) shortcuts.remove(shortcuts[1]) # Check the primary shortcut. @@ -290,7 +290,7 @@ class ActionList(object): actions.append(action) ActionList.shortcut_map[shortcuts[0]] = actions else: - log.warn('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % + log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % (shortcuts[0], action.objectName())) shortcuts.remove(shortcuts[0]) action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts]) diff --git a/openlp/plugins/bibles/forms/bibleupgradeform.py b/openlp/plugins/bibles/forms/bibleupgradeform.py index 09c0942b7..2b0b57695 100644 --- a/openlp/plugins/bibles/forms/bibleupgradeform.py +++ b/openlp/plugins/bibles/forms/bibleupgradeform.py @@ -423,7 +423,7 @@ class BibleUpgradeForm(OpenLPWizard): else: language_id = self.new_bibles[number].get_language(name) if not language_id: - log.warn('Upgrading from "%s" failed' % filename[0]) + log.warning('Upgrading from "%s" failed' % filename[0]) self.new_bibles[number].session.close() del self.new_bibles[number] self.increment_progress_bar( @@ -444,7 +444,7 @@ class BibleUpgradeForm(OpenLPWizard): book_ref_id = self.new_bibles[number].\ get_book_ref_id_by_name(book, len(books), language_id) if not book_ref_id: - log.warn('Upgrading books from %s - download name: "%s" aborted by user' % ( + log.warning('Upgrading books from %s - download name: "%s" aborted by user' % ( meta_data['download_source'], meta_data['download_name'])) self.new_bibles[number].session.close() del self.new_bibles[number] @@ -457,7 +457,7 @@ class BibleUpgradeForm(OpenLPWizard): if oldbook: verses = old_bible.get_verses(oldbook['id']) if not verses: - log.warn('No verses found to import for book "%s"', book) + log.warning('No verses found to import for book "%s"', book) continue for verse in verses: if self.stop_import_flag: @@ -472,7 +472,7 @@ class BibleUpgradeForm(OpenLPWizard): if not language_id: language_id = self.new_bibles[number].get_language(name) if not language_id: - log.warn('Upgrading books from "%s" failed' % name) + log.warning('Upgrading books from "%s" failed' % name) self.new_bibles[number].session.close() del self.new_bibles[number] self.increment_progress_bar( @@ -493,7 +493,7 @@ class BibleUpgradeForm(OpenLPWizard): (number + 1, max_bibles, name, book['name'])) book_ref_id = self.new_bibles[number].get_book_ref_id_by_name(book['name'], len(books), language_id) if not book_ref_id: - log.warn('Upgrading books from %s " failed - aborted by user' % name) + log.warning('Upgrading books from %s " failed - aborted by user' % name) self.new_bibles[number].session.close() del self.new_bibles[number] self.success[number] = False @@ -503,7 +503,7 @@ class BibleUpgradeForm(OpenLPWizard): book_details['testament_id']) verses = old_bible.get_verses(book['id']) if not verses: - log.warn('No verses found to import for book "%s"', book['name']) + log.warning('No verses found to import for book "%s"', book['name']) self.new_bibles[number].delete_book(db_book) continue for verse in verses: diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 64b024639..8be0dbb5f 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -165,7 +165,7 @@ class BGExtract(RegistryProperties): if len(verse_parts) > 1: verse = int(verse_parts[0]) except TypeError: - log.warn('Illegal verse number: %s', str(verse)) + log.warning('Illegal verse number: %s', str(verse)) verses.append((verse, text)) verse_list = {} for verse, text in verses[::-1]: @@ -198,7 +198,7 @@ class BGExtract(RegistryProperties): if len(verse_parts) > 1: clean_verse_num = int(verse_parts[0]) except TypeError: - log.warn('Illegal verse number: %s', str(raw_verse_num)) + log.warning('Illegal verse number: %s', str(raw_verse_num)) if clean_verse_num: verse_text = raw_verse_num.next_element part = raw_verse_num.next_element.next_element diff --git a/openlp/plugins/bibles/lib/opensong.py b/openlp/plugins/bibles/lib/opensong.py index fa8323d7f..ecd1f718e 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -123,7 +123,7 @@ class OpenSongBible(BibleDB): if len(verse_parts) > 1: number = int(verse_parts[0]) except TypeError: - log.warn('Illegal verse number: %s', str(verse.attrib['n'])) + log.warning('Illegal verse number: %s', str(verse.attrib['n'])) verse_number = number else: verse_number += 1 diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 584c1401f..1d5e111c9 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -129,7 +129,7 @@ class ImpressController(PresentationController): try: uno_instance = get_uno_instance(resolver) except: - log.warn('Unable to find running instance ') + log.warning('Unable to find running instance ') self.start_process() loop += 1 try: @@ -138,7 +138,7 @@ class ImpressController(PresentationController): desktop = self.manager.createInstanceWithContext("com.sun.star.frame.Desktop", uno_instance) return desktop except: - log.warn('Failed to get UNO desktop') + log.warning('Failed to get UNO desktop') return None def get_com_desktop(self): @@ -152,7 +152,7 @@ class ImpressController(PresentationController): try: desktop = self.manager.createInstance('com.sun.star.frame.Desktop') except (AttributeError, pywintypes.com_error): - log.warn('Failure to find desktop - Impress may have closed') + log.warning('Failure to find desktop - Impress may have closed') return desktop if desktop else None def get_com_servicemanager(self): @@ -163,7 +163,7 @@ class ImpressController(PresentationController): try: return Dispatch('com.sun.star.ServiceManager') except pywintypes.com_error: - log.warn('Failed to get COM service manager. Impress Controller has been disabled') + log.warning('Failed to get COM service manager. Impress Controller has been disabled') return None def kill(self): @@ -180,7 +180,7 @@ class ImpressController(PresentationController): else: desktop = self.get_com_desktop() except: - log.warn('Failed to find an OpenOffice desktop to terminate') + log.warning('Failed to find an OpenOffice desktop to terminate') if not desktop: return docs = desktop.getComponents() @@ -198,7 +198,7 @@ class ImpressController(PresentationController): desktop.terminate() log.debug('OpenOffice killed') except: - log.warn('Failed to terminate OpenOffice') + log.warning('Failed to terminate OpenOffice') class ImpressDocument(PresentationDocument): @@ -244,7 +244,7 @@ class ImpressDocument(PresentationDocument): try: self.document = desktop.loadComponentFromURL(url, '_blank', 0, properties) except: - log.warn('Failed to load presentation %s' % url) + log.warning('Failed to load presentation %s' % url) return False if os.name == 'nt': # As we can't start minimized the Impress window gets in the way. @@ -318,7 +318,7 @@ class ImpressDocument(PresentationDocument): self.presentation = None self.document.dispose() except: - log.warn("Closing presentation failed") + log.warning("Closing presentation failed") self.document = None self.controller.remove_doc(self) @@ -335,7 +335,7 @@ class ImpressDocument(PresentationDocument): log.debug("getPresentation failed to find a presentation") return False except: - log.warn("getPresentation failed to find a presentation") + log.warning("getPresentation failed to find a presentation") return False return True diff --git a/openlp/plugins/presentations/lib/messagelistener.py b/openlp/plugins/presentations/lib/messagelistener.py index 724282eb4..ac115228a 100644 --- a/openlp/plugins/presentations/lib/messagelistener.py +++ b/openlp/plugins/presentations/lib/messagelistener.py @@ -98,7 +98,7 @@ class Controller(object): return True if not self.doc.is_loaded(): if not self.doc.load_presentation(): - log.warn('Failed to activate %s' % self.doc.filepath) + log.warning('Failed to activate %s' % self.doc.filepath) return False if self.is_live: self.doc.start_presentation() @@ -109,7 +109,7 @@ class Controller(object): if self.doc.is_active(): return True else: - log.warn('Failed to activate %s' % self.doc.filepath) + log.warning('Failed to activate %s' % self.doc.filepath) return False def slide(self, slide): diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index 5e0d7395d..7f080df22 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -90,7 +90,7 @@ class PresentationPlugin(Plugin): try: self.controllers[controller].start_process() except Exception: - log.warn('Failed to start controller process') + log.warning('Failed to start controller process') self.controllers[controller].available = False self.media_item.build_file_mask_string() @@ -134,7 +134,7 @@ class PresentationPlugin(Plugin): try: __import__(module_name, globals(), locals(), []) except ImportError: - log.warn('Failed to import %s on path %s', module_name, path) + log.warning('Failed to import %s on path %s', module_name, path) controller_classes = PresentationController.__subclasses__() for controller_class in controller_classes: controller = controller_class(self) diff --git a/openlp/plugins/songs/lib/songshowplusimport.py b/openlp/plugins/songs/lib/songshowplusimport.py index aebded029..b57a9bac1 100644 --- a/openlp/plugins/songs/lib/songshowplusimport.py +++ b/openlp/plugins/songs/lib/songshowplusimport.py @@ -152,7 +152,7 @@ class SongShowPlusImport(SongImport): if match: self.ccli_number = int(match.group()) else: - log.warn("Can't parse CCLI Number from string: %s" % self.decode(data)) + log.warning("Can't parse CCLI Number from string: %s" % self.decode(data)) elif block_key == VERSE: self.add_verse(self.decode(data), "%s%s" % (VerseType.tags[VerseType.Verse], verse_no)) elif block_key == CHORUS: diff --git a/openlp/plugins/songs/lib/worshipcenterproimport.py b/openlp/plugins/songs/lib/worshipcenterproimport.py index b24d2ae83..8c32870a1 100644 --- a/openlp/plugins/songs/lib/worshipcenterproimport.py +++ b/openlp/plugins/songs/lib/worshipcenterproimport.py @@ -58,7 +58,7 @@ class WorshipCenterProImport(SongImport): try: conn = pyodbc.connect('DRIVER={Microsoft Access Driver (*.mdb)};DBQ=%s' % self.import_source) except (pyodbc.DatabaseError, pyodbc.IntegrityError, pyodbc.InternalError, pyodbc.OperationalError) as e: - log.warn('Unable to connect the WorshipCenter Pro database %s. %s', self.import_source, str(e)) + log.warning('Unable to connect the WorshipCenter Pro database %s. %s', self.import_source, str(e)) # Unfortunately no specific exception type self.log_error(self.import_source, translate('SongsPlugin.WorshipCenterProImport', 'Unable to connect the WorshipCenter Pro database.')) From 154334a33259a68784c8bd011d85e556a6f59f44 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 1 Jul 2014 19:48:36 +0200 Subject: [PATCH 24/66] Fix for dvd loading --- openlp/plugins/media/forms/mediaclipselectorform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 045fb390d..c27a60f64 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -207,7 +207,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): 'Given path does not exists')) self.toggle_disable_load_media(False) return - self.vlc_media = self.vlc_instance.media_new_location(path) + self.vlc_media = self.vlc_instance.media_new_location('file://' + path) if not self.vlc_media: log.debug('vlc media player is none') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', From 3c91a6bbfd45714ebe617a5446e2063112c21291 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 1 Jul 2014 22:06:55 +0200 Subject: [PATCH 25/66] Fixed seeking when playing mediaclip --- openlp/core/ui/media/vlcplayer.py | 18 ++++++++++++++---- .../media/forms/mediaclipselectordialog.py | 1 - 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index 0c6d91078..8c957ea1d 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -246,10 +246,12 @@ class VlcPlayer(MediaPlayer): if controller.media_info.start_time > 0: log.debug('vlc play, starttime set: ' + str(controller.media_info.start_time)) start_time = controller.media_info.start_time + controller.media_info.length = controller.media_info.end_time - controller.media_info.start_time + else: + controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) self.volume(display, controller.media_info.volume) - if start_time > 0: - self.seek(display, int(start_time * 1000)) - controller.media_info.length = int(display.vlc_media_player.get_media().get_duration() / 1000) + if start_time > 0 and display.vlc_media_player.is_seekable(): + display.vlc_media_player.set_time(int(start_time * 1000)) controller.seek_slider.setMaximum(controller.media_info.length * 1000) self.state = MediaState.Playing display.vlc_widget.raise_() @@ -283,6 +285,9 @@ class VlcPlayer(MediaPlayer): """ Go to a particular position """ + if display.controller.media_info.media_type == MediaType.CD \ + or display.controller.media_info.media_type == MediaType.DVD: + seek_value += int(display.controller.media_info.start_time * 1000) if display.vlc_media_player.is_seekable(): display.vlc_media_player.set_time(seek_value) @@ -315,7 +320,12 @@ class VlcPlayer(MediaPlayer): self.set_visible(display, False) if not controller.seek_slider.isSliderDown(): controller.seek_slider.blockSignals(True) - controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) + if display.controller.media_info.media_type == MediaType.CD \ + or display.controller.media_info.media_type == MediaType.DVD: + controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time() - + int(display.controller.media_info.start_time * 1000)) + else: + controller.seek_slider.setSliderPosition(display.vlc_media_player.get_time()) controller.seek_slider.blockSignals(False) def get_info(self): diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index ad23aa83e..225be5cb0 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -166,7 +166,6 @@ class Ui_MediaClipSelector(object): self.position_horizontalslider.setInvertedAppearance(False) self.position_horizontalslider.setObjectName("position_horizontalslider") self.gridLayout.addWidget(self.position_horizontalslider, 6, 1, 1, 3) - self.retranslateUi(MediaClipSelector) QtCore.QMetaObject.connectSlotsByName(MediaClipSelector) MediaClipSelector.setTabOrder(self.media_path_combobox, self.load_disc_pushbutton) From 6f7f3be3ffa2c158742a6420a6010a86c49085de Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 3 Jul 2014 09:09:55 +0200 Subject: [PATCH 26/66] Fix optical drive detection on windows --- .../media/forms/mediaclipselectorform.py | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index c27a60f64..7216891a8 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -29,7 +29,7 @@ import os if os.name == 'nt': - from ctypes import windll + from win32com.client import Dispatch import string import sys if sys.platform.startswith('linux'): @@ -581,18 +581,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # insert empty string as first item self.media_path_combobox.addItem('') if os.name == 'nt': - # use win api to fine optical drives - bitmask = windll.kernel32.GetLogicalDrives() - for letter in string.uppercase: - if bitmask & 1: - try: - type = windll.kernel32.GetDriveTypeW('%s:\\' % letter) - # if type is 5, it is a cd-rom drive - if type == 5: - self.media_path_combobox.addItem('%s:\\' % letter) - except Exception as e: - log.debug('Exception while looking for optical drives: ', e) - bitmask >>= 1 + # use win api to find optical drives + fso = Dispatch('scripting.filesystemobject') + for drive in fso.Drives : + log.debug('Drive %s has type %d' % (drive.DriveLetter, drive.DriveType)) + # if type is 4, it is a cd-rom drive + if drive.DriveType == 4: + self.media_path_combobox.addItem('%s:\\' % drive.DriveLetter) elif sys.platform.startswith('linux'): # Get disc devices from dbus and find the ones that are optical bus = dbus.SystemBus() From d19e7bf3aa50015d4a2408e5690f87f9e50d692b Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 3 Jul 2014 11:27:54 +0200 Subject: [PATCH 27/66] Fix disc loading for windows --- openlp/plugins/media/forms/mediaclipselectordialog.py | 2 ++ openlp/plugins/media/forms/mediaclipselectorform.py | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index 225be5cb0..a25c0badb 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -204,3 +204,5 @@ class Ui_MediaClipSelector(object): self.subtitle_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Subtitle track", None)) self.jump_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to end point", None)) self.media_path_label.setText(translate("MediaPlugin.MediaClipSelector", "Media path", None)) + self.media_path_combobox.lineEdit().setPlaceholderText(translate("MediaPlugin.MediaClipSelector", + "Select drive from list", None)) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 7216891a8..8620ab9a9 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -35,6 +35,7 @@ import sys if sys.platform.startswith('linux'): import dbus import logging +import re from datetime import datetime @@ -207,7 +208,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): 'Given path does not exists')) self.toggle_disable_load_media(False) return - self.vlc_media = self.vlc_instance.media_new_location('file://' + path) + # If on windows fix path for VLC use + if os.name == 'nt': + # If the given path is in the format "D:\" or "D:", prefix it with "/" to make VLC happy + pattern = re.compile('^\w:\\\\*$') + if pattern.match(path): + path = '/' + path + self.vlc_media = self.vlc_instance.media_new_location('dvd://' + path) if not self.vlc_media: log.debug('vlc media player is none') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', @@ -583,7 +590,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if os.name == 'nt': # use win api to find optical drives fso = Dispatch('scripting.filesystemobject') - for drive in fso.Drives : + for drive in fso.Drives: log.debug('Drive %s has type %d' % (drive.DriveLetter, drive.DriveType)) # if type is 4, it is a cd-rom drive if drive.DriveType == 4: From 8a545ceaf77c01b4f8d72f5a7518186c9c0f4f06 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 3 Jul 2014 16:19:11 +0200 Subject: [PATCH 28/66] Try to fix some timing issues --- .../media/forms/mediaclipselectorform.py | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 8620ab9a9..b35f5ad1b 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -32,10 +32,12 @@ if os.name == 'nt': from win32com.client import Dispatch import string import sys + if sys.platform.startswith('linux'): import dbus import logging import re +from time import sleep from datetime import datetime @@ -239,6 +241,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): 'VLC player failed playing the media')) self.toggle_disable_load_media(False) return + # Sleep 1 second to make sure VLC has the needed metadata + self.vlc_media_player.audio_set_mute(True) + sleep(1) self.vlc_media_player.set_pause(1) self.vlc_media_player.set_time(0) if not self.audio_cd: @@ -257,6 +262,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.title_combo_box.setDisabled(False) self.vlc_media_player.set_pause(1) self.toggle_disable_load_media(False) + log.debug('leaving on_load_disc_pushbutton_clicked, vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(bool) def on_play_pushbutton_clicked(self, clicked): @@ -366,30 +372,37 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ log.debug('in on_title_combo_box_changed, index: %d', index) if not self.vlc_media_player: + log.error('vlc_media_player was None') return if self.audio_cd: self.vlc_media = self.audio_cd_tracks.item_at_index(index) self.vlc_media_player.set_media(self.vlc_media) self.vlc_media_player.set_time(0) self.vlc_media_player.play() - self.vlc_media_player.audio_set_mute(True) if not self.media_state_wait(vlc.State.Playing): + log.error('Could not start playing audio cd, needed to get track info') return + self.vlc_media_player.audio_set_mute(True) + # Sleep 1 second to make sure VLC has the needed metadata + sleep(1) # pause - self.vlc_media_player.set_pause(1) self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) self.vlc_media_player.audio_set_mute(False) self.toggle_disable_player(False) else: self.vlc_media_player.set_title(index) self.vlc_media_player.set_time(0) self.vlc_media_player.play() - self.vlc_media_player.audio_set_mute(True) if not self.media_state_wait(vlc.State.Playing): + log.error('Could not start playing dvd, needed to get track info') return + self.vlc_media_player.audio_set_mute(True) + # Sleep 1 second to make sure VLC has the needed metadata + sleep(1) # pause - self.vlc_media_player.set_pause(1) self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) # Get audio tracks, insert in combobox audio_tracks = self.vlc_media_player.audio_get_track_description() self.audio_tracks_combobox.clear() @@ -415,6 +428,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.toggle_disable_player(False) # Set media length info self.playback_length = self.vlc_media_player.get_length() + log.debug('playback_length: %d ms' % self.playback_length) self.position_horizontalslider.setMaximum(self.playback_length) # setup start and end time rounded_vlc_ms_length = int(round(self.playback_length / 100.0) * 100.0) @@ -423,6 +437,10 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.start_timeedit.setMaximumTime(playback_length_time) self.end_timeedit.setMaximumTime(playback_length_time) self.end_timeedit.setTime(playback_length_time) + # Pause once again, just to make sure + self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) + log.debug('leaving on_title_combo_box_currentIndexChanged, vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(int) def on_audio_tracks_combobox_currentIndexChanged(self, index): @@ -574,7 +592,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): while media_state != self.vlc_media_player.get_state(): if self.vlc_media_player.get_state() == vlc.State.Error: return False - if (datetime.now() - start).seconds > 15: + if (datetime.now() - start).seconds > 30: return False return True @@ -585,8 +603,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ # Clear list first self.media_path_combobox.clear() - # insert empty string as first item - self.media_path_combobox.addItem('') if os.name == 'nt': # use win api to find optical drives fso = Dispatch('scripting.filesystemobject') From 9d87fa46ca3791b181fc50d0f379126ba59e15cb Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Thu, 3 Jul 2014 20:43:35 +0200 Subject: [PATCH 29/66] Try to fix playing-when-loading --- openlp/plugins/media/forms/mediaclipselectorform.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index b35f5ad1b..77cd72c25 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -260,9 +260,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # Enable audio track combobox if anything is in it if len(titles) > 0: self.title_combo_box.setDisabled(False) - self.vlc_media_player.set_pause(1) self.toggle_disable_load_media(False) - log.debug('leaving on_load_disc_pushbutton_clicked, vlc_media_player state: %s' % self.vlc_media_player.get_state()) + log.debug('load_disc_pushbutton end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(bool) def on_play_pushbutton_clicked(self, clicked): @@ -438,9 +437,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.end_timeedit.setMaximumTime(playback_length_time) self.end_timeedit.setTime(playback_length_time) # Pause once again, just to make sure - self.vlc_media_player.set_time(0) - self.vlc_media_player.set_pause(1) - log.debug('leaving on_title_combo_box_currentIndexChanged, vlc_media_player state: %s' % self.vlc_media_player.get_state()) + loop_count = 0 + while self.vlc_media_player.get_state() == vlc.State.Playing and loop_count < 20: + sleep(0.1) + self.vlc_media_player.set_pause(1) + log.debug('title_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(int) def on_audio_tracks_combobox_currentIndexChanged(self, index): From 33e104d57d7fc82512ae91b0c8e2d712d0910519 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Fri, 4 Jul 2014 10:45:17 +0200 Subject: [PATCH 30/66] Tried to fix some linux vs windows issues. Also added a test. --- openlp/core/ui/media/__init__.py | 2 +- .../media/forms/mediaclipselectorform.py | 7 ++- tests/functional/openlp_core_ui/test_media.py | 58 ++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/openlp/core/ui/media/__init__.py b/openlp/core/ui/media/__init__.py index 2c422dbd0..a44604d2f 100644 --- a/openlp/core/ui/media/__init__.py +++ b/openlp/core/ui/media/__init__.py @@ -129,7 +129,7 @@ def parse_optical_path(input): filename = clip_info[7] # Windows path usually contains a colon after the drive letter if len(clip_info) > 8: - filename += clip_info[8] + filename += ':' + clip_info[8] return filename, title, audio_track, subtitle_track, start, end, clip_name diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 77cd72c25..11a92d9d2 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -210,13 +210,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): 'Given path does not exists')) self.toggle_disable_load_media(False) return - # If on windows fix path for VLC use + # VLC behaves a bit differently on windows and linux when loading, which creates problems when trying to + # detect if we're dealing with a DVD or CD, so we use different loading approaches depending on the OS. if os.name == 'nt': # If the given path is in the format "D:\" or "D:", prefix it with "/" to make VLC happy pattern = re.compile('^\w:\\\\*$') if pattern.match(path): path = '/' + path - self.vlc_media = self.vlc_instance.media_new_location('dvd://' + path) + self.vlc_media = self.vlc_instance.media_new_location('dvd://' + path) + else: + self.vlc_media = self.vlc_instance.media_new_path(path) if not self.vlc_media: log.debug('vlc media player is none') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', diff --git a/tests/functional/openlp_core_ui/test_media.py b/tests/functional/openlp_core_ui/test_media.py index 4c6fa7f86..fbb5787c0 100644 --- a/tests/functional/openlp_core_ui/test_media.py +++ b/tests/functional/openlp_core_ui/test_media.py @@ -32,7 +32,7 @@ Package to test the openlp.core.ui package. from PyQt4 import QtCore from unittest import TestCase -from openlp.core.ui.media import get_media_players +from openlp.core.ui.media import get_media_players, parse_optical_path from tests.functional import MagicMock, patch from tests.helpers.testmixin import TestMixin @@ -126,3 +126,59 @@ class TestMedia(TestCase, TestMixin): # THEN: the used_players should be an empty list, and the overridden player should be an empty string self.assertEqual(['vlc', 'webkit', 'phonon'], used_players, 'Used players should be correct') self.assertEqual('vlc,webkit,phonon', overridden_player, 'Overridden player should be a string of players') + + def test_parse_optical_path_linux(self): + """ + Test that test_parse_optical_path() parses a optical path with linux device path correctly + """ + + # GIVEN: An optical formatted path + org_title_track = 1 + org_audio_track = 2 + org_subtitle_track = -1 + org_start = 1234 + org_end = 4321 + org_name = 'test name' + org_device_path = '/dev/dvd' + path = 'optical:%d:%d:%d:%d:%d:%s:%s' % (org_title_track, org_audio_track, org_subtitle_track, + org_start, org_end, org_name, org_device_path) + + # WHEN: parsing the path + (device_path, title_track, audio_track, subtitle_track, start, end, name) = parse_optical_path(path) + + # THEN: The return values should match the original values + self.assertEqual(org_title_track, title_track, 'Returned title_track should match the original') + self.assertEqual(org_audio_track, audio_track, 'Returned audio_track should match the original') + self.assertEqual(org_subtitle_track, subtitle_track, 'Returned subtitle_track should match the original') + self.assertEqual(org_start, start, 'Returned start should match the original') + self.assertEqual(org_end, end, 'Returned end should match the original') + self.assertEqual(org_name, name, 'Returned end should match the original') + self.assertEqual(org_device_path, device_path, 'Returned device_path should match the original') + + def test_parse_optical_path_win(self): + """ + Test that test_parse_optical_path() parses a optical path with windows device path correctly + """ + + # GIVEN: An optical formatted path + org_title_track = 1 + org_audio_track = 2 + org_subtitle_track = -1 + org_start = 1234 + org_end = 4321 + org_name = 'test name' + org_device_path = 'D:' + path = 'optical:%d:%d:%d:%d:%d:%s:%s' % (org_title_track, org_audio_track, org_subtitle_track, + org_start, org_end, org_name, org_device_path) + + # WHEN: parsing the path + (device_path, title_track, audio_track, subtitle_track, start, end, name) = parse_optical_path(path) + + # THEN: The return values should match the original values + self.assertEqual(org_title_track, title_track, 'Returned title_track should match the original') + self.assertEqual(org_audio_track, audio_track, 'Returned audio_track should match the original') + self.assertEqual(org_subtitle_track, subtitle_track, 'Returned subtitle_track should match the original') + self.assertEqual(org_start, start, 'Returned start should match the original') + self.assertEqual(org_end, end, 'Returned end should match the original') + self.assertEqual(org_name, name, 'Returned end should match the original') + self.assertEqual(org_device_path, device_path, 'Returned device_path should match the original') From 32ff9cdeb736724caaac2e95ae79d81bdc4a3b29 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 6 Jul 2014 21:48:23 +0200 Subject: [PATCH 31/66] Patch by Samuel --- openlp/plugins/media/forms/mediaclipselectorform.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 11a92d9d2..13bad606c 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -246,9 +246,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): return # Sleep 1 second to make sure VLC has the needed metadata self.vlc_media_player.audio_set_mute(True) - sleep(1) - self.vlc_media_player.set_pause(1) - self.vlc_media_player.set_time(0) + #sleep(1) + #self.vlc_media_player.set_pause(1) + #self.vlc_media_player.set_time(0) if not self.audio_cd: # Get titles, insert in combobox titles = self.vlc_media_player.video_get_title_description() @@ -401,10 +401,10 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): return self.vlc_media_player.audio_set_mute(True) # Sleep 1 second to make sure VLC has the needed metadata - sleep(1) + #sleep(1) # pause self.vlc_media_player.set_time(0) - self.vlc_media_player.set_pause(1) + #self.vlc_media_player.set_pause(1) # Get audio tracks, insert in combobox audio_tracks = self.vlc_media_player.audio_get_track_description() self.audio_tracks_combobox.clear() @@ -444,6 +444,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): while self.vlc_media_player.get_state() == vlc.State.Playing and loop_count < 20: sleep(0.1) self.vlc_media_player.set_pause(1) + loop_count += 1 log.debug('title_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(int) From bec0071e7c22694fbf737cc734289e8a64db091d Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 7 Jul 2014 13:30:03 +0200 Subject: [PATCH 32/66] Small adjustments --- openlp/plugins/media/forms/mediaclipselectorform.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 13bad606c..51063ffbd 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -244,11 +244,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): 'VLC player failed playing the media')) self.toggle_disable_load_media(False) return - # Sleep 1 second to make sure VLC has the needed metadata self.vlc_media_player.audio_set_mute(True) - #sleep(1) - #self.vlc_media_player.set_pause(1) - #self.vlc_media_player.set_time(0) if not self.audio_cd: # Get titles, insert in combobox titles = self.vlc_media_player.video_get_title_description() @@ -401,10 +397,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): return self.vlc_media_player.audio_set_mute(True) # Sleep 1 second to make sure VLC has the needed metadata - #sleep(1) - # pause + sleep(1) self.vlc_media_player.set_time(0) - #self.vlc_media_player.set_pause(1) # Get audio tracks, insert in combobox audio_tracks = self.vlc_media_player.audio_get_track_description() self.audio_tracks_combobox.clear() @@ -425,6 +419,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if len(subtitles_tracks) > 0: self.subtitle_tracks_combobox.setDisabled(False) self.vlc_media_player.audio_set_mute(False) + self.vlc_media_player.set_pause(1) # If a title or audio track is available the player is enabled if self.title_combo_box.count() > 0 or len(audio_tracks) > 0: self.toggle_disable_player(False) From 67ab5082862cda6f248f0c0f7cf3078e760bd679 Mon Sep 17 00:00:00 2001 From: Phill Ridout Date: Sun, 20 Jul 2014 21:00:06 +0100 Subject: [PATCH 33/66] Tweeked test to use a call list --- tests/functional/openlp_core_lib/test_file_dialog.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_file_dialog.py b/tests/functional/openlp_core_lib/test_file_dialog.py index 875e422da..1190810da 100644 --- a/tests/functional/openlp_core_lib/test_file_dialog.py +++ b/tests/functional/openlp_core_lib/test_file_dialog.py @@ -5,7 +5,7 @@ from unittest import TestCase from openlp.core.common import UiStrings from openlp.core.lib.filedialog import FileDialog -from tests.functional import MagicMock, patch +from tests.functional import MagicMock, call, patch class TestFileDialog(TestCase): @@ -65,11 +65,9 @@ class TestFileDialog(TestCase): # THEN: os.path.exists should have been called with known args. QmessageBox.information should have been # called. The returned result should correlate with the input. - self.mocked_os.path.exists.assert_any_call('/Valid File') - self.mocked_os.path.exists.assert_any_call('/url%20encoded%20file%20%231') - self.mocked_os.path.exists.assert_any_call('/url encoded file #1') - self.mocked_os.path.exists.assert_any_call('/non-existing') - self.mocked_os.path.exists.assert_any_call('/non-existing') + call_list = [call('/Valid File'), call('/url%20encoded%20file%20%231'), call('/url encoded file #1'), + call('/non-existing'), call('/non-existing')] + self.mocked_os.path.exists.assert_has_calls(call_list) self.mocked_qt_gui.QmessageBox.information.called_with(self.mocked_parent, UiStrings().FileNotFound, UiStrings().FileNotFoundMessage % '/non-existing') self.assertEqual(result, ['/Valid File', '/url encoded file #1'], 'The returned file list is incorrect') From 4e6561b9c210fe56f403f678f6d2b5423aaeea6f Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 20 Aug 2014 22:09:07 +0200 Subject: [PATCH 35/66] Minor syntactic sugar --- openlp/plugins/media/forms/mediaclipselectordialog.py | 2 +- openlp/plugins/media/forms/mediaclipselectorform.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index a25c0badb..2721836fe 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -167,7 +167,7 @@ class Ui_MediaClipSelector(object): self.position_horizontalslider.setObjectName("position_horizontalslider") self.gridLayout.addWidget(self.position_horizontalslider, 6, 1, 1, 3) self.retranslateUi(MediaClipSelector) - QtCore.QMetaObject.connectSlotsByName(MediaClipSelector) + #QtCore.QMetaObject.connectSlotsByName(MediaClipSelector) MediaClipSelector.setTabOrder(self.media_path_combobox, self.load_disc_pushbutton) MediaClipSelector.setTabOrder(self.load_disc_pushbutton, self.title_combo_box) MediaClipSelector.setTabOrder(self.title_combo_box, self.audio_tracks_combobox) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 51063ffbd..5a520d992 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -72,6 +72,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): Constructor """ super(MediaClipSelectorForm, self).__init__(parent) + self.vlc_instance = None + self.vlc_media_player = None + self.vlc_media = None + self.timer = None + self.audio_cd_tracks = None + self.audio_cd = False + self.playback_length = 0 self.media_item = media_item self.setupUi(self) # most actions auto-connect due to the functions name, so only a few left to do From 4fc6025f1a936e65f71147901263946224ff4961 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Wed, 20 Aug 2014 23:33:30 +0200 Subject: [PATCH 36/66] Removed styled frame - looks ugly in KDE and is unnecessary --- openlp/plugins/media/forms/mediaclipselectordialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index 2721836fe..aa27b3d1c 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -138,7 +138,7 @@ class Ui_MediaClipSelector(object): self.media_view_frame = QtGui.QFrame(self.centralwidget) self.media_view_frame.setMinimumSize(QtCore.QSize(665, 375)) self.media_view_frame.setStyleSheet("background-color:black;") - self.media_view_frame.setFrameShape(QtGui.QFrame.StyledPanel) + self.media_view_frame.setFrameShape(QtGui.QFrame.NoFrame) self.media_view_frame.setFrameShadow(QtGui.QFrame.Raised) self.media_view_frame.setObjectName("media_view_frame") self.gridLayout.addWidget(self.media_view_frame, 5, 0, 1, 5) From 86291d0c412df1df9e1154f84922478d35fdc274 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 21 Aug 2014 01:35:24 +0200 Subject: [PATCH 37/66] Round 1 of media clip dialog refresh --- .../media/forms/mediaclipselectordialog.py | 343 +++++++++--------- .../media/forms/mediaclipselectorform.py | 98 ++--- openlp/plugins/media/lib/mediaitem.py | 12 +- openlp/plugins/songs/forms/songexportform.py | 2 +- .../media/forms/test_mediaclipselectorform.py | 12 +- 5 files changed, 237 insertions(+), 230 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index aa27b3d1c..fe62c99d3 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -30,179 +30,180 @@ from PyQt4 import QtCore, QtGui from openlp.core.common import translate +from openlp.core.lib import build_icon class Ui_MediaClipSelector(object): - def setupUi(self, MediaClipSelector): - MediaClipSelector.setObjectName("MediaClipSelector") - MediaClipSelector.resize(683, 739) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(MediaClipSelector.sizePolicy().hasHeightForWidth()) - MediaClipSelector.setSizePolicy(sizePolicy) - MediaClipSelector.setMinimumSize(QtCore.QSize(683, 686)) - MediaClipSelector.setFocusPolicy(QtCore.Qt.NoFocus) - MediaClipSelector.setAutoFillBackground(False) - MediaClipSelector.setInputMethodHints(QtCore.Qt.ImhNone) - self.centralwidget = QtGui.QWidget(MediaClipSelector) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth()) - self.centralwidget.setSizePolicy(sizePolicy) - self.centralwidget.setObjectName("centralwidget") - self.gridLayout = QtGui.QGridLayout(self.centralwidget) - self.gridLayout.setObjectName("gridLayout") - self.media_path_combobox = QtGui.QComboBox(self.centralwidget) - self.media_path_combobox.setEnabled(True) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.media_path_combobox.sizePolicy().hasHeightForWidth()) - self.media_path_combobox.setSizePolicy(sizePolicy) + def setupUi(self, media_clip_selector): + media_clip_selector.setObjectName('media_clip_selector') + media_clip_selector.resize(554, 654) + self.combobox_size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) + media_clip_selector.setSizePolicy( + QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)) + self.central_widget = QtGui.QWidget(media_clip_selector) + self.central_widget.setSizePolicy( + QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) + self.central_widget.setObjectName('central_widget') + self.main_layout = QtGui.QVBoxLayout(self.central_widget) + self.main_layout.setObjectName('main_layout') + self.central_widget.setLayout(self.main_layout) + # Source groupbox + self.source_groupbox = QtGui.QGroupBox(self.central_widget) + self.source_groupbox.setObjectName('source_groupbox') + self.source_layout = QtGui.QHBoxLayout() + self.source_layout.setObjectName('source_layout') + self.source_groupbox.setLayout(self.source_layout) + # Media path label + self.media_path_label = QtGui.QLabel(self.source_groupbox) + self.media_path_label.setObjectName('media_path_label') + self.source_layout.addWidget(self.media_path_label) + # Media path combobox + self.media_path_combobox = QtGui.QComboBox(self.source_groupbox) + # Make the combobox expand + self.media_path_combobox.setSizePolicy(self.combobox_size_policy) self.media_path_combobox.setEditable(True) - self.media_path_combobox.setObjectName("media_path_combobox") - self.gridLayout.addWidget(self.media_path_combobox, 0, 2, 1, 2) - self.start_timeedit = QtGui.QTimeEdit(self.centralwidget) - self.start_timeedit.setEnabled(True) - self.start_timeedit.setObjectName("start_timeedit") - self.gridLayout.addWidget(self.start_timeedit, 7, 2, 1, 1) - self.end_timeedit = QtGui.QTimeEdit(self.centralwidget) - self.end_timeedit.setEnabled(True) - self.end_timeedit.setObjectName("end_timeedit") - self.gridLayout.addWidget(self.end_timeedit, 8, 2, 1, 1) - self.set_start_pushbutton = QtGui.QPushButton(self.centralwidget) - self.set_start_pushbutton.setEnabled(True) - self.set_start_pushbutton.setObjectName("set_start_pushbutton") - self.gridLayout.addWidget(self.set_start_pushbutton, 7, 3, 1, 1) - self.load_disc_pushbutton = QtGui.QPushButton(self.centralwidget) - self.load_disc_pushbutton.setEnabled(True) - self.load_disc_pushbutton.setObjectName("load_disc_pushbutton") - self.gridLayout.addWidget(self.load_disc_pushbutton, 0, 4, 1, 1) - spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) - self.gridLayout.addItem(spacerItem, 9, 3, 1, 1) - self.play_pushbutton = QtGui.QPushButton(self.centralwidget) - self.play_pushbutton.setEnabled(True) - self.play_pushbutton.setText("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.play_pushbutton.setIcon(icon) - self.play_pushbutton.setObjectName("play_pushbutton") - self.gridLayout.addWidget(self.play_pushbutton, 6, 0, 1, 1) - self.end_point_label = QtGui.QLabel(self.centralwidget) - self.end_point_label.setEnabled(True) - self.end_point_label.setObjectName("end_point_label") - self.gridLayout.addWidget(self.end_point_label, 8, 0, 1, 1) - self.subtitle_tracks_combobox = QtGui.QComboBox(self.centralwidget) - self.subtitle_tracks_combobox.setEnabled(True) - self.subtitle_tracks_combobox.setObjectName("subtitle_tracks_combobox") - self.gridLayout.addWidget(self.subtitle_tracks_combobox, 4, 2, 1, 2) - self.title_label = QtGui.QLabel(self.centralwidget) - self.title_label.setEnabled(True) - self.title_label.setObjectName("title_label") - self.gridLayout.addWidget(self.title_label, 2, 0, 1, 1) - self.audio_tracks_combobox = QtGui.QComboBox(self.centralwidget) - self.audio_tracks_combobox.setEnabled(True) - self.audio_tracks_combobox.setObjectName("audio_tracks_combobox") - self.gridLayout.addWidget(self.audio_tracks_combobox, 3, 2, 1, 2) - self.set_end_pushbutton = QtGui.QPushButton(self.centralwidget) - self.set_end_pushbutton.setEnabled(True) - self.set_end_pushbutton.setObjectName("set_end_pushbutton") - self.gridLayout.addWidget(self.set_end_pushbutton, 8, 3, 1, 1) - self.save_pushbutton = QtGui.QPushButton(self.centralwidget) - self.save_pushbutton.setEnabled(True) - self.save_pushbutton.setObjectName("save_pushbutton") - self.gridLayout.addWidget(self.save_pushbutton, 10, 3, 1, 1) - self.close_pushbutton = QtGui.QPushButton(self.centralwidget) - self.close_pushbutton.setEnabled(True) - self.close_pushbutton.setObjectName("close_pushbutton") - self.gridLayout.addWidget(self.close_pushbutton, 10, 4, 1, 1) - self.start_point_label = QtGui.QLabel(self.centralwidget) - self.start_point_label.setEnabled(True) - self.start_point_label.setObjectName("start_point_label") - self.gridLayout.addWidget(self.start_point_label, 7, 0, 1, 2) - self.jump_start_pushbutton = QtGui.QPushButton(self.centralwidget) - self.jump_start_pushbutton.setEnabled(True) - self.jump_start_pushbutton.setObjectName("jump_start_pushbutton") - self.gridLayout.addWidget(self.jump_start_pushbutton, 7, 4, 1, 1) - self.audio_track_label = QtGui.QLabel(self.centralwidget) - self.audio_track_label.setEnabled(True) - self.audio_track_label.setObjectName("audio_track_label") - self.gridLayout.addWidget(self.audio_track_label, 3, 0, 1, 2) - self.media_position_timeedit = QtGui.QTimeEdit(self.centralwidget) - self.media_position_timeedit.setEnabled(True) - self.media_position_timeedit.setReadOnly(True) - self.media_position_timeedit.setObjectName("media_position_timeedit") - self.gridLayout.addWidget(self.media_position_timeedit, 6, 4, 1, 1) - self.media_view_frame = QtGui.QFrame(self.centralwidget) - self.media_view_frame.setMinimumSize(QtCore.QSize(665, 375)) - self.media_view_frame.setStyleSheet("background-color:black;") - self.media_view_frame.setFrameShape(QtGui.QFrame.NoFrame) - self.media_view_frame.setFrameShadow(QtGui.QFrame.Raised) - self.media_view_frame.setObjectName("media_view_frame") - self.gridLayout.addWidget(self.media_view_frame, 5, 0, 1, 5) - self.subtitle_track_label = QtGui.QLabel(self.centralwidget) - self.subtitle_track_label.setEnabled(True) - self.subtitle_track_label.setObjectName("subtitle_track_label") - self.gridLayout.addWidget(self.subtitle_track_label, 4, 0, 1, 2) - self.jump_end_pushbutton = QtGui.QPushButton(self.centralwidget) - self.jump_end_pushbutton.setEnabled(True) - self.jump_end_pushbutton.setObjectName("jump_end_pushbutton") - self.gridLayout.addWidget(self.jump_end_pushbutton, 8, 4, 1, 1) - self.media_path_label = QtGui.QLabel(self.centralwidget) - self.media_path_label.setEnabled(True) - self.media_path_label.setObjectName("media_path_label") - self.gridLayout.addWidget(self.media_path_label, 0, 0, 1, 2) - self.title_combo_box = QtGui.QComboBox(self.centralwidget) - self.title_combo_box.setEnabled(True) - self.title_combo_box.setProperty("currentText", "") - self.title_combo_box.setObjectName("title_combo_box") - self.gridLayout.addWidget(self.title_combo_box, 2, 2, 1, 2) - self.position_horizontalslider = QtGui.QSlider(self.centralwidget) - self.position_horizontalslider.setEnabled(True) - self.position_horizontalslider.setTracking(False) - self.position_horizontalslider.setOrientation(QtCore.Qt.Horizontal) - self.position_horizontalslider.setInvertedAppearance(False) - self.position_horizontalslider.setObjectName("position_horizontalslider") - self.gridLayout.addWidget(self.position_horizontalslider, 6, 1, 1, 3) - self.retranslateUi(MediaClipSelector) - #QtCore.QMetaObject.connectSlotsByName(MediaClipSelector) - MediaClipSelector.setTabOrder(self.media_path_combobox, self.load_disc_pushbutton) - MediaClipSelector.setTabOrder(self.load_disc_pushbutton, self.title_combo_box) - MediaClipSelector.setTabOrder(self.title_combo_box, self.audio_tracks_combobox) - MediaClipSelector.setTabOrder(self.audio_tracks_combobox, self.subtitle_tracks_combobox) - MediaClipSelector.setTabOrder(self.subtitle_tracks_combobox, self.play_pushbutton) - MediaClipSelector.setTabOrder(self.play_pushbutton, self.position_horizontalslider) - MediaClipSelector.setTabOrder(self.position_horizontalslider, self.media_position_timeedit) - MediaClipSelector.setTabOrder(self.media_position_timeedit, self.start_timeedit) - MediaClipSelector.setTabOrder(self.start_timeedit, self.set_start_pushbutton) - MediaClipSelector.setTabOrder(self.set_start_pushbutton, self.jump_start_pushbutton) - MediaClipSelector.setTabOrder(self.jump_start_pushbutton, self.end_timeedit) - MediaClipSelector.setTabOrder(self.end_timeedit, self.set_end_pushbutton) - MediaClipSelector.setTabOrder(self.set_end_pushbutton, self.jump_end_pushbutton) - MediaClipSelector.setTabOrder(self.jump_end_pushbutton, self.save_pushbutton) - MediaClipSelector.setTabOrder(self.save_pushbutton, self.close_pushbutton) + self.media_path_combobox.setObjectName('media_path_combobox') + self.source_layout.addWidget(self.media_path_combobox) + # Load disc button + self.load_disc_button = QtGui.QPushButton(self.central_widget) + self.load_disc_button.setEnabled(True) + self.load_disc_button.setObjectName('load_disc_button') + self.source_layout.addWidget(self.load_disc_button) + self.main_layout.addWidget(self.source_groupbox) + # Track details group box + self.track_groupbox = QtGui.QGroupBox(self.central_widget) + self.track_groupbox.setObjectName('track_groupbox') + self.track_layout = QtGui.QFormLayout() + self.track_layout.setObjectName('track_layout') + self.label_alignment = self.track_layout.labelAlignment() + self.track_groupbox.setLayout(self.track_layout) + # Title track + self.title_label = QtGui.QLabel(self.track_groupbox) + self.title_label.setObjectName('title_label') + self.titles_combo_box = QtGui.QComboBox(self.track_groupbox) + self.titles_combo_box.setSizePolicy(self.combobox_size_policy) + self.titles_combo_box.setEditText('') + self.titles_combo_box.setObjectName('titles_combo_box') + self.track_layout.addRow(self.title_label, self.titles_combo_box) + # Audio track + self.audio_track_label = QtGui.QLabel(self.track_groupbox) + self.audio_track_label.setObjectName('audio_track_label') + self.audio_tracks_combobox = QtGui.QComboBox(self.track_groupbox) + self.audio_tracks_combobox.setSizePolicy(self.combobox_size_policy) + self.audio_tracks_combobox.setObjectName('audio_tracks_combobox') + self.track_layout.addRow(self.audio_track_label, self.audio_tracks_combobox) + self.main_layout.addWidget(self.track_groupbox) + # Subtitle track + self.subtitle_track_label = QtGui.QLabel(self.track_groupbox) + self.subtitle_track_label.setObjectName('subtitle_track_label') + self.subtitle_tracks_combobox = QtGui.QComboBox(self.track_groupbox) + self.subtitle_tracks_combobox.setSizePolicy(self.combobox_size_policy) + self.subtitle_tracks_combobox.setObjectName('subtitle_tracks_combobox') + self.track_layout.addRow(self.subtitle_track_label, self.subtitle_tracks_combobox) + # Preview frame + self.preview_frame = QtGui.QFrame(self.central_widget) + self.preview_frame.setMinimumSize(QtCore.QSize(320, 240)) + self.preview_frame.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)) + self.preview_frame.setStyleSheet('background-color:black;') + self.preview_frame.setFrameShape(QtGui.QFrame.NoFrame) + self.preview_frame.setObjectName('preview_frame') + self.main_layout.addWidget(self.preview_frame) + # player controls + self.controls_layout = QtGui.QHBoxLayout() + self.controls_layout.setObjectName('controls_layout') + self.play_button = QtGui.QToolButton(self.central_widget) + self.play_button.setIcon(build_icon(':/slides/media_playback_start.png')) + self.play_button.setObjectName('play_button') + self.controls_layout.addWidget(self.play_button) + self.position_slider = QtGui.QSlider(self.central_widget) + self.position_slider.setTracking(False) + self.position_slider.setOrientation(QtCore.Qt.Horizontal) + self.position_slider.setObjectName('position_slider') + self.controls_layout.addWidget(self.position_slider) + self.position_timeedit = QtGui.QTimeEdit(self.central_widget) + self.position_timeedit.setReadOnly(True) + self.position_timeedit.setObjectName('position_timeedit') + self.controls_layout.addWidget(self.position_timeedit) + self.main_layout.addLayout(self.controls_layout) + # Range + self.range_groupbox = QtGui.QGroupBox(self.central_widget) + self.range_groupbox.setObjectName('range_groupbox') + self.range_layout = QtGui.QGridLayout() + self.range_layout.setObjectName('range_layout') + self.range_groupbox.setLayout(self.range_layout) + # Start position + self.start_position_label = QtGui.QLabel(self.range_groupbox) + self.start_position_label.setObjectName('start_position_label') + self.range_layout.addWidget(self.start_position_label, 0, 0, self.label_alignment) + self.start_position_edit = QtGui.QTimeEdit(self.range_groupbox) + self.start_position_edit.setObjectName('start_position_edit') + self.range_layout.addWidget(self.start_position_edit, 0, 1) + self.set_start_button = QtGui.QPushButton(self.range_groupbox) + self.set_start_button.setObjectName('set_start_button') + self.range_layout.addWidget(self.set_start_button, 0, 2) + self.jump_start_button = QtGui.QPushButton(self.range_groupbox) + self.jump_start_button.setObjectName('jump_start_button') + self.range_layout.addWidget(self.jump_start_button, 0, 3) + # End position + self.end_position_label = QtGui.QLabel(self.range_groupbox) + self.end_position_label.setObjectName('end_position_label') + self.range_layout.addWidget(self.end_position_label, 1, 0, self.label_alignment) + self.end_timeedit = QtGui.QTimeEdit(self.range_groupbox) + self.end_timeedit.setObjectName('end_timeedit') + self.range_layout.addWidget(self.end_timeedit, 1, 1) + self.set_end_button = QtGui.QPushButton(self.range_groupbox) + self.set_end_button.setObjectName('set_end_button') + self.range_layout.addWidget(self.set_end_button, 1, 2) + self.jump_end_button = QtGui.QPushButton(self.range_groupbox) + self.jump_end_button.setObjectName('jump_end_button') + self.range_layout.addWidget(self.jump_end_button, 1, 3) + self.main_layout.addWidget(self.range_groupbox) + # Save and close buttons + self.button_box = QtGui.QDialogButtonBox(self.central_widget) + self.button_box.addButton(QtGui.QDialogButtonBox.Save) + self.button_box.addButton(QtGui.QDialogButtonBox.Cancel) + self.close_button = self.button_box.button(QtGui.QDialogButtonBox.Cancel) + self.save_button = self.button_box.button(QtGui.QDialogButtonBox.Save) + self.main_layout.addWidget(self.button_box) - def retranslateUi(self, MediaClipSelector): - MediaClipSelector.setWindowTitle(translate("MediaPlugin.MediaClipSelector", "Select media clip", None)) - self.start_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) - self.end_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) - self.set_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", - "Set current position as start point", None)) - self.load_disc_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Load disc", None)) - self.end_point_label.setText(translate("MediaPlugin.MediaClipSelector", "End point", None)) - self.title_label.setText(translate("MediaPlugin.MediaClipSelector", "Title", None)) - self.set_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", - "Set current position as end point", None)) - self.save_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Save current clip", None)) - self.close_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Close", None)) - self.start_point_label.setText(translate("MediaPlugin.MediaClipSelector", "Start point", None)) - self.jump_start_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to start point", None)) - self.audio_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Audio track", None)) - self.media_position_timeedit.setDisplayFormat(translate("MediaPlugin.MediaClipSelector", "HH:mm:ss.z", None)) - self.subtitle_track_label.setText(translate("MediaPlugin.MediaClipSelector", "Subtitle track", None)) - self.jump_end_pushbutton.setText(translate("MediaPlugin.MediaClipSelector", "Jump to end point", None)) - self.media_path_label.setText(translate("MediaPlugin.MediaClipSelector", "Media path", None)) - self.media_path_combobox.lineEdit().setPlaceholderText(translate("MediaPlugin.MediaClipSelector", - "Select drive from list", None)) + self.retranslateUi(media_clip_selector) + self.button_box.accepted.connect(media_clip_selector.accept) + self.button_box.rejected.connect(media_clip_selector.reject) + QtCore.QMetaObject.connectSlotsByName(media_clip_selector) + media_clip_selector.setTabOrder(self.media_path_combobox, self.load_disc_button) + media_clip_selector.setTabOrder(self.load_disc_button, self.titles_combo_box) + media_clip_selector.setTabOrder(self.titles_combo_box, self.audio_tracks_combobox) + media_clip_selector.setTabOrder(self.audio_tracks_combobox, self.subtitle_tracks_combobox) + media_clip_selector.setTabOrder(self.subtitle_tracks_combobox, self.play_button) + media_clip_selector.setTabOrder(self.play_button, self.position_slider) + media_clip_selector.setTabOrder(self.position_slider, self.position_timeedit) + media_clip_selector.setTabOrder(self.position_timeedit, self.start_position_edit) + media_clip_selector.setTabOrder(self.start_position_edit, self.set_start_button) + media_clip_selector.setTabOrder(self.set_start_button, self.jump_start_button) + media_clip_selector.setTabOrder(self.jump_start_button, self.end_timeedit) + media_clip_selector.setTabOrder(self.end_timeedit, self.set_end_button) + media_clip_selector.setTabOrder(self.set_end_button, self.jump_end_button) + media_clip_selector.setTabOrder(self.jump_end_button, self.save_button) + media_clip_selector.setTabOrder(self.save_button, self.close_button) + + def retranslateUi(self, media_clip_selector): + media_clip_selector.setWindowTitle(translate('MediaPlugin.MediaClipSelector', 'Select Media Clip')) + self.source_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Source')) + self.media_path_label.setText(translate('MediaPlugin.MediaClipSelector', 'Media path:')) + self.media_path_combobox.lineEdit().setPlaceholderText(translate('MediaPlugin.MediaClipSelector', + 'Select drive from list')) + self.load_disc_button.setText(translate('MediaPlugin.MediaClipSelector', 'Load disc')) + self.track_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Track Details')) + self.title_label.setText(translate('MediaPlugin.MediaClipSelector', 'Title:')) + self.audio_track_label.setText(translate('MediaPlugin.MediaClipSelector', 'Audio track:')) + self.subtitle_track_label.setText(translate('MediaPlugin.MediaClipSelector', 'Subtitle track:')) + self.position_timeedit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.range_groupbox.setTitle(translate('MediaPlugin.MediaClipSelector', 'Clip Range')) + self.start_position_label.setText(translate('MediaPlugin.MediaClipSelector', 'Start point:')) + self.start_position_edit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.set_start_button.setText(translate('MediaPlugin.MediaClipSelector', 'Set start point')) + self.jump_start_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to start point')) + self.end_position_label.setText(translate('MediaPlugin.MediaClipSelector', 'End point:')) + self.end_timeedit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) + self.set_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Set end point')) + self.jump_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to end point')) \ No newline at end of file diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 5a520d992..eec75d623 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -81,8 +81,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.playback_length = 0 self.media_item = media_item self.setupUi(self) - # most actions auto-connect due to the functions name, so only a few left to do - self.close_pushbutton.clicked.connect(self.reject) # setup play/pause icon self.play_icon = QtGui.QIcon() self.play_icon.addPixmap(QtGui.QPixmap(":/slides/media_playback_start.png"), QtGui.QIcon.Normal, @@ -96,6 +94,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): Exit Dialog and do not save """ log.debug('MediaClipSelectorForm.reject') + print(self.geometry().height()) + print(self.geometry().width()) # Tear down vlc if self.vlc_media_player: self.vlc_media_player.stop() @@ -107,7 +107,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if self.vlc_media: self.vlc_media.release() self.vlc_media = None - QtGui.QDialog.reject(self) + return QtGui.QDialog.reject(self) def exec_(self): """ @@ -122,16 +122,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): Reset the UI to default values """ self.playback_length = 0 - self.position_horizontalslider.setMinimum(0) + self.position_slider.setMinimum(0) self.disable_all() self.toggle_disable_load_media(False) self.subtitle_tracks_combobox.clear() self.audio_tracks_combobox.clear() - self.title_combo_box.clear() + self.titles_combo_box.clear() time = QtCore.QTime() - self.start_timeedit.setTime(time) + self.start_position_edit.setTime(time) self.end_timeedit.setTime(time) - self.media_position_timeedit.setTime(time) + self.position_timeedit.setTime(time) def setup_vlc(self): """ @@ -145,7 +145,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # This is platform specific! # You have to give the id of the QFrame (or similar object) # to vlc, different platforms have different functions for this. - win_id = int(self.media_view_frame.winId()) + win_id = int(self.preview_frame.winId()) if sys.platform == "win32": self.vlc_media_player.set_hwnd(win_id) elif sys.platform == "darwin": @@ -182,16 +182,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.audio_cd_tracks = self.vlc_media.subitems() if not self.audio_cd_tracks or self.audio_cd_tracks.count() < 1: return False - # Insert into title_combo_box - self.title_combo_box.clear() + # Insert into titles_combo_box + self.titles_combo_box.clear() for i in range(self.audio_cd_tracks.count()): item = self.audio_cd_tracks.item_at_index(i) item_title = item.get_meta(vlc.Meta.Title) - self.title_combo_box.addItem(item_title, i) + self.titles_combo_box.addItem(item_title, i) self.vlc_media_player.set_media(self.audio_cd_tracks.item_at_index(0)) self.audio_cd = True - self.title_combo_box.setDisabled(False) - self.title_combo_box.setCurrentIndex(0) + self.titles_combo_box.setDisabled(False) + self.titles_combo_box.setCurrentIndex(0) self.on_title_combo_box_currentIndexChanged(0) return True @@ -255,19 +255,19 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if not self.audio_cd: # Get titles, insert in combobox titles = self.vlc_media_player.video_get_title_description() - self.title_combo_box.clear() + self.titles_combo_box.clear() for title in titles: - self.title_combo_box.addItem(title[1].decode(), title[0]) + self.titles_combo_box.addItem(title[1].decode(), title[0]) # Main title is usually title #1 if len(titles) > 1: - self.title_combo_box.setCurrentIndex(1) + self.titles_combo_box.setCurrentIndex(1) else: - self.title_combo_box.setCurrentIndex(0) + self.titles_combo_box.setCurrentIndex(0) # Enable audio track combobox if anything is in it if len(titles) > 0: - self.title_combo_box.setDisabled(False) + self.titles_combo_box.setDisabled(False) self.toggle_disable_load_media(False) - log.debug('load_disc_pushbutton end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(bool) def on_play_pushbutton_clicked(self, clicked): @@ -278,23 +278,23 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ if self.vlc_media_player.get_state() == vlc.State.Playing: self.vlc_media_player.pause() - self.play_pushbutton.setIcon(self.play_icon) + self.play_button.setIcon(self.play_icon) else: self.vlc_media_player.play() self.media_state_wait(vlc.State.Playing) - self.play_pushbutton.setIcon(self.pause_icon) + self.play_button.setIcon(self.pause_icon) @QtCore.pyqtSlot(bool) def on_set_start_pushbutton_clicked(self, clicked): """ - Copy the current player position to start_timeedit + Copy the current player position to start_position_edit :param clicked: Given from signal, not used. """ vlc_ms_pos = self.vlc_media_player.get_time() time = QtCore.QTime() new_pos_time = time.addMSecs(vlc_ms_pos) - self.start_timeedit.setTime(new_pos_time) + self.start_position_edit.setTime(new_pos_time) # If start time is after end time, update end time. end_time = self.end_timeedit.time() if end_time < new_pos_time: @@ -312,14 +312,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): new_pos_time = time.addMSecs(vlc_ms_pos) self.end_timeedit.setTime(new_pos_time) # If start time is after end time, update start time. - start_time = self.start_timeedit.time() + start_time = self.start_position_edit.time() if start_time > new_pos_time: - self.start_timeedit.setTime(new_pos_time) + self.start_position_edit.setTime(new_pos_time) @QtCore.pyqtSlot(QtCore.QTime) def on_start_timeedit_timeChanged(self, new_time): """ - Called when start_timeedit is changed manually + Called when start_position_edit is changed manually :param new_time: The new time """ @@ -336,9 +336,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param new_time: The new time """ # If start time is after end time, update start time. - start_time = self.start_timeedit.time() + start_time = self.start_position_edit.time() if start_time > new_time: - self.start_timeedit.setTime(new_time) + self.start_position_edit.setTime(new_time) @QtCore.pyqtSlot(bool) def on_jump_end_pushbutton_clicked(self, clicked): @@ -357,11 +357,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): @QtCore.pyqtSlot(bool) def on_jump_start_pushbutton_clicked(self, clicked): """ - Set the player position to the position stored in start_timeedit + Set the player position to the position stored in start_position_edit :param clicked: Given from signal, not used. """ - start_time = self.start_timeedit.time() + start_time = self.start_position_edit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ start_time.minute() * 60 * 1000 + \ start_time.second() * 1000 + \ @@ -428,17 +428,17 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.audio_set_mute(False) self.vlc_media_player.set_pause(1) # If a title or audio track is available the player is enabled - if self.title_combo_box.count() > 0 or len(audio_tracks) > 0: + if self.titles_combo_box.count() > 0 or len(audio_tracks) > 0: self.toggle_disable_player(False) # Set media length info self.playback_length = self.vlc_media_player.get_length() log.debug('playback_length: %d ms' % self.playback_length) - self.position_horizontalslider.setMaximum(self.playback_length) + self.position_slider.setMaximum(self.playback_length) # setup start and end time rounded_vlc_ms_length = int(round(self.playback_length / 100.0) * 100.0) time = QtCore.QTime() playback_length_time = time.addMSecs(rounded_vlc_ms_length) - self.start_timeedit.setMaximumTime(playback_length_time) + self.start_position_edit.setMaximumTime(playback_length_time) self.end_timeedit.setMaximumTime(playback_length_time) self.end_timeedit.setTime(playback_length_time) # Pause once again, just to make sure @@ -447,7 +447,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): sleep(0.1) self.vlc_media_player.set_pause(1) loop_count += 1 - log.debug('title_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + log.debug('titles_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(int) def on_audio_tracks_combobox_currentIndexChanged(self, index): @@ -493,15 +493,15 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): rounded_vlc_ms_pos = int(round(vlc_ms_pos / 100.0) * 100.0) time = QtCore.QTime() new_pos_time = time.addMSecs(rounded_vlc_ms_pos) - self.media_position_timeedit.setTime(new_pos_time) - self.position_horizontalslider.setSliderPosition(vlc_ms_pos) + self.position_timeedit.setTime(new_pos_time) + self.position_slider.setSliderPosition(vlc_ms_pos) def disable_all(self): """ Disable all elements in the dialog """ self.toggle_disable_load_media(True) - self.title_combo_box.setDisabled(True) + self.titles_combo_box.setDisabled(True) self.audio_tracks_combobox.setDisabled(True) self.subtitle_tracks_combobox.setDisabled(True) self.toggle_disable_player(True) @@ -513,7 +513,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param action: If True elements are disabled, if False they are enabled. """ self.media_path_combobox.setDisabled(action) - self.load_disc_pushbutton.setDisabled(action) + self.load_disc_button.setDisabled(action) def toggle_disable_player(self, action): """ @@ -521,16 +521,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param action: If True elements are disabled, if False they are enabled. """ - self.play_pushbutton.setDisabled(action) - self.position_horizontalslider.setDisabled(action) - self.media_position_timeedit.setDisabled(action) - self.start_timeedit.setDisabled(action) - self.set_start_pushbutton.setDisabled(action) - self.jump_start_pushbutton.setDisabled(action) + self.play_button.setDisabled(action) + self.position_slider.setDisabled(action) + self.position_timeedit.setDisabled(action) + self.start_position_edit.setDisabled(action) + self.set_start_button.setDisabled(action) + self.jump_start_button.setDisabled(action) self.end_timeedit.setDisabled(action) - self.set_end_pushbutton.setDisabled(action) - self.jump_end_pushbutton.setDisabled(action) - self.save_pushbutton.setDisabled(action) + self.set_end_button.setDisabled(action) + self.jump_end_button.setDisabled(action) + self.save_button.setDisabled(action) @QtCore.pyqtSlot(bool) def on_save_pushbutton_clicked(self, clicked): @@ -540,7 +540,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): :param clicked: Given from signal, not used. """ log.debug('in on_save_pushbutton_clicked') - start_time = self.start_timeedit.time() + start_time = self.start_position_edit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ start_time.minute() * 60 * 1000 + \ start_time.second() * 1000 + \ @@ -550,7 +550,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): end_time.minute() * 60 * 1000 + \ end_time.second() * 1000 + \ end_time.msec() - title = self.title_combo_box.itemData(self.title_combo_box.currentIndex()) + title = self.titles_combo_box.itemData(self.titles_combo_box.currentIndex()) path = self.media_path_combobox.currentText() optical = '' if self.audio_cd: diff --git a/openlp/plugins/media/lib/mediaitem.py b/openlp/plugins/media/lib/mediaitem.py index 5bdbe4d33..f0224aa88 100644 --- a/openlp/plugins/media/lib/mediaitem.py +++ b/openlp/plugins/media/lib/mediaitem.py @@ -287,8 +287,8 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): check_directory_exists(self.service_path) self.load_list(Settings().value(self.settings_section + '/media files')) self.rebuild_players() - if VLC_AVAILABLE: - self.media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) + # if VLC_AVAILABLE: + # self.media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) def rebuild_players(self): """ @@ -415,7 +415,13 @@ class MediaMediaItem(MediaManagerItem, RegistryProperties): """ When the load optical button is clicked, open the clip selector window. """ - self.media_clip_selector_form.exec_() + # self.media_clip_selector_form.exec_() + if VLC_AVAILABLE: + media_clip_selector_form = MediaClipSelectorForm(self, self.main_window, None) + media_clip_selector_form.exec_() + del media_clip_selector_form + else: + QtGui.QMessageBox.critical(self, 'VLC is not available', 'VLC is not available') def add_optical_clip(self, optical): """ diff --git a/openlp/plugins/songs/forms/songexportform.py b/openlp/plugins/songs/forms/songexportform.py index 589da4d33..412ec0369 100644 --- a/openlp/plugins/songs/forms/songexportform.py +++ b/openlp/plugins/songs/forms/songexportform.py @@ -124,7 +124,7 @@ class SongExportForm(OpenLPWizard): self.export_song_layout = QtGui.QHBoxLayout(self.export_song_page) self.export_song_layout.setObjectName('export_song_layout') self.grid_layout = QtGui.QGridLayout() - self.grid_layout.setObjectName('grid_layout') + self.grid_layout.setObjectName('range_layout') self.selected_list_widget = QtGui.QListWidget(self.export_song_page) self.selected_list_widget.setObjectName('selected_list_widget') self.grid_layout.addWidget(self.selected_list_widget, 1, 0, 1, 1) diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index 241ef26c3..e97a06238 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -100,7 +100,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.exec_() # WHEN: The load button is clicked with no path set - QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) # THEN: we should get an error mocked_critical_error_message_box.assert_called_with(message='No path was given') @@ -109,7 +109,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): mocked_os_path_exists.return_value = False self.form.media_path_combobox.insertItem(0, '/non-existing/test-path.test') self.form.media_path_combobox.setCurrentIndex(0) - QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) # THEN: we should get an error assert self.form.media_path_combobox.currentText() == '/non-existing/test-path.test',\ @@ -122,7 +122,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.vlc_media_player.play.return_value = -1 self.form.media_path_combobox.insertItem(0, '/existing/test-path.test') self.form.media_path_combobox.setCurrentIndex(0) - QtTest.QTest.mouseClick(self.form.load_disc_pushbutton, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.form.load_disc_button, QtCore.Qt.LeftButton) # THEN: we should get an error assert self.form.media_path_combobox.currentText() == '/existing/test-path.test',\ @@ -141,15 +141,15 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.subtitle_tracks_combobox.itemData = MagicMock() self.form.audio_tracks_combobox.itemData.return_value = None self.form.subtitle_tracks_combobox.itemData.return_value = None - self.form.title_combo_box.insertItem(0, 'Test Title 0') - self.form.title_combo_box.insertItem(1, 'Test Title 1') + self.form.titles_combo_box.insertItem(0, 'Test Title 0') + self.form.titles_combo_box.insertItem(1, 'Test Title 1') # WHEN: There exists audio and subtitle tracks and the index is updated. self.form.vlc_media_player.audio_get_track_description.return_value = [(-1, b'Disabled'), (0, b'Audio Track 1')] self.form.vlc_media_player.video_get_spu_description.return_value = [(-1, b'Disabled'), (0, b'Subtitle Track 1')] - self.form.title_combo_box.setCurrentIndex(1) + self.form.titles_combo_box.setCurrentIndex(1) # THEN: The subtitle and audio track comboboxes should be updated and get signals and call itemData. self.form.audio_tracks_combobox.itemData.assert_any_call(0) From 554ce81dd38122d97075ab54563ec748b0320a1c Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 21 Aug 2014 01:45:55 +0200 Subject: [PATCH 38/66] Added some padding and make the contents of the dialog expand out. --- .../media/forms/mediaclipselectordialog.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index fe62c99d3..88b4616eb 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -40,17 +40,14 @@ class Ui_MediaClipSelector(object): self.combobox_size_policy = QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.Fixed) media_clip_selector.setSizePolicy( QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)) - self.central_widget = QtGui.QWidget(media_clip_selector) - self.central_widget.setSizePolicy( - QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) - self.central_widget.setObjectName('central_widget') - self.main_layout = QtGui.QVBoxLayout(self.central_widget) + self.main_layout = QtGui.QVBoxLayout(media_clip_selector) + self.main_layout.setContentsMargins(8, 8, 8, 8) self.main_layout.setObjectName('main_layout') - self.central_widget.setLayout(self.main_layout) # Source groupbox - self.source_groupbox = QtGui.QGroupBox(self.central_widget) + self.source_groupbox = QtGui.QGroupBox(media_clip_selector) self.source_groupbox.setObjectName('source_groupbox') self.source_layout = QtGui.QHBoxLayout() + self.source_layout.setContentsMargins(8, 8, 8, 8) self.source_layout.setObjectName('source_layout') self.source_groupbox.setLayout(self.source_layout) # Media path label @@ -65,15 +62,16 @@ class Ui_MediaClipSelector(object): self.media_path_combobox.setObjectName('media_path_combobox') self.source_layout.addWidget(self.media_path_combobox) # Load disc button - self.load_disc_button = QtGui.QPushButton(self.central_widget) + self.load_disc_button = QtGui.QPushButton(media_clip_selector) self.load_disc_button.setEnabled(True) self.load_disc_button.setObjectName('load_disc_button') self.source_layout.addWidget(self.load_disc_button) self.main_layout.addWidget(self.source_groupbox) # Track details group box - self.track_groupbox = QtGui.QGroupBox(self.central_widget) + self.track_groupbox = QtGui.QGroupBox(media_clip_selector) self.track_groupbox.setObjectName('track_groupbox') self.track_layout = QtGui.QFormLayout() + self.track_layout.setContentsMargins(8, 8, 8, 8) self.track_layout.setObjectName('track_layout') self.label_alignment = self.track_layout.labelAlignment() self.track_groupbox.setLayout(self.track_layout) @@ -101,7 +99,7 @@ class Ui_MediaClipSelector(object): self.subtitle_tracks_combobox.setObjectName('subtitle_tracks_combobox') self.track_layout.addRow(self.subtitle_track_label, self.subtitle_tracks_combobox) # Preview frame - self.preview_frame = QtGui.QFrame(self.central_widget) + self.preview_frame = QtGui.QFrame(media_clip_selector) self.preview_frame.setMinimumSize(QtCore.QSize(320, 240)) self.preview_frame.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)) self.preview_frame.setStyleSheet('background-color:black;') @@ -111,24 +109,25 @@ class Ui_MediaClipSelector(object): # player controls self.controls_layout = QtGui.QHBoxLayout() self.controls_layout.setObjectName('controls_layout') - self.play_button = QtGui.QToolButton(self.central_widget) + self.play_button = QtGui.QToolButton(media_clip_selector) self.play_button.setIcon(build_icon(':/slides/media_playback_start.png')) self.play_button.setObjectName('play_button') self.controls_layout.addWidget(self.play_button) - self.position_slider = QtGui.QSlider(self.central_widget) + self.position_slider = QtGui.QSlider(media_clip_selector) self.position_slider.setTracking(False) self.position_slider.setOrientation(QtCore.Qt.Horizontal) self.position_slider.setObjectName('position_slider') self.controls_layout.addWidget(self.position_slider) - self.position_timeedit = QtGui.QTimeEdit(self.central_widget) + self.position_timeedit = QtGui.QTimeEdit(media_clip_selector) self.position_timeedit.setReadOnly(True) self.position_timeedit.setObjectName('position_timeedit') self.controls_layout.addWidget(self.position_timeedit) self.main_layout.addLayout(self.controls_layout) # Range - self.range_groupbox = QtGui.QGroupBox(self.central_widget) + self.range_groupbox = QtGui.QGroupBox(media_clip_selector) self.range_groupbox.setObjectName('range_groupbox') self.range_layout = QtGui.QGridLayout() + self.range_layout.setContentsMargins(8, 8, 8, 8) self.range_layout.setObjectName('range_layout') self.range_groupbox.setLayout(self.range_layout) # Start position @@ -159,7 +158,7 @@ class Ui_MediaClipSelector(object): self.range_layout.addWidget(self.jump_end_button, 1, 3) self.main_layout.addWidget(self.range_groupbox) # Save and close buttons - self.button_box = QtGui.QDialogButtonBox(self.central_widget) + self.button_box = QtGui.QDialogButtonBox(media_clip_selector) self.button_box.addButton(QtGui.QDialogButtonBox.Save) self.button_box.addButton(QtGui.QDialogButtonBox.Cancel) self.close_button = self.button_box.button(QtGui.QDialogButtonBox.Cancel) @@ -206,4 +205,4 @@ class Ui_MediaClipSelector(object): self.end_position_label.setText(translate('MediaPlugin.MediaClipSelector', 'End point:')) self.end_timeedit.setDisplayFormat(translate('MediaPlugin.MediaClipSelector', 'HH:mm:ss.z')) self.set_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Set end point')) - self.jump_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to end point')) \ No newline at end of file + self.jump_end_button.setText(translate('MediaPlugin.MediaClipSelector', 'Jump to end point')) From 7b25295c7fc014958125cc0ea535154c03a216c9 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 21 Aug 2014 01:49:22 +0200 Subject: [PATCH 39/66] Remove some debugging prints... oops --- openlp/plugins/media/forms/mediaclipselectorform.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index eec75d623..449b95d46 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -94,8 +94,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): Exit Dialog and do not save """ log.debug('MediaClipSelectorForm.reject') - print(self.geometry().height()) - print(self.geometry().width()) # Tear down vlc if self.vlc_media_player: self.vlc_media_player.stop() From 80656491581e541c818061de406c2877e3ae51b5 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Sat, 23 Aug 2014 00:12:35 +0200 Subject: [PATCH 40/66] Make the wizards look right on OS X --- openlp/core/ui/firsttimeform.py | 12 ++++++++---- openlp/core/ui/firsttimewizard.py | 10 +++++++--- openlp/core/ui/themewizard.py | 6 +++++- openlp/core/ui/wizard.py | 5 ++++- resources/images/openlp-2.qrc | 7 ++++--- resources/images/openlp-osx-wizard.png | Bin 0 -> 38669 bytes scripts/generate_resources.sh | 2 +- 7 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 resources/images/openlp-osx-wizard.png diff --git a/openlp/core/ui/firsttimeform.py b/openlp/core/ui/firsttimeform.py index d7c16f0d3..8599c8d35 100644 --- a/openlp/core/ui/firsttimeform.py +++ b/openlp/core/ui/firsttimeform.py @@ -387,17 +387,21 @@ class FirstTimeForm(QtGui.QWizard, Ui_FirstTimeWizard, RegistryProperties): self.progress_bar.setValue(self.progress_bar.maximum()) if self.has_run_wizard: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the finish button to return to OpenLP.')) + 'Download complete. Click the %s button to return to OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Download complete. Click the finish button to start OpenLP.')) + 'Download complete. Click the %s button to start OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: if self.has_run_wizard: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Click the finish button to return to OpenLP.')) + 'Click the %s button to return to OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) else: self.progress_label.setText(translate('OpenLP.FirstTimeWizard', - 'Click the finish button to start OpenLP.')) + 'Click the %s button to start OpenLP.') % + self.buttonText(QtGui.QWizard.FinishButton)) self.finish_button.setVisible(True) self.finish_button.setEnabled(True) self.cancel_button.setVisible(False) diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index ff1675ff5..3e7f057ea 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -64,9 +64,12 @@ class Ui_FirstTimeWizard(object): first_time_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) first_time_wizard.resize(550, 386) first_time_wizard.setModal(True) - first_time_wizard.setWizardStyle(QtGui.QWizard.ModernStyle) first_time_wizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage | QtGui.QWizard.HaveCustomButton1) + if sys.platform == 'darwin': + first_time_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, + QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + first_time_wizard.resize(634, 386) self.finish_button = self.button(QtGui.QWizard.FinishButton) self.no_internet_finish_button = self.button(QtGui.QWizard.CustomButton1) self.cancel_button = self.button(QtGui.QWizard.CancelButton) @@ -212,7 +215,8 @@ class Ui_FirstTimeWizard(object): translate('OpenLP.FirstTimeWizard', 'Welcome to the First Time Wizard')) self.information_label.setText( translate('OpenLP.FirstTimeWizard', 'This wizard will help you to configure OpenLP for initial use. ' - 'Click the next button below to start.')) + 'Click the %s button below to start.') % + self.buttonText(QtGui.QWizard.NextButton)) self.plugin_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Activate required Plugins')) self.plugin_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select the Plugins you wish to use. ')) self.songs_check_box.setText(translate('OpenLP.FirstTimeWizard', 'Songs')) @@ -236,7 +240,7 @@ class Ui_FirstTimeWizard(object): 'wizard by selecting "Tools/Re-run First Time Wizard" from OpenLP.') self.cancelWizardText = translate('OpenLP.FirstTimeWizard', '\n\nTo cancel the First Time Wizard completely (and not start OpenLP), ' - 'click the Cancel button now.') + 'click the %s button now.') % self.buttonText(QtGui.QWizard.CancelButton) self.songs_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Songs')) self.songs_page.setSubTitle(translate('OpenLP.FirstTimeWizard', 'Select and download public domain songs.')) self.bibles_page.setTitle(translate('OpenLP.FirstTimeWizard', 'Sample Bibles')) diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index bda52c807..60878536a 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -29,6 +29,8 @@ """ The Create/Edit theme wizard """ +import sys + from PyQt4 import QtCore, QtGui from openlp.core.common import UiStrings, translate @@ -48,9 +50,11 @@ class Ui_ThemeWizard(object): themeWizard.setObjectName('OpenLP.ThemeWizard') themeWizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) themeWizard.setModal(True) - themeWizard.setWizardStyle(QtGui.QWizard.ModernStyle) themeWizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) + if sys.platform == 'darwin': + themeWizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + #themeWizard.resize(634, 386) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) # Welcome Page add_welcome_page(themeWizard, ':/wizards/wizard_createtheme.bmp') diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 23bc0a9e1..4ba258780 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -31,6 +31,7 @@ The :mod:``wizard`` module provides generic wizard tools for OpenLP. """ import logging import os +import sys from PyQt4 import QtGui @@ -121,9 +122,11 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties): """ self.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) self.setModal(True) - self.setWizardStyle(QtGui.QWizard.ModernStyle) self.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage) + if sys.platform == 'darwin': + self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + #self.resize(634, 386) add_welcome_page(self, image) self.add_custom_pages() if self.with_progress_page: diff --git a/resources/images/openlp-2.qrc b/resources/images/openlp-2.qrc index 79036f08f..1196fedd0 100644 --- a/resources/images/openlp-2.qrc +++ b/resources/images/openlp-2.qrc @@ -99,6 +99,7 @@ export_load.png + openlp-osx-wizard.png wizard_exportsong.bmp wizard_importsong.bmp wizard_importbible.bmp @@ -150,10 +151,10 @@ messagebox_warning.png - network_server.png + network_server.png network_ssl.png - network_auth.png - + network_auth.png + song_usage_active.png song_usage_inactive.png diff --git a/resources/images/openlp-osx-wizard.png b/resources/images/openlp-osx-wizard.png new file mode 100644 index 0000000000000000000000000000000000000000..e748ba4508371809f596c1cda5d25abac25f9491 GIT binary patch literal 38669 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 openlp/core/resources.py From 3101d72b483db08042f44f74e8fc2e82747062c4 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 24 Aug 2014 15:40:45 +0100 Subject: [PATCH 41/66] Rewrote OSIS import, added tests. --- openlp/plugins/bibles/lib/opensong.py | 3 +- openlp/plugins/bibles/lib/osis.py | 255 +++++++++--------- .../openlp_plugins/bibles/test_osisimport.py | 162 +++++++++++ tests/resources/bibles/dk1933.json | 16 ++ tests/resources/bibles/kjv.json | 16 ++ tests/resources/bibles/osis-dk1933.xml | 32 +++ tests/resources/bibles/osis-kjv.xml | 41 +++ tests/resources/bibles/osis-web.xml | 109 ++++++++ tests/resources/bibles/web.json | 16 ++ 9 files changed, 522 insertions(+), 128 deletions(-) create mode 100644 tests/functional/openlp_plugins/bibles/test_osisimport.py create mode 100644 tests/resources/bibles/dk1933.json create mode 100644 tests/resources/bibles/kjv.json create mode 100644 tests/resources/bibles/osis-dk1933.xml create mode 100644 tests/resources/bibles/osis-kjv.xml create mode 100644 tests/resources/bibles/osis-web.xml create mode 100644 tests/resources/bibles/web.json diff --git a/openlp/plugins/bibles/lib/opensong.py b/openlp/plugins/bibles/lib/opensong.py index fa8323d7f..dccdbf2cf 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -30,7 +30,7 @@ import logging from lxml import etree, objectify -from openlp.core.common import translate +from openlp.core.common import translate, trace_error_handler from openlp.core.lib.ui import critical_error_message_box from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB @@ -134,6 +134,7 @@ class OpenSongBible(BibleDB): self.session.commit() self.application.process_events() except etree.XMLSyntaxError as inst: + trace_error_handler(log) critical_error_message_box( message=translate('BiblesPlugin.OpenSongImport', 'Incorrect Bible file type supplied. OpenSong Bibles may be ' diff --git a/openlp/plugins/bibles/lib/osis.py b/openlp/plugins/bibles/lib/osis.py index 4f85bef1a..851db39c4 100644 --- a/openlp/plugins/bibles/lib/osis.py +++ b/openlp/plugins/bibles/lib/osis.py @@ -27,14 +27,12 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### -import os import logging -import chardet -import codecs -import re +from lxml import etree -from openlp.core.common import AppLocation, translate +from openlp.core.common import translate, trace_error_handler from openlp.plugins.bibles.lib.db import BibleDB, BiblesResourcesDB +from openlp.core.lib.ui import critical_error_message_box log = logging.getLogger(__name__) @@ -53,143 +51,146 @@ class OSISBible(BibleDB): log.debug(self.__class__.__name__) BibleDB.__init__(self, parent, **kwargs) self.filename = kwargs['filename'] - self.language_regex = re.compile(r'(.*?)') - self.verse_regex = re.compile(r'(.*?)') - self.note_regex = re.compile(r'(.*?)') - self.title_regex = re.compile(r'(.*?)') - self.milestone_regex = re.compile(r'') - self.fi_regex = re.compile(r'(.*?)') - self.rf_regex = re.compile(r'(.*?)') - self.lb_regex = re.compile(r'') - self.lg_regex = re.compile(r'') - self.l_regex = re.compile(r'') - self.w_regex = re.compile(r'') - self.q_regex = re.compile(r'') - self.q1_regex = re.compile(r'') - self.q2_regex = re.compile(r'') - self.trans_regex = re.compile(r'(.*?)') - self.divine_name_regex = re.compile(r'(.*?)') - self.spaces_regex = re.compile(r'([ ]{2,})') - filepath = os.path.join( - AppLocation.get_directory(AppLocation.PluginsDir), 'bibles', 'resources', 'osisbooks.csv') def do_import(self, bible_name=None): """ Loads a Bible from file. """ log.debug('Starting OSIS import from "%s"' % self.filename) - detect_file = None - db_book = None - osis = None + if not isinstance(self.filename, str): + self.filename = str(self.filename, 'utf8') + import_file = None success = True - last_chapter = 0 - match_count = 0 - self.wizard.increment_progress_bar( - translate('BiblesPlugin.OsisImport', 'Detecting encoding (this may take a few minutes)...')) try: - detect_file = open(self.filename, 'r') - details = chardet.detect(detect_file.read(1048576)) - detect_file.seek(0) - lines_in_file = int(len(detect_file.readlines())) - except IOError: - log.exception('Failed to detect OSIS file encoding') - return - finally: - if detect_file: - detect_file.close() - try: - osis = codecs.open(self.filename, 'r', details['encoding']) - repl = replacement - language_id = False - # Decide if the bible probably contains only NT or AT and NT or - # AT, NT and Apocrypha - if lines_in_file < 11500: - book_count = 27 - chapter_count = 260 - elif lines_in_file < 34200: - book_count = 66 - chapter_count = 1188 - else: - book_count = 67 - chapter_count = 1336 - for file_record in osis: + # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding + # detection, and the two mechanisms together interfere with each other. + import_file = open(self.filename, 'rb') + language_id = self.get_language(bible_name) + if not language_id: + log.error('Importing books from "%s" failed' % self.filename) + return False + osis_bible_tree = etree.parse(import_file) + namespace = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'} + num_books = int(osis_bible_tree.xpath("count(//ns:div[@type='book'])", namespaces=namespace)) + log.debug('number of books: %d' % num_books) + self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', + 'Removing unused tags (this may take a few minutes)...')) + # We strip unused tags from the XML, this should leave us with only chapter, verse and div tags. + # Strip tags we don't use - keep content + etree.strip_tags(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}p', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}l', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}lg', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}q', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}a', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}w', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}divineName', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}foreign', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}hi', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}inscription', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}mentioned', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}name', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}reference', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}seg', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}transChange', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}salute', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}signed', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}closer', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}speech', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}speaker', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}list', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}item', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}table', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}head', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}row', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}cell', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}caption')) + # Strip tags we don't use - remove content + etree.strip_elements(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}note', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}milestone', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}title', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}abbr', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}catchWord', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}index', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdg', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdgGroup', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}figure'), + with_tail=False) + # Precompile a few xpath-querys + verse_in_chapter = etree.XPath('count(//ns:chapter[1]/ns:verse)', namespaces=namespace) + text_in_verse = etree.XPath('count(//ns:verse[1]/text())', namespaces=namespace) + # Find books in the bible + bible_books = osis_bible_tree.xpath("//ns:div[@type='book']", namespaces=namespace) + for book in bible_books: if self.stop_import_flag: break - # Try to find the bible language - if not language_id: - language_match = self.language_regex.search(file_record) - if language_match: - language = BiblesResourcesDB.get_language( - language_match.group(1)) - if language: - language_id = language['id'] - self.save_meta('language_id', language_id) - continue - match = self.verse_regex.search(file_record) - if match: - # Set meta language_id if not detected till now - if not language_id: - language_id = self.get_language(bible_name) - if not language_id: - log.error('Importing books from "%s" failed' % self.filename) - return False - match_count += 1 - book = str(match.group(1)) - chapter = int(match.group(2)) - verse = int(match.group(3)) - verse_text = match.group(4) - book_ref_id = self.get_book_ref_id_by_name(book, book_count, language_id) - if not book_ref_id: - log.error('Importing books from "%s" failed' % self.filename) - return False - book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) - if not db_book or db_book.name != book_details['name']: - log.debug('New book: "%s"' % book_details['name']) - db_book = self.create_book( - book_details['name'], - book_ref_id, - book_details['testament_id']) - if last_chapter == 0: - self.wizard.progress_bar.setMaximum(chapter_count) - if last_chapter != chapter: - if last_chapter != 0: - self.session.commit() + # Remove div-tags in the book + etree.strip_tags(book, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}div')) + book_ref_id = self.get_book_ref_id_by_name(book.get('osisID'), num_books) + if not book_ref_id: + book_ref_id = self.get_book_ref_id_by_localised_name(book.get('osisID')) + if not book_ref_id: + log.error('Importing books from "%s" failed' % self.filename) + return False + book_details = BiblesResourcesDB.get_book_by_id(book_ref_id) + db_book = self.create_book(book_details['name'], book_ref_id, book_details['testament_id']) + # Find out if chapter-tags contains the verses, or if it is used as milestone/anchor + if int(verse_in_chapter(book)) > 0: + # The chapter tags contains the verses + for chapter in book: + chapter_number = chapter.get("osisID").split('.')[1] + # Find out if verse-tags contains the text, or if it is used as milestone/anchor + if int(text_in_verse(chapter)) == 0: + # verse-tags are used as milestone + for verse in chapter: + # If this tag marks the start of a verse, the verse text is between this tag and + # the next tag, which the "tail" attribute gives us. + if verse.get('sID'): + verse_number = verse.get("osisID").split('.')[2] + verse_text = verse.tail + if verse_text: + self.create_verse(db_book.id, chapter_number, verse_number, verse_text.strip()) + else: + # Verse-tags contains the text + for verse in chapter: + verse_number = verse.get("osisID").split('.')[2] + self.create_verse(db_book.id, chapter_number, verse_number, verse.text.strip()) self.wizard.increment_progress_bar( - translate('BiblesPlugin.OsisImport', 'Importing %s %s...', - 'Importing ...') % (book_details['name'], chapter)) - last_chapter = chapter - # All of this rigmarole below is because the mod2osis tool from the Sword library embeds XML in the - # OSIS but neglects to enclose the verse text (with XML) in <[CDATA[ ]]> tags. - verse_text = self.note_regex.sub('', verse_text) - verse_text = self.title_regex.sub('', verse_text) - verse_text = self.milestone_regex.sub('', verse_text) - verse_text = self.fi_regex.sub('', verse_text) - verse_text = self.rf_regex.sub('', verse_text) - verse_text = self.lb_regex.sub(' ', verse_text) - verse_text = self.lg_regex.sub('', verse_text) - verse_text = self.l_regex.sub(' ', verse_text) - verse_text = self.w_regex.sub('', verse_text) - verse_text = self.q1_regex.sub('"', verse_text) - verse_text = self.q2_regex.sub('\'', verse_text) - verse_text = self.q_regex.sub('', verse_text) - verse_text = self.divine_name_regex.sub(repl, verse_text) - verse_text = self.trans_regex.sub('', verse_text) - verse_text = verse_text.replace('', '') \ - .replace('', '').replace('', '') \ - .replace('', '').replace('', '') \ - .replace('', '').replace('', '') - verse_text = self.spaces_regex.sub(' ', verse_text) - self.create_verse(db_book.id, chapter, verse, verse_text) - self.application.process_events() - self.session.commit() - if match_count == 0: - success = False + translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...' % + {'bookname': db_book.name, 'chapter': chapter_number})) + else: + log.debug('chapters are milestones') + # The chapter tags is used as milestones. For now we assume verses is also milestones + chapter_number = 0 + for element in book: + if element.tag == '{http://www.bibletechnologies.net/2003/OSIS/namespace}chapter' \ + and element.get('sID'): + chapter_number = element.get("osisID").split('.')[1] + self.wizard.increment_progress_bar( + translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...' % + {'bookname': db_book.name, 'chapter': chapter_number})) + elif element.tag == '{http://www.bibletechnologies.net/2003/OSIS/namespace}verse' \ + and element.get('sID'): + # If this tag marks the start of a verse, the verse text is between this tag and + # the next tag, which the "tail" attribute gives us. + verse_number = element.get("osisID").split('.')[2] + verse_text = element.tail + if verse_text: + self.create_verse(db_book.id, chapter_number, verse_number, verse_text.strip()) + self.session.commit() + self.application.process_events() except (ValueError, IOError): log.exception('Loading bible from OSIS file failed') + trace_error_handler(log) success = False + except etree.XMLSyntaxError as e: + log.exception('Loading bible from OSIS file failed') + trace_error_handler(log) + success = False + critical_error_message_box(message=translate('BiblesPlugin.OsisImport', + 'The file is not a valid OSIS-XML file: \n%s' % e.msg)) finally: - if osis: - osis.close() + if import_file: + import_file.close() if self.stop_import_flag: return False else: diff --git a/tests/functional/openlp_plugins/bibles/test_osisimport.py b/tests/functional/openlp_plugins/bibles/test_osisimport.py new file mode 100644 index 000000000..af437c267 --- /dev/null +++ b/tests/functional/openlp_plugins/bibles/test_osisimport.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 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 # +############################################################################### +""" +This module contains tests for the OSIS Bible importer. +""" + +import os +import json +from unittest import TestCase + +from tests.functional import MagicMock, patch +from openlp.plugins.bibles.lib.osis import OSISBible +from openlp.plugins.bibles.lib.db import BibleDB + +TEST_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), + '..', '..', '..', 'resources', 'bibles')) + + +class TestOsisImport(TestCase): + """ + Test the functions in the :mod:`osisimport` module. + """ + + def setUp(self): + self.registry_patcher = patch('openlp.plugins.bibles.lib.db.Registry') + self.registry_patcher.start() + self.manager_patcher = patch('openlp.plugins.bibles.lib.db.Manager') + self.manager_patcher.start() + + def tearDown(self): + self.registry_patcher.stop() + self.manager_patcher.stop() + + def create_importer_test(self): + """ + Test creating an instance of the OSIS file importer + """ + # GIVEN: A mocked out "manager" + mocked_manager = MagicMock() + + # WHEN: An importer object is created + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + + # THEN: The importer should be an instance of BibleDB + self.assertIsInstance(importer, BibleDB) + + def file_import_nested_tags_test(self): + """ + Test the actual import of OSIS Bible file, with nested chapter and verse tags + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'dk1933.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'osis-dk1933.xml' + with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'Danish' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) + + def file_import_mixed_tags_test(self): + """ + Test the actual import of OSIS Bible file, with nested chapter and milestone verse tags. + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'kjv.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'osis-kjv.xml' + with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'English' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) + + def file_import_milestone_tags_test(self): + """ + Test the actual import of OSIS Bible file, with milestone chapter and verse tags. + """ + # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions + # get_book_ref_id_by_name, create_verse, create_book, session and get_language. + result_file = open(os.path.join(TEST_PATH, 'web.json'), 'rb') + test_data = json.loads(result_file.read().decode()) + bible_file = 'osis-web.xml' + with patch('openlp.plugins.bibles.lib.osis.OSISBible.application'): + mocked_manager = MagicMock() + mocked_import_wizard = MagicMock() + importer = OSISBible(mocked_manager, path='.', name='.', filename='') + importer.wizard = mocked_import_wizard + importer.get_book_ref_id_by_name = MagicMock() + importer.create_verse = MagicMock() + importer.create_book = MagicMock() + importer.session = MagicMock() + importer.get_language = MagicMock() + importer.get_language.return_value = 'English' + + # WHEN: Importing bible file + importer.filename = os.path.join(TEST_PATH, bible_file) + importer.do_import() + + # THEN: The create_verse() method should have been called with each verse in the file. + self.assertTrue(importer.create_verse.called) + print(importer.create_verse.call_list()) + for verse_tag, verse_text in test_data['verses']: + importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) diff --git a/tests/resources/bibles/dk1933.json b/tests/resources/bibles/dk1933.json new file mode 100644 index 000000000..f364cb47e --- /dev/null +++ b/tests/resources/bibles/dk1933.json @@ -0,0 +1,16 @@ +{ + "book": "Genesis", + "chapter": 1, + "verses": [ + [ "1", "I Begyndelsen skabte Gud Himmelen og Jorden."], + [ "2", "Og Jorden var øde og tom, og der var Mørke over Verdensdybet. Men Guds Ånd svævede over Vandene." ], + [ "3", "Og Gud sagde: \"Der blive Lys!\" Og der blev Lys." ], + [ "4", "Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket," ], + [ "5", "og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, og det blev Morgen, første Dag." ], + [ "6", "Derpå sagde Gud: \"Der blive en Hvælving midt i Vandene til at skille Vandene ad!\"" ], + [ "7", "Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen fra Vandet over Hvælvingen;" ], + [ "8", "og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag." ], + [ "9", "Derpå sagde Gud: \"Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!\" Og således skete det;" ], + [ "10", "og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, at det var godt." ] + ] +} \ No newline at end of file diff --git a/tests/resources/bibles/kjv.json b/tests/resources/bibles/kjv.json new file mode 100644 index 000000000..a375a1b40 --- /dev/null +++ b/tests/resources/bibles/kjv.json @@ -0,0 +1,16 @@ +{ + "book": "Genesis", + "chapter": 1, + "verses": [ + [ "1", "In the beginning God created the heaven and the earth."], + [ "2", "And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters." ], + [ "3", "And God said, Let there be light: and there was light." ], + [ "4", "And God saw the light, that it was good: and God divided the light from the darkness." ], + [ "5", "And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day." ], + [ "6", "And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters." ], + [ "7", "And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so." ], + [ "8", "And God called the firmament Heaven. And the evening and the morning were the second day." ], + [ "9", "And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so." ], + [ "10", "And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good." ] + ] +} diff --git a/tests/resources/bibles/osis-dk1933.xml b/tests/resources/bibles/osis-dk1933.xml new file mode 100644 index 000000000..d51d073fe --- /dev/null +++ b/tests/resources/bibles/osis-dk1933.xml @@ -0,0 +1,32 @@ + + + + +
+ + Dette er Biblen + Bible.DanDetteBiblen + Bible.KJV + + + Bible.KJV + +
+
+
+ + I Begyndelsen skabte Gud Himmelen og Jorden. + Og Jorden var øde og tom, og der var Mørke over Verdensdybet. Men Guds Ånd svævede over Vandene.

+ Og Gud sagde: "Der blive Lys!" Og der blev Lys. + Og Gud så, at Lyset var godt, og Gud satte Skel mellem Lyset og Mørket, + og Gud kaldte Lyset Dag, og Mørket kaldte han Nat. Og det blev Aften, og det blev Morgen, første Dag.

+ Derpå sagde Gud: "Der blive en Hvælving midt i Vandene til at skille Vandene ad!" + Og således skete det: Gud gjorde Hvælvingen og skilte Vandet under Hvælvingen fra Vandet over Hvælvingen; + og Gud kaldte Hvælvingen Himmel. Og det blev Aften, og det blev Morgen, anden Dag.

+ Derpå sagde Gud: "Vandet under Himmelen samle sig på eet Sted, så det faste Land kommer til Syne!" Og således skete det; + og Gud kaldte det faste Land Jord, og Stedet, hvor Vandet samlede sig, kaldte han Hav. Og Gud så, at det var godt. + +

+
+
+
diff --git a/tests/resources/bibles/osis-kjv.xml b/tests/resources/bibles/osis-kjv.xml new file mode 100644 index 000000000..72e42e3aa --- /dev/null +++ b/tests/resources/bibles/osis-kjv.xml @@ -0,0 +1,41 @@ + + + +
+ + King James Version (1769) with Strongs Numbers and Morphology + Bible.KJV + Gen-Rev + Bible.KJV + + + Bible.KJV + + + Dict.Strongs + + + Dict.Robinsons + + + Dict.strongMorph + +
+
+THE FIRST BOOK OF MOSES CALLED GENESIS + +CHAPTER 1. +In the beginning God created the heaven and the earth. +And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters. +And God said, Let there be light: and there was light. +And God saw the light, that it was good: and God divided the light from the darkness.the light from…: Heb. between the light and between the darkness +And God called the light Day, and the darkness he called Night. And the evening and the morning were the first day.And the evening…: Heb. And the evening was, and the morning was etc. +And God said, Let there be a firmament in the midst of the waters, and let it divide the waters from the waters.firmament: Heb. expansion +And God made the firmament, and divided the waters which were under the firmament from the waters which were above the firmament: and it was so. +And God called the firmament Heaven. And the evening and the morning were the second day.And the evening…: Heb. And the evening was, and the morning was etc. +And God said, Let the waters under the heaven be gathered together unto one place, and let the dry land appear: and it was so. +And God called the dry land Earth; and the gathering together of the waters called he Seas: and God saw that it was good. + +
+
+
diff --git a/tests/resources/bibles/osis-web.xml b/tests/resources/bibles/osis-web.xml new file mode 100644 index 000000000..515739ea8 --- /dev/null +++ b/tests/resources/bibles/osis-web.xml @@ -0,0 +1,109 @@ + + + +
+ + 2007-08-26T08.23.41 +

This draft version of the World English Bible is +substantially complete in the New Testament, Genesis, Exodus, Job, Psalms, Proverbs, Ecclesiastes, Song of Solomon, and the “minor” prophets. Editing continues on the other books of the Old Testament. All WEB companion Apocrypha books are still in +rough draft form.

+

Converted web.gbf in GBF to web.osis.xml in +an XML format that is mostly compliant with OSIS 2.0 using gbf2osis.exe. +(Please see http://ebt.cx/translation/ for links to this software.)

+

GBF and OSIS metadata fields do not exactly correspond to each other, so +the conversion is not perfect in the metadata. However, the Scripture portion +should be correct.

+

No attempt was to convert quotation marks to structural markers using q or +speech elements, because this would require language and style-dependent +processing, and because the current OSIS specification is deficient in that +quotation mark processing is not guaranteed to produce the correct results +for all languages and translations. In English texts, the hard part of the +conversion to markup is figuring out what ’ means. +The other difficulty is that OSIS in no way guarantees that these punctuation +marks would be reconstituted properly by software that reads OSIS files +for anything other than modern English, and even then, it does not +accommodate all styles of punctuation and all cases. +We strongly recommend that anyone using OSIS NOT replace quotation mark +punctuation in any existing text with q or speech elements. It is better +for multiple language processing capabilities to leave the quotation +punctuation as part of the text. If you need the q or speech markup, then you +may supplement those punctuation marks with those markup elements, but specify +the n='' parameter in those elements to indicate that no generation of any +punctuation from those markup elements is required or desired. That way you +can have BOTH correct punctuation already in the text AND markup so that you +can automatically determine when you are in a quotation or not, independent +of language. This may be useful for a search by speaker, for example.

+

The output of gbf2osis marks Jesus' words in a non-standard way using the q +element AND quotation marks if they were marked with FR/Fr markers in the GBF +file. The OSIS 2.0 specification requires that quotation marks be stripped out, +and reinserted by software that reads the OSIS files when q elements are used. +This is not acceptable for the reasons given above, and we choose not to do +that, but we used the q element with who='Jesus' to indicate Jesus' words. +Do not generate any additional punctuation due to these markers. The correct +punctuation is already in the text.

+

OSIS does not currently support footnote start anchors. Therefore, these +start anchors have been represented with milestone elements, in case someone +might like to use them, for example, to start an href element in a conversion +to HTML. (OSIS sort of supports the same idea by allowing a catchword to be +defined within a footnote, but I did not implement the processing to convert +to this different way of doing things, and it isn't exactly the same, anyway.)

+

Traditional psalm book titles are rendered as text rather than titles, because +the title element does not support containing transChange elements, as would be +required to encode the KJV text using OSIS title elements. This may actually be +a superior solution, anyway, in that the Masoretic text makes no such distinction +(even though many modern typeset Bibles do make a typographic distinction in this +case).

+

The schema location headers were modified to use local copies rather than the +standard locations so that these files could be validated and used without an +Internet connection active at all times (very important for the developer's +remote island location), but you may wish to change them back.

+
+ + World English Bible + WEB committee + 2007-08-26 + Rainbow Missions, Inc. + Bible + Bible.en.WEB.draft.2007-08-26 + http://eBible.org/web/ + ENG + Wherever English is spoken in the world. + The World English Bible is dedicated to the Public Domain by the translators and editors. It is not copyrighted. “World English Bible” and the World English Bible logo are a trademarks of Rainbow +Missions, Inc. They may only be used to identify this translation of the Holy Bible as published by Rainbow Missions, Inc., and faithful copies and quotations. “Faithful copies” include copies converted to other formats (i. e. HTML, PDF, etc.) or +typeset differently, without altering the text of the Scriptures, except that changing the spellings between preferred American and British usage is allowed. Use of the markings of direct quotes of Jesus Christ for different rendition (i. e. red text) +is optional. Comments and typo reports are welcome at http://eBible.org/cgi-bin/comment.cgi. Please see http://eBible.org/web/ for updates, revision status, free downloads, and printed edition purchase information. + Gen-Mal + Tob-AddEsth + Bar-EpJer + AddDan + Matt-Rev + Bible.WEB + +
+
+
+ Genesis + +

+ +In the beginning GodThe Hebrew word rendered “God” is “Elohim.” After “God,” the Hebrew has the two letters “Aleph Tav” (the first and last letters of the Hebrew alphabet) as a grammatical marker. created the heavens and the earth. +Now the earth was formless and empty. Darkness was on the surface of the deep. God’s Spirit was hovering over the surface of the waters.

+

+ +God said, “Let there be light,” and there was light. +God saw the light, and saw that it was good. God divided the light from the darkness. +God called the light “day,” and the darkness he called “night.” There was evening and there was morning, one day.

+

+ +God said, “Let there be an expanse in the middle of the waters, and let it divide the waters from the waters.” +God made the expanse, and divided the waters which were under the expanse from the waters which were above the expanse; and it was so. +God called the expanse “sky.” There was evening and there was morning, a second day.

+

+ +God said, “Let the waters under the sky be gathered together to one place, and let the dry land appear;” and it was so. +God called the dry land “earth,” and the gathering together of the waters he called “seas.” God saw that it was good. +

+
+
+
+
diff --git a/tests/resources/bibles/web.json b/tests/resources/bibles/web.json new file mode 100644 index 000000000..0fbc95669 --- /dev/null +++ b/tests/resources/bibles/web.json @@ -0,0 +1,16 @@ +{ + "book": "Genesis", + "chapter": "1", + "verses": [ + [ "1", "In the beginning God created the heavens and the earth."], + [ "2", "Now the earth was formless and empty. Darkness was on the surface of the deep. God’s Spirit was hovering over the surface of the waters." ], + [ "3", "God said, “Let there be light,” and there was light." ], + [ "4", "God saw the light, and saw that it was good. God divided the light from the darkness." ], + [ "5", "God called the light “day,” and the darkness he called “night.” There was evening and there was morning, one day." ], + [ "6", "God said, “Let there be an expanse in the middle of the waters, and let it divide the waters from the waters.”" ], + [ "7", "God made the expanse, and divided the waters which were under the expanse from the waters which were above the expanse; and it was so." ], + [ "8", "God called the expanse “sky.” There was evening and there was morning, a second day." ], + [ "9", "God said, “Let the waters under the sky be gathered together to one place, and let the dry land appear;” and it was so." ], + [ "10", "God called the dry land “earth,” and the gathering together of the waters he called “seas.” God saw that it was good." ] + ] +} From d48bf080940e67375d8459aa1421157cf719f05c Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 24 Aug 2014 16:04:42 +0100 Subject: [PATCH 42/66] Remove a few debug prints and move a few lines... --- openlp/plugins/bibles/lib/osis.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/openlp/plugins/bibles/lib/osis.py b/openlp/plugins/bibles/lib/osis.py index 851db39c4..5dd9d9d3e 100644 --- a/openlp/plugins/bibles/lib/osis.py +++ b/openlp/plugins/bibles/lib/osis.py @@ -72,10 +72,20 @@ class OSISBible(BibleDB): osis_bible_tree = etree.parse(import_file) namespace = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'} num_books = int(osis_bible_tree.xpath("count(//ns:div[@type='book'])", namespaces=namespace)) - log.debug('number of books: %d' % num_books) self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', 'Removing unused tags (this may take a few minutes)...')) # We strip unused tags from the XML, this should leave us with only chapter, verse and div tags. + # Strip tags we don't use - remove content + etree.strip_elements(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}note', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}milestone', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}title', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}abbr', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}catchWord', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}index', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdg', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdgGroup', + '{http://www.bibletechnologies.net/2003/OSIS/namespace}figure'), + with_tail=False) # Strip tags we don't use - keep content etree.strip_tags(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}p', '{http://www.bibletechnologies.net/2003/OSIS/namespace}l', @@ -104,17 +114,6 @@ class OSISBible(BibleDB): '{http://www.bibletechnologies.net/2003/OSIS/namespace}row', '{http://www.bibletechnologies.net/2003/OSIS/namespace}cell', '{http://www.bibletechnologies.net/2003/OSIS/namespace}caption')) - # Strip tags we don't use - remove content - etree.strip_elements(osis_bible_tree, ('{http://www.bibletechnologies.net/2003/OSIS/namespace}note', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}milestone', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}title', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}abbr', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}catchWord', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}index', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdg', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}rdgGroup', - '{http://www.bibletechnologies.net/2003/OSIS/namespace}figure'), - with_tail=False) # Precompile a few xpath-querys verse_in_chapter = etree.XPath('count(//ns:chapter[1]/ns:verse)', namespaces=namespace) text_in_verse = etree.XPath('count(//ns:verse[1]/text())', namespaces=namespace) @@ -158,7 +157,6 @@ class OSISBible(BibleDB): translate('BiblesPlugin.OsisImport', 'Importing %(bookname)s %(chapter)s...' % {'bookname': db_book.name, 'chapter': chapter_number})) else: - log.debug('chapters are milestones') # The chapter tags is used as milestones. For now we assume verses is also milestones chapter_number = 0 for element in book: From 71a1b26e1cf6f919b198f67f1a741952b361c079 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sun, 24 Aug 2014 16:14:29 +0100 Subject: [PATCH 43/66] Changed test desciption. --- tests/functional/openlp_plugins/bibles/test_osisimport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/openlp_plugins/bibles/test_osisimport.py b/tests/functional/openlp_plugins/bibles/test_osisimport.py index af437c267..540403f84 100644 --- a/tests/functional/openlp_plugins/bibles/test_osisimport.py +++ b/tests/functional/openlp_plugins/bibles/test_osisimport.py @@ -102,7 +102,7 @@ class TestOsisImport(TestCase): def file_import_mixed_tags_test(self): """ - Test the actual import of OSIS Bible file, with nested chapter and milestone verse tags. + Test the actual import of OSIS Bible file, with chapter tags containing milestone verse tags. """ # GIVEN: Test files with a mocked out "manager", "import_wizard", and mocked functions # get_book_ref_id_by_name, create_verse, create_book, session and get_language. From 0b93faf7b9c877fef5714b8c9b228065278b906f Mon Sep 17 00:00:00 2001 From: Phill Ridout Date: Tue, 26 Aug 2014 18:38:07 +0100 Subject: [PATCH 44/66] PEP fixes --- openlp/core/utils/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openlp/core/utils/actions.py b/openlp/core/utils/actions.py index fb794866b..9b5117233 100644 --- a/openlp/core/utils/actions.py +++ b/openlp/core/utils/actions.py @@ -280,7 +280,7 @@ class ActionList(object): ActionList.shortcut_map[shortcuts[1]] = actions else: log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % - (shortcuts[1], action.objectName())) + (shortcuts[1], action.objectName())) shortcuts.remove(shortcuts[1]) # Check the primary shortcut. existing_actions = ActionList.shortcut_map.get(shortcuts[0], []) @@ -291,7 +291,7 @@ class ActionList(object): ActionList.shortcut_map[shortcuts[0]] = actions else: log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' % - (shortcuts[0], action.objectName())) + (shortcuts[0], action.objectName())) shortcuts.remove(shortcuts[0]) action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts]) From 62811d660fbbc005da735eb1649c2437c9719e92 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Wed, 27 Aug 2014 10:43:18 +0200 Subject: [PATCH 45/66] Implemented a workaround for bug1251437 Fixes: https://launchpad.net/bugs/1251437 --- openlp/core/utils/__init__.py | 19 +++++++++++++++++++ openlp/plugins/bibles/lib/http.py | 1 - .../openlp_plugins/bibles/test_lib_http.py | 13 +++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 9b024eb84..255384b2d 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -109,6 +109,22 @@ class VersionThread(QtCore.QThread): Registry().execute('openlp_version_check', '%s' % version) +class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler): + """ + Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248 + (Redirecting to urls with special chars) + """ + def redirect_request(self, req, fp, code, msg, headers, newurl): + # Test if the newurl can be decoded to ascii + try: + test_url = newurl.encode('latin1').decode('ascii') + fixed_url = newurl + except Exception: + # The url could not be decoded to ascii, so we do some url encoding + fixed_url = urllib.parse.quote(newurl.encode('latin1').decode('utf-8', 'replace'), safe='/:') + return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url) + + def get_application_version(): """ Returns the application version of the running instance of OpenLP:: @@ -341,6 +357,9 @@ def get_web_page(url, header=None, update_openlp=False): # http://docs.python.org/library/urllib2.html if not url: return None + # This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437 + opener = urllib.request.build_opener(HTTPRedirectHandlerFixed()) + urllib.request.install_opener(opener) req = urllib.request.Request(url) if not header or header[0].lower() != 'user-agent': user_agent = _get_user_agent() diff --git a/openlp/plugins/bibles/lib/http.py b/openlp/plugins/bibles/lib/http.py index 6b26dfabe..9f60ecc33 100644 --- a/openlp/plugins/bibles/lib/http.py +++ b/openlp/plugins/bibles/lib/http.py @@ -32,7 +32,6 @@ The :mod:`http` module enables OpenLP to retrieve scripture from bible websites. import logging import re import socket -import urllib.request import urllib.parse import urllib.error from html.parser import HTMLParseError diff --git a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py index 517732e4d..9d72677ce 100644 --- a/tests/interfaces/openlp_plugins/bibles/test_lib_http.py +++ b/tests/interfaces/openlp_plugins/bibles/test_lib_http.py @@ -59,6 +59,19 @@ class TestBibleHTTP(TestCase): # THEN: We should get back a valid service item assert len(books) == 66, 'The bible should not have had any books added or removed' + def bible_gateway_extract_books_support_redirect_test(self): + """ + Test the Bible Gateway retrieval of book list for DN1933 bible with redirect (bug 1251437) + """ + # GIVEN: A new Bible Gateway extraction class + handler = BGExtract() + + # WHEN: The Books list is called + books = handler.get_books_from_http('DN1933') + + # THEN: We should get back a valid service item + assert len(books) == 66, 'This bible should have 66 books' + def bible_gateway_extract_verse_test(self): """ Test the Bible Gateway retrieval of verse list for NIV bible John 3 From 7fecaa1d70703d191a3c74491710347d973fc9f4 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Wed, 27 Aug 2014 15:10:33 +0200 Subject: [PATCH 46/66] Added language detection when importing Fixes: https://launchpad.net/bugs/1214875 --- openlp/plugins/bibles/lib/opensong.py | 1 + openlp/plugins/bibles/lib/osis.py | 13 ++++++++++--- openlp/plugins/bibles/lib/zefania.py | 11 +++++++++-- .../openlp_plugins/bibles/test_osisimport.py | 1 - 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/bibles/lib/opensong.py b/openlp/plugins/bibles/lib/opensong.py index dccdbf2cf..420ace1ec 100644 --- a/openlp/plugins/bibles/lib/opensong.py +++ b/openlp/plugins/bibles/lib/opensong.py @@ -88,6 +88,7 @@ class OpenSongBible(BibleDB): 'Incorrect Bible file type supplied. This looks like a Zefania XML bible, ' 'please use the Zefania import option.')) return False + # No language info in the opensong format, so ask the user language_id = self.get_language(bible_name) if not language_id: log.error('Importing books from "%s" failed' % self.filename) diff --git a/openlp/plugins/bibles/lib/osis.py b/openlp/plugins/bibles/lib/osis.py index 5dd9d9d3e..9f0bb3801 100644 --- a/openlp/plugins/bibles/lib/osis.py +++ b/openlp/plugins/bibles/lib/osis.py @@ -65,12 +65,19 @@ class OSISBible(BibleDB): # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding # detection, and the two mechanisms together interfere with each other. import_file = open(self.filename, 'rb') - language_id = self.get_language(bible_name) + osis_bible_tree = etree.parse(import_file) + namespace = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'} + # Find bible language + language_id = None + language = osis_bible_tree.xpath("//ns:osisText/@xml:lang", namespaces=namespace) + if language: + language_id = BiblesResourcesDB.get_language(language[0]) + # The language couldn't be detected, ask the user + if not language_id: + language_id = self.get_language(bible_name) if not language_id: log.error('Importing books from "%s" failed' % self.filename) return False - osis_bible_tree = etree.parse(import_file) - namespace = {'ns': 'http://www.bibletechnologies.net/2003/OSIS/namespace'} num_books = int(osis_bible_tree.xpath("count(//ns:div[@type='book'])", namespaces=namespace)) self.wizard.increment_progress_bar(translate('BiblesPlugin.OsisImport', 'Removing unused tags (this may take a few minutes)...')) diff --git a/openlp/plugins/bibles/lib/zefania.py b/openlp/plugins/bibles/lib/zefania.py index c52b58eae..81fb49eb5 100644 --- a/openlp/plugins/bibles/lib/zefania.py +++ b/openlp/plugins/bibles/lib/zefania.py @@ -64,11 +64,18 @@ class ZefaniaBible(BibleDB): # NOTE: We don't need to do any of the normal encoding detection here, because lxml does it's own encoding # detection, and the two mechanisms together interfere with each other. import_file = open(self.filename, 'rb') - language_id = self.get_language(bible_name) + zefania_bible_tree = etree.parse(import_file) + # Find bible language + language_id = None + language = zefania_bible_tree.xpath("/XMLBIBLE/INFORMATION/language/text()") + if language: + language_id = BiblesResourcesDB.get_language(language[0]) + # The language couldn't be detected, ask the user + if not language_id: + language_id = self.get_language(bible_name) if not language_id: log.error('Importing books from "%s" failed' % self.filename) return False - zefania_bible_tree = etree.parse(import_file) num_books = int(zefania_bible_tree.xpath("count(//BIBLEBOOK)")) # Strip tags we don't use - keep content etree.strip_tags(zefania_bible_tree, ('STYLE', 'GRAM', 'NOTE', 'SUP', 'XREF')) diff --git a/tests/functional/openlp_plugins/bibles/test_osisimport.py b/tests/functional/openlp_plugins/bibles/test_osisimport.py index 540403f84..ba23feba1 100644 --- a/tests/functional/openlp_plugins/bibles/test_osisimport.py +++ b/tests/functional/openlp_plugins/bibles/test_osisimport.py @@ -157,6 +157,5 @@ class TestOsisImport(TestCase): # THEN: The create_verse() method should have been called with each verse in the file. self.assertTrue(importer.create_verse.called) - print(importer.create_verse.call_list()) for verse_tag, verse_text in test_data['verses']: importer.create_verse.assert_any_call(importer.create_book().id, '1', verse_tag, verse_text) From f3207dac8e2760c1f9fffaa0b836a35b42624997 Mon Sep 17 00:00:00 2001 From: Jonathan Springer Date: Wed, 27 Aug 2014 19:18:06 -0400 Subject: [PATCH 47/66] Consolidate platform specific checks into the common module --- openlp/core/__init__.py | 8 +-- openlp/core/common/__init__.py | 27 ++++++++++ openlp/core/common/applocation.py | 8 +-- openlp/core/common/registryproperties.py | 6 +-- openlp/core/common/settings.py | 6 +-- openlp/core/ui/exceptionform.py | 4 +- openlp/core/ui/maindisplay.py | 6 +-- openlp/core/ui/mainwindow.py | 13 ++--- openlp/core/ui/media/vlcplayer.py | 8 +-- openlp/core/utils/__init__.py | 4 +- openlp/core/utils/languagemanager.py | 4 +- .../presentations/lib/impresscontroller.py | 20 ++++---- .../presentations/lib/pdfcontroller.py | 4 +- .../presentations/lib/powerpointcontroller.py | 10 ++-- .../presentations/lib/pptviewcontroller.py | 8 +-- openlp/plugins/songs/forms/songselectform.py | 4 +- openlp/plugins/songs/lib/importer.py | 6 +-- .../plugins/songs/lib/importers/openoffice.py | 9 ++-- .../songs/lib/importers/songsoffellowship.py | 3 +- .../openlp_core_common/test_common.py | 51 ++++++++++++++++++- 20 files changed, 146 insertions(+), 63 deletions(-) diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 4ab94a250..32ef5c5a3 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -36,14 +36,14 @@ logging and a plugin framework are contained within the openlp.core module. import os import sys -import platform import logging from optparse import OptionParser from traceback import format_exception from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, OpenLPMixin, AppLocation, Settings, UiStrings, check_directory_exists +from openlp.core.common import Registry, OpenLPMixin, AppLocation, Settings, UiStrings, check_directory_exists, \ + is_macosx, is_win from openlp.core.lib import ScreenList from openlp.core.resources import qInitResources from openlp.core.ui.mainwindow import MainWindow @@ -126,7 +126,7 @@ class OpenLP(OpenLPMixin, QtGui.QApplication): alternate_rows_repair_stylesheet = \ 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' application_stylesheet += alternate_rows_repair_stylesheet - if os.name == 'nt': + if is_win(): application_stylesheet += NT_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) @@ -275,7 +275,7 @@ def main(args=None): # Throw the rest of the arguments at Qt, just in case. qt_args.extend(args) # Bug #1018855: Set the WM_CLASS property in X11 - if platform.system() not in ['Windows', 'Darwin']: + if not is_win() and not is_macosx(): qt_args.append('OpenLP') # Initialise the resources qInitResources() diff --git a/openlp/core/common/__init__.py b/openlp/core/common/__init__.py index 22207dec4..0776547ae 100644 --- a/openlp/core/common/__init__.py +++ b/openlp/core/common/__init__.py @@ -127,6 +127,33 @@ def de_hump(name): sub_name = FIRST_CAMEL_REGEX.sub(r'\1_\2', name) return SECOND_CAMEL_REGEX.sub(r'\1_\2', sub_name).lower() + +def is_win(): + """ + Returns true if running on a system with a nt kernel e.g. Windows, Wine + + :return: True if system is running a nt kernel false otherwise + """ + return os.name.startswith('nt') + + +def is_macosx(): + """ + Returns true if running on a system with a darwin kernel e.g. Mac OS X + + :return: True if system is running a darwin kernel false otherwise + """ + return sys.platform.startswith('darwin') + + +def is_linux(): + """ + Returns true if running on a system with a linux kernel e.g. Ubuntu, Debian, etc + + :return: True if system is running a linux kernel false otherwise + """ + return sys.platform.startswith('linux') + from .openlpmixin import OpenLPMixin from .registry import Registry from .registrymixin import RegistryMixin diff --git a/openlp/core/common/applocation.py b/openlp/core/common/applocation.py index 073d3c7f7..89f637e69 100644 --- a/openlp/core/common/applocation.py +++ b/openlp/core/common/applocation.py @@ -33,10 +33,10 @@ import logging import os import sys -from openlp.core.common import Settings +from openlp.core.common import Settings, is_win, is_macosx -if sys.platform != 'win32' and sys.platform != 'darwin': +if not is_win() and not is_macosx(): try: from xdg import BaseDirectory XDG_BASE_AVAILABLE = True @@ -145,13 +145,13 @@ def _get_os_dir_path(dir_type): directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources')) if os.path.exists(directory): return directory - if sys.platform == 'win32': + if is_win(): if dir_type == AppLocation.DataDir: return os.path.join(str(os.getenv('APPDATA')), 'openlp', 'data') elif dir_type == AppLocation.LanguageDir: return os.path.dirname(openlp.__file__) return os.path.join(str(os.getenv('APPDATA')), 'openlp') - elif sys.platform == 'darwin': + elif is_macosx(): if dir_type == AppLocation.DataDir: return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'Data') elif dir_type == AppLocation.LanguageDir: diff --git a/openlp/core/common/registryproperties.py b/openlp/core/common/registryproperties.py index 791fc33f7..e2cfffa09 100644 --- a/openlp/core/common/registryproperties.py +++ b/openlp/core/common/registryproperties.py @@ -29,9 +29,7 @@ """ Provide Registry values for adding to classes """ -import os - -from openlp.core.common import Registry +from openlp.core.common import Registry, is_win class RegistryProperties(object): @@ -45,7 +43,7 @@ class RegistryProperties(object): Adds the openlp to the class dynamically. Windows needs to access the application in a dynamic manner. """ - if os.name == 'nt': + if is_win(): return Registry().get('application') else: if not hasattr(self, '_application') or not self._application: diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 634bc5ced..f7202b590 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -36,7 +36,7 @@ import sys from PyQt4 import QtCore, QtGui -from openlp.core.common import ThemeLevel, SlideLimits, UiStrings +from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux log = logging.getLogger(__name__) @@ -44,7 +44,7 @@ log = logging.getLogger(__name__) # Fix for bug #1014422. X11_BYPASS_DEFAULT = True -if sys.platform.startswith('linux'): +if is_linux(): # Default to False on Gnome. X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID')) # Default to False on Xfce. @@ -86,7 +86,7 @@ class Settings(QtCore.QSettings): """ __default_settings__ = { 'advanced/add page break': False, - 'advanced/alternate rows': not sys.platform.startswith('win'), + 'advanced/alternate rows': not is_win(), 'advanced/current media plugin': -1, 'advanced/data path': '', 'advanced/default color': '#ffffff', diff --git a/openlp/core/ui/exceptionform.py b/openlp/core/ui/exceptionform.py index e0228a43b..65b858b75 100644 --- a/openlp/core/ui/exceptionform.py +++ b/openlp/core/ui/exceptionform.py @@ -38,7 +38,7 @@ import bs4 import sqlalchemy from lxml import etree -from openlp.core.common import RegistryProperties +from openlp.core.common import RegistryProperties, is_linux from PyQt4 import Qt, QtCore, QtGui, QtWebKit @@ -137,7 +137,7 @@ class ExceptionForm(QtGui.QDialog, Ui_ExceptionDialog, RegistryProperties): 'pyICU: %s\n' % ICU_VERSION + \ 'pyUNO bridge: %s\n' % self._pyuno_import() + \ 'VLC: %s\n' % VLC_VERSION - if platform.system() == 'Linux': + if is_linux(): if os.environ.get('KDE_FULL_SESSION') == 'true': system += 'Desktop: KDE SC\n' elif os.environ.get('GNOME_DESKTOP_SESSION_ID'): diff --git a/openlp/core/ui/maindisplay.py b/openlp/core/ui/maindisplay.py index 5c905c972..f9f00a235 100644 --- a/openlp/core/ui/maindisplay.py +++ b/openlp/core/ui/maindisplay.py @@ -43,7 +43,7 @@ import sys from PyQt4 import QtCore, QtGui, QtWebKit, QtOpenGL from PyQt4.phonon import Phonon -from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, Settings, translate +from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, Settings, translate, is_macosx from openlp.core.lib import ServiceItem, ImageSource, build_html, expand_tags, image_to_byte from openlp.core.lib.theme import BackgroundType @@ -74,7 +74,7 @@ class Display(QtGui.QGraphicsView): # OpenGL. Only white blank screen is shown on the 2nd monitor all the # time. We need to investigate more how to use OpenGL properly on Mac OS # X. - if sys.platform != 'darwin': + if not is_macosx(): self.setViewport(QtOpenGL.QGLWidget()) def setup(self): @@ -143,7 +143,7 @@ class MainDisplay(OpenLPMixin, Display, RegistryProperties): # on Mac OS X. For next OpenLP version we should test it on other # platforms. For OpenLP 2.0 keep it only for OS X to not cause any # regressions on other platforms. - if sys.platform == 'darwin': + if is_macosx(): window_flags = QtCore.Qt.FramelessWindowHint | QtCore.Qt.Window # For primary screen ensure it stays above the OS X dock # and menu bar diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 6894293ce..1a6b688c7 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -41,7 +41,8 @@ from datetime import datetime from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, translate +from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, check_directory_exists, translate, \ + is_win, is_macosx from openlp.core.lib import Renderer, OpenLPDockWidget, PluginManager, ImageManager, PluginStatus, ScreenList, \ build_icon from openlp.core.lib.ui import UiStrings, create_action @@ -289,7 +290,7 @@ class Ui_MainWindow(object): triggers=self.on_about_item_clicked) # Give QT Extra Hint that this is an About Menu Item self.about_item.setMenuRole(QtGui.QAction.AboutRole) - if os.name == 'nt': + if is_win(): self.local_help_file = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'OpenLP.chm') self.offline_help_item = create_action(main_window, 'offlineHelpItem', icon=':/system/system_help_contents.png', @@ -323,7 +324,7 @@ class Ui_MainWindow(object): # Qt on OS X looks for keywords in the menu items title to determine which menu items get added to the main # menu. If we are running on Mac OS X the menu items whose title contains those keywords but don't belong in the # main menu need to be marked as such with QAction.NoRole. - if sys.platform == 'darwin': + if is_macosx(): self.settings_shortcuts_item.setMenuRole(QtGui.QAction.NoRole) self.formatting_tag_item.setMenuRole(QtGui.QAction.NoRole) add_actions(self.settings_menu, (self.settings_plugin_list_item, self.settings_language_menu.menuAction(), @@ -332,7 +333,7 @@ class Ui_MainWindow(object): add_actions(self.tools_menu, (self.tools_open_data_folder, None)) add_actions(self.tools_menu, (self.tools_first_time_wizard, None)) add_actions(self.tools_menu, [self.update_theme_images]) - if os.name == 'nt': + if is_win(): add_actions(self.help_menu, (self.offline_help_item, self.on_line_help_item, None, self.web_site_item, self.about_item)) else: @@ -426,7 +427,7 @@ class Ui_MainWindow(object): self.settings_plugin_list_item.setStatusTip(translate('OpenLP.MainWindow', 'List the Plugins')) self.about_item.setText(translate('OpenLP.MainWindow', '&About')) self.about_item.setStatusTip(translate('OpenLP.MainWindow', 'More information about OpenLP')) - if os.name == 'nt': + if is_win(): self.offline_help_item.setText(translate('OpenLP.MainWindow', '&User Guide')) self.on_line_help_item.setText(translate('OpenLP.MainWindow', '&Online Help')) self.search_shortcut_action.setText(UiStrings().Search) @@ -1073,7 +1074,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow, RegistryProperties): if self.live_controller.display: self.live_controller.display.close() self.live_controller.display = None - if os.name == 'nt': + if is_win(): # Needed for Windows to stop crashes on exit Registry().remove('application') diff --git a/openlp/core/ui/media/vlcplayer.py b/openlp/core/ui/media/vlcplayer.py index d02526b0e..4394126c0 100644 --- a/openlp/core/ui/media/vlcplayer.py +++ b/openlp/core/ui/media/vlcplayer.py @@ -38,7 +38,7 @@ import threading from PyQt4 import QtGui -from openlp.core.common import Settings +from openlp.core.common import Settings, is_win, is_macosx from openlp.core.lib import translate from openlp.core.ui.media import MediaState from openlp.core.ui.media.mediaplayer import MediaPlayer @@ -52,7 +52,7 @@ try: except (ImportError, NameError, NotImplementedError): pass except OSError as e: - if sys.platform.startswith('win'): + if is_win(): if not isinstance(e, WindowsError) and e.winerror != 126: raise else: @@ -139,9 +139,9 @@ class VlcPlayer(MediaPlayer): # You have to give the id of the QFrame (or similar object) # to vlc, different platforms have different functions for this. win_id = int(display.vlc_widget.winId()) - if sys.platform == "win32": + if is_win(): display.vlc_media_player.set_hwnd(win_id) - elif sys.platform == "darwin": + elif is_macosx(): # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa # framework and not the old Carbon. display.vlc_media_player.set_nsobject(win_id) diff --git a/openlp/core/utils/__init__.py b/openlp/core/utils/__init__.py index 9b024eb84..bd72db1ac 100644 --- a/openlp/core/utils/__init__.py +++ b/openlp/core/utils/__init__.py @@ -44,10 +44,10 @@ from random import randint from PyQt4 import QtGui, QtCore -from openlp.core.common import Registry, AppLocation, Settings +from openlp.core.common import Registry, AppLocation, Settings, is_win, is_macosx -if sys.platform != 'win32' and sys.platform != 'darwin': +if not is_win() and not is_macosx(): try: from xdg import BaseDirectory XDG_BASE_AVAILABLE = True diff --git a/openlp/core/utils/languagemanager.py b/openlp/core/utils/languagemanager.py index dd048e04c..3c741e58f 100644 --- a/openlp/core/utils/languagemanager.py +++ b/openlp/core/utils/languagemanager.py @@ -35,7 +35,7 @@ import sys from PyQt4 import QtCore, QtGui -from openlp.core.common import AppLocation, Settings, translate +from openlp.core.common import AppLocation, Settings, translate, is_win, is_macosx log = logging.getLogger(__name__) @@ -60,7 +60,7 @@ class LanguageManager(object): app_translator = QtCore.QTranslator() app_translator.load(language, lang_path) # A translator for buttons and other default strings provided by Qt. - if sys.platform != 'win32' and sys.platform != 'darwin': + if not is_win() and not is_macosx(): lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath) default_translator = QtCore.QTranslator() default_translator.load('qt_%s' % language, lang_path) diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index 1d5e111c9..d032b9161 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -42,7 +42,9 @@ import logging import os import time -if os.name == 'nt': +from openlp.core.common import is_win + +if is_win(): from win32com.client import Dispatch import pywintypes # Declare an empty exception to match the exception imported from UNO @@ -93,7 +95,7 @@ class ImpressController(PresentationController): Impress is able to run on this machine. """ log.debug('check_available') - if os.name == 'nt': + if is_win(): return self.get_com_servicemanager() is not None else: return uno_available @@ -104,7 +106,7 @@ class ImpressController(PresentationController): UNO interface when required. """ log.debug('start process Openoffice') - if os.name == 'nt': + if is_win(): self.manager = self.get_com_servicemanager() self.manager._FlagAsMethod('Bridge_GetStruct') self.manager._FlagAsMethod('Bridge_GetValueObject') @@ -175,7 +177,7 @@ class ImpressController(PresentationController): self.docs[0].close_presentation() desktop = None try: - if os.name != 'nt': + if not is_win(): desktop = self.get_uno_desktop() else: desktop = self.get_com_desktop() @@ -223,7 +225,7 @@ class ImpressDocument(PresentationDocument): is available the presentation is loaded and started. """ log.debug('Load Presentation OpenOffice') - if os.name == 'nt': + if is_win(): desktop = self.controller.get_com_desktop() if desktop is None: self.controller.start_process() @@ -236,7 +238,7 @@ class ImpressDocument(PresentationDocument): return False self.desktop = desktop properties = [] - if os.name != 'nt': + if not is_win(): # Recent versions of Impress on Windows won't start the presentation if it starts as minimized. It seems OK # on Linux though. properties.append(self.create_property('Minimized', True)) @@ -246,7 +248,7 @@ class ImpressDocument(PresentationDocument): except: log.warning('Failed to load presentation %s' % url) return False - if os.name == 'nt': + if is_win(): # As we can't start minimized the Impress window gets in the way. # Either window.setPosSize(0, 0, 200, 400, 12) or .setVisible(False) window = self.document.getCurrentController().getFrame().getContainerWindow() @@ -264,7 +266,7 @@ class ImpressDocument(PresentationDocument): log.debug('create thumbnails OpenOffice') if self.check_thumbnails(): return - if os.name == 'nt': + if is_win(): thumb_dir_url = 'file:///' + self.get_temp_folder().replace('\\', '/') \ .replace(':', '|').replace(' ', '%20') else: @@ -297,7 +299,7 @@ class ImpressDocument(PresentationDocument): Create an OOo style property object which are passed into some Uno methods. """ log.debug('create property OpenOffice') - if os.name == 'nt': + if is_win(): property_object = self.controller.manager.Bridge_GetStruct('com.sun.star.beans.PropertyValue') else: property_object = PropertyValue() diff --git a/openlp/plugins/presentations/lib/pdfcontroller.py b/openlp/plugins/presentations/lib/pdfcontroller.py index b98ae131a..0283fefd4 100644 --- a/openlp/plugins/presentations/lib/pdfcontroller.py +++ b/openlp/plugins/presentations/lib/pdfcontroller.py @@ -34,7 +34,7 @@ import re from subprocess import check_output, CalledProcessError, STDOUT from openlp.core.utils import AppLocation -from openlp.core.common import Settings +from openlp.core.common import Settings, is_win from openlp.core.lib import ScreenList from .presentationcontroller import PresentationController, PresentationDocument @@ -123,7 +123,7 @@ class PdfController(PresentationController): else: # Fallback to autodetection application_path = AppLocation.get_directory(AppLocation.AppDir) - if os.name == 'nt': + if is_win(): # for windows we only accept mudraw.exe in the base folder application_path = AppLocation.get_directory(AppLocation.AppDir) if os.path.isfile(os.path.join(application_path, 'mudraw.exe')): diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index 0f9c2ff35..f42e4f814 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -33,7 +33,9 @@ This modul is for controlling powerpiont. PPT API documentation: import os import logging -if os.name == 'nt': +from openlp.core.common import is_win + +if is_win(): from win32com.client import Dispatch import winreg import win32ui @@ -69,7 +71,7 @@ class PowerpointController(PresentationController): PowerPoint is able to run on this machine. """ log.debug('check_available') - if os.name == 'nt': + if is_win(): try: winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, 'PowerPoint.Application').Close() return True @@ -77,7 +79,7 @@ class PowerpointController(PresentationController): pass return False - if os.name == 'nt': + if is_win(): def start_process(self): """ Loads PowerPoint process. @@ -271,7 +273,7 @@ class PowerpointDocument(PresentationDocument): trace_error_handler(log) self.show_error_msg() - if os.name == 'nt': + if is_win(): def start_presentation(self): """ Starts a presentation from the beginning. diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index a9090dd1e..7e03e322f 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -30,7 +30,9 @@ import logging import os -if os.name == 'nt': +from openlp.core.common import is_win + +if is_win(): from ctypes import cdll from ctypes.wintypes import RECT @@ -63,11 +65,11 @@ class PptviewController(PresentationController): PPT Viewer is able to run on this machine. """ log.debug('check_available') - if os.name != 'nt': + if not is_win(): return False return self.check_installed() - if os.name == 'nt': + if is_win(): def check_installed(self): """ Check the viewer is installed. diff --git a/openlp/plugins/songs/forms/songselectform.py b/openlp/plugins/songs/forms/songselectform.py index f9f658c5b..aecdf9682 100755 --- a/openlp/plugins/songs/forms/songselectform.py +++ b/openlp/plugins/songs/forms/songselectform.py @@ -37,7 +37,7 @@ from time import sleep from PyQt4 import QtCore, QtGui from openlp.core import Settings -from openlp.core.common import Registry +from openlp.core.common import Registry, is_win from openlp.core.lib import translate from openlp.plugins.songs.forms.songselectdialog import Ui_SongSelectDialog from openlp.plugins.songs.lib.songselect import SongSelectImport @@ -377,7 +377,7 @@ class SongSelectForm(QtGui.QDialog, Ui_SongSelectDialog): Adds the openlp to the class dynamically. Windows needs to access the application in a dynamic manner. """ - if os.name == 'nt': + if is_win(): return Registry().get('application') else: if not hasattr(self, '_application'): diff --git a/openlp/plugins/songs/lib/importer.py b/openlp/plugins/songs/lib/importer.py index e9ee2c2f3..12c8887f7 100644 --- a/openlp/plugins/songs/lib/importer.py +++ b/openlp/plugins/songs/lib/importer.py @@ -32,7 +32,7 @@ The :mod:`importer` modules provides the general song import functionality. import os import logging -from openlp.core.common import translate, UiStrings +from openlp.core.common import translate, UiStrings, is_win from openlp.core.ui.wizard import WizardStrings from .importers.opensong import OpenSongImport from .importers.easyslides import EasySlidesImport @@ -70,14 +70,14 @@ except ImportError: log.exception('Error importing %s', 'OooImport') HAS_OOO = False HAS_MEDIASHOUT = False -if os.name == 'nt': +if is_win(): try: from .importers.mediashout import MediaShoutImport HAS_MEDIASHOUT = True except ImportError: log.exception('Error importing %s', 'MediaShoutImport') HAS_WORSHIPCENTERPRO = False -if os.name == 'nt': +if is_win(): try: from .importers.worshipcenterpro import WorshipCenterProImport HAS_WORSHIPCENTERPRO = True diff --git a/openlp/plugins/songs/lib/importers/openoffice.py b/openlp/plugins/songs/lib/importers/openoffice.py index 0e499f7ae..a0bb1df88 100644 --- a/openlp/plugins/songs/lib/importers/openoffice.py +++ b/openlp/plugins/songs/lib/importers/openoffice.py @@ -32,13 +32,14 @@ import time from PyQt4 import QtCore +from openlp.core.common import is_win from openlp.core.utils import get_uno_command, get_uno_instance from openlp.core.lib import translate from .songimport import SongImport log = logging.getLogger(__name__) -if os.name == 'nt': +if is_win(): from win32com.client import Dispatch NoConnectException = Exception else: @@ -106,7 +107,7 @@ class OpenOfficeImport(SongImport): Start OpenOffice.org process TODO: The presentation/Impress plugin may already have it running """ - if os.name == 'nt': + if is_win(): self.start_ooo_process() self.desktop = self.ooo_manager.createInstance('com.sun.star.frame.Desktop') else: @@ -133,7 +134,7 @@ class OpenOfficeImport(SongImport): Start the OO Process """ try: - if os.name == 'nt': + if is_win(): self.ooo_manager = Dispatch('com.sun.star.ServiceManager') self.ooo_manager._FlagAsMethod('Bridge_GetStruct') self.ooo_manager._FlagAsMethod('Bridge_GetValueObject') @@ -150,7 +151,7 @@ class OpenOfficeImport(SongImport): Open the passed file in OpenOffice.org Impress """ self.file_path = file_path - if os.name == 'nt': + if is_win(): url = file_path.replace('\\', '/') url = url.replace(':', '|').replace(' ', '%20') url = 'file:///' + url diff --git a/openlp/plugins/songs/lib/importers/songsoffellowship.py b/openlp/plugins/songs/lib/importers/songsoffellowship.py index c1ef8666f..2cc49caef 100644 --- a/openlp/plugins/songs/lib/importers/songsoffellowship.py +++ b/openlp/plugins/songs/lib/importers/songsoffellowship.py @@ -37,12 +37,13 @@ import logging import os import re +from openlp.core.common import is_win from .openoffice import OpenOfficeImport log = logging.getLogger(__name__) -if os.name == 'nt': +if is_win(): from .openoffice import PAGE_BEFORE, PAGE_AFTER, PAGE_BOTH RuntimeException = Exception else: diff --git a/tests/functional/openlp_core_common/test_common.py b/tests/functional/openlp_core_common/test_common.py index f52256c5c..0474bd404 100644 --- a/tests/functional/openlp_core_common/test_common.py +++ b/tests/functional/openlp_core_common/test_common.py @@ -32,7 +32,8 @@ Functional tests to test the AppLocation class and related methods. from unittest import TestCase -from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate +from openlp.core.common import check_directory_exists, de_hump, trace_error_handler, translate, is_win, is_macosx, \ + is_linux from tests.functional import MagicMock, patch @@ -139,3 +140,51 @@ class TestCommonFunctions(TestCase): # THEN: the translated string should be returned, and the mocked function should have been called mocked_translate.assert_called_with(context, text, comment, encoding, n) self.assertEqual('Translated string', result, 'The translated string should have been returned') + + def is_win_test(self): + """ + Test the is_win() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'nt' and 'win32' repectivly + mocked_os.name = 'nt' + mocked_sys.platform = 'win32' + + # THEN: The three platform functions should perform properly + self.assertTrue(is_win(), 'is_win() should return True') + self.assertFalse(is_macosx(), 'is_macosx() should return False') + self.assertFalse(is_linux(), 'is_linux() should return False') + + def is_macosx_test(self): + """ + Test the is_macosx() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'darwin' repectivly + mocked_os.name = 'posix' + mocked_sys.platform = 'darwin' + + # THEN: The three platform functions should perform properly + self.assertTrue(is_macosx(), 'is_macosx() should return True') + self.assertFalse(is_win(), 'is_win() should return False') + self.assertFalse(is_linux(), 'is_linux() should return False') + + def is_linux_test(self): + """ + Test the is_linux() function + """ + # GIVEN: Mocked out objects + with patch('openlp.core.common.os') as mocked_os, patch('openlp.core.common.sys') as mocked_sys: + + # WHEN: The mocked os.name and sys.platform are set to 'posix' and 'linux3' repectivly + mocked_os.name = 'posix' + mocked_sys.platform = 'linux3' + + # THEN: The three platform functions should perform properly + self.assertTrue(is_linux(), 'is_linux() should return True') + self.assertFalse(is_win(), 'is_win() should return False') + self.assertFalse(is_macosx(), 'is_macosx() should return False') From 25608fd672a07413d39a9be8c14cc84f707dd34a Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 2 Sep 2014 21:15:18 +0100 Subject: [PATCH 48/66] Fixed signals. --- .../media/forms/mediaclipselectordialog.py | 4 +-- .../media/forms/mediaclipselectorform.py | 26 +++++++++---------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index 88b4616eb..94d5ee05e 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -160,8 +160,8 @@ class Ui_MediaClipSelector(object): # Save and close buttons self.button_box = QtGui.QDialogButtonBox(media_clip_selector) self.button_box.addButton(QtGui.QDialogButtonBox.Save) - self.button_box.addButton(QtGui.QDialogButtonBox.Cancel) - self.close_button = self.button_box.button(QtGui.QDialogButtonBox.Cancel) + self.button_box.addButton(QtGui.QDialogButtonBox.Close) + self.close_button = self.button_box.button(QtGui.QDialogButtonBox.Close) self.save_button = self.button_box.button(QtGui.QDialogButtonBox.Save) self.main_layout.addWidget(self.button_box) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 449b95d46..28d37f32e 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -195,12 +195,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): return True @QtCore.pyqtSlot(bool) - def on_load_disc_pushbutton_clicked(self, clicked): + def on_load_disc_button_clicked(self, clicked): """ Load the media when the load-button has been clicked :param clicked: Given from signal, not used. """ + log.debug('on_load_disc_button_clicked') self.disable_all() path = self.media_path_combobox.currentText() # Check if given path is non-empty and exists before starting VLC @@ -268,7 +269,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(bool) - def on_play_pushbutton_clicked(self, clicked): + def on_play_button_clicked(self, clicked): """ Toggle the playback @@ -283,7 +284,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.play_button.setIcon(self.pause_icon) @QtCore.pyqtSlot(bool) - def on_set_start_pushbutton_clicked(self, clicked): + def on_set_start_button_clicked(self, clicked): """ Copy the current player position to start_position_edit @@ -299,7 +300,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.end_timeedit.setTime(new_pos_time) @QtCore.pyqtSlot(bool) - def on_set_end_pushbutton_clicked(self, clicked): + def on_set_end_button_clicked(self, clicked): """ Copy the current player position to end_timeedit @@ -339,7 +340,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.start_position_edit.setTime(new_time) @QtCore.pyqtSlot(bool) - def on_jump_end_pushbutton_clicked(self, clicked): + def on_jump_end_button_clicked(self, clicked): """ Set the player position to the position stored in end_timeedit @@ -353,7 +354,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.set_time(end_time_ms) @QtCore.pyqtSlot(bool) - def on_jump_start_pushbutton_clicked(self, clicked): + def on_jump_start_button_clicked(self, clicked): """ Set the player position to the position stored in start_position_edit @@ -367,13 +368,13 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.set_time(start_time_ms) @QtCore.pyqtSlot(int) - def on_title_combo_box_currentIndexChanged(self, index): + def on_titles_combo_box_currentIndexChanged(self, index): """ When a new title is chosen, it is loaded by VLC and info about audio and subtitle tracks is reloaded :param index: The index of the newly chosen title track. """ - log.debug('in on_title_combo_box_changed, index: %d', index) + log.debug('in on_titles_combo_box_changed, index: %d', index) if not self.vlc_media_player: log.error('vlc_media_player was None') return @@ -474,7 +475,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if subtitle_track: self.vlc_media_player.video_set_spu(int(subtitle_track)) - def on_position_horizontalslider_sliderMoved(self, position): + def on_position_slider_sliderMoved(self, position): """ Set player position according to new slider position. @@ -530,14 +531,11 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.jump_end_button.setDisabled(action) self.save_button.setDisabled(action) - @QtCore.pyqtSlot(bool) - def on_save_pushbutton_clicked(self, clicked): + def accept(self): """ Saves the current media and trackinfo as a clip to the mediamanager - - :param clicked: Given from signal, not used. """ - log.debug('in on_save_pushbutton_clicked') + log.debug('in on_save_button_clicked') start_time = self.start_position_edit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ start_time.minute() * 60 * 1000 + \ From 3b4ce374f2bffe476c0a828d094e3a955d8abcc6 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Tue, 2 Sep 2014 21:24:57 +0100 Subject: [PATCH 49/66] pep8 fix --- openlp/plugins/media/forms/mediaclipselectordialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/media/forms/mediaclipselectordialog.py b/openlp/plugins/media/forms/mediaclipselectordialog.py index 94d5ee05e..ca091693e 100644 --- a/openlp/plugins/media/forms/mediaclipselectordialog.py +++ b/openlp/plugins/media/forms/mediaclipselectordialog.py @@ -101,7 +101,8 @@ class Ui_MediaClipSelector(object): # Preview frame self.preview_frame = QtGui.QFrame(media_clip_selector) self.preview_frame.setMinimumSize(QtCore.QSize(320, 240)) - self.preview_frame.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, QtGui.QSizePolicy.MinimumExpanding)) + self.preview_frame.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.MinimumExpanding, + QtGui.QSizePolicy.MinimumExpanding)) self.preview_frame.setStyleSheet('background-color:black;') self.preview_frame.setFrameShape(QtGui.QFrame.NoFrame) self.preview_frame.setObjectName('preview_frame') From cf8f15964c0f898ee6a3abd9a9ea988ab51b00e5 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 2 Sep 2014 23:15:58 +0200 Subject: [PATCH 50/66] Move to use the new is_macosx() method, and tweak the theme wizard a little more. --- openlp/core/ui/firsttimewizard.py | 6 ++--- openlp/core/ui/themeform.py | 1 + openlp/core/ui/themewizard.py | 44 +++++++++++++++---------------- openlp/core/ui/wizard.py | 5 ++-- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/openlp/core/ui/firsttimewizard.py b/openlp/core/ui/firsttimewizard.py index 3e7f057ea..c5098eda6 100644 --- a/openlp/core/ui/firsttimewizard.py +++ b/openlp/core/ui/firsttimewizard.py @@ -31,9 +31,7 @@ The UI widgets for the first time wizard. """ from PyQt4 import QtCore, QtGui -import sys - -from openlp.core.common import translate +from openlp.core.common import translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page @@ -66,7 +64,7 @@ class Ui_FirstTimeWizard(object): first_time_wizard.setModal(True) first_time_wizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage | QtGui.QWizard.HaveCustomButton1) - if sys.platform == 'darwin': + if is_macosx(): first_time_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) first_time_wizard.resize(634, 386) diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index 46fd227dd..dcf081f8b 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -170,6 +170,7 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): else: pixmap_width = int(pixmap_height * self.display_aspect_ratio + 0.5) self.preview_box_label.setFixedSize(pixmap_width + 2 * frame_width, pixmap_height + 2 * frame_width) + print(self.size()) def validateCurrentPage(self): """ diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index 60878536a..0046e3e1d 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -29,11 +29,9 @@ """ The Create/Edit theme wizard """ -import sys - from PyQt4 import QtCore, QtGui -from openlp.core.common import UiStrings, translate +from openlp.core.common import UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.theme import HorizontalType, BackgroundType, BackgroundGradientType from openlp.core.lib.ui import add_welcome_page, create_valign_selection_widgets @@ -43,21 +41,21 @@ class Ui_ThemeWizard(object): """ The Create/Edit theme wizard """ - def setupUi(self, themeWizard): + def setupUi(self, theme_wizard): """ Set up the UI """ - themeWizard.setObjectName('OpenLP.ThemeWizard') - themeWizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) - themeWizard.setModal(True) - themeWizard.setOptions(QtGui.QWizard.IndependentPages | + theme_wizard.setObjectName('OpenLP.ThemeWizard') + theme_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) + theme_wizard.setModal(True) + theme_wizard.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) - if sys.platform == 'darwin': - themeWizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - #themeWizard.resize(634, 386) + if is_macosx(): + theme_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) + theme_wizard.resize(646, 386) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) # Welcome Page - add_welcome_page(themeWizard, ':/wizards/wizard_createtheme.bmp') + add_welcome_page(theme_wizard, ':/wizards/wizard_createtheme.bmp') # Background Page self.background_page = QtGui.QWizardPage() self.background_page.setObjectName('background_page') @@ -141,7 +139,7 @@ class Ui_ThemeWizard(object): self.transparent_layout.setObjectName('Transparent_layout') self.background_stack.addWidget(self.transparent_widget) self.background_layout.addLayout(self.background_stack) - themeWizard.addPage(self.background_page) + theme_wizard.addPage(self.background_page) # Main Area Page self.main_area_page = QtGui.QWizardPage() self.main_area_page.setObjectName('main_area_page') @@ -222,7 +220,7 @@ class Ui_ThemeWizard(object): self.shadow_size_spin_box.setObjectName('shadow_size_spin_box') self.shadow_layout.addWidget(self.shadow_size_spin_box) self.main_area_layout.addRow(self.shadow_check_box, self.shadow_layout) - themeWizard.addPage(self.main_area_page) + theme_wizard.addPage(self.main_area_page) # Footer Area Page self.footer_area_page = QtGui.QWizardPage() self.footer_area_page.setObjectName('footer_area_page') @@ -246,7 +244,7 @@ class Ui_ThemeWizard(object): self.footer_size_spin_box.setObjectName('FooterSizeSpinBox') self.footer_area_layout.addRow(self.footer_size_label, self.footer_size_spin_box) self.footer_area_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) - themeWizard.addPage(self.footer_area_page) + theme_wizard.addPage(self.footer_area_page) # Alignment Page self.alignment_page = QtGui.QWizardPage() self.alignment_page.setObjectName('alignment_page') @@ -268,7 +266,7 @@ class Ui_ThemeWizard(object): self.transitions_check_box.setObjectName('transitions_check_box') self.alignment_layout.addRow(self.transitions_label, self.transitions_check_box) self.alignment_layout.setItem(3, QtGui.QFormLayout.LabelRole, self.spacer) - themeWizard.addPage(self.alignment_page) + theme_wizard.addPage(self.alignment_page) # Area Position Page self.area_position_page = QtGui.QWizardPage() self.area_position_page.setObjectName('area_position_page') @@ -338,7 +336,7 @@ class Ui_ThemeWizard(object): self.footer_height_spin_box.setObjectName('footer_height_spin_box') self.footer_position_layout.addRow(self.footer_height_label, self.footer_height_spin_box) self.area_position_layout.addWidget(self.footer_position_group_box) - themeWizard.addPage(self.area_position_page) + theme_wizard.addPage(self.area_position_page) # Preview Page self.preview_page = QtGui.QWizardPage() self.preview_page.setObjectName('preview_page') @@ -366,8 +364,8 @@ class Ui_ThemeWizard(object): self.preview_box_label.setObjectName('preview_box_label') self.preview_area_layout.addWidget(self.preview_box_label) self.preview_layout.addWidget(self.preview_area) - themeWizard.addPage(self.preview_page) - self.retranslateUi(themeWizard) + theme_wizard.addPage(self.preview_page) + self.retranslateUi(theme_wizard) QtCore.QObject.connect(self.background_combo_box, QtCore.SIGNAL('currentIndexChanged(int)'), self.background_stack, QtCore.SLOT('setCurrentIndex(int)')) QtCore.QObject.connect(self.outline_check_box, QtCore.SIGNAL('toggled(bool)'), self.outline_color_button, @@ -395,11 +393,11 @@ class Ui_ThemeWizard(object): QtCore.QObject.connect(self.footer_position_check_box, QtCore.SIGNAL('toggled(bool)'), self.footer_height_spin_box, QtCore.SLOT('setDisabled(bool)')) - def retranslateUi(self, themeWizard): + def retranslateUi(self, theme_wizard): """ Translate the UI on the fly """ - themeWizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard')) + theme_wizard.setWindowTitle(translate('OpenLP.ThemeWizard', 'Theme Wizard')) self.title_label.setText('%s' % translate('OpenLP.ThemeWizard', 'Welcome to the Theme Wizard')) self.information_label.setText( @@ -488,8 +486,8 @@ class Ui_ThemeWizard(object): self.footer_height_label.setText(translate('OpenLP.ThemeWizard', 'Height:')) self.footer_height_spin_box.setSuffix(translate('OpenLP.ThemeWizard', 'px')) self.footer_position_check_box.setText(translate('OpenLP.ThemeWizard', 'Use default location')) - themeWizard.setOption(QtGui.QWizard.HaveCustomButton1, False) - themeWizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview')) + theme_wizard.setOption(QtGui.QWizard.HaveCustomButton1, False) + theme_wizard.setButtonText(QtGui.QWizard.CustomButton1, translate('OpenLP.ThemeWizard', 'Layout Preview')) self.preview_page.setTitle(translate('OpenLP.ThemeWizard', 'Preview and Save')) self.preview_page.setSubTitle(translate('OpenLP.ThemeWizard', 'Preview the theme and save it.')) self.theme_name_label.setText(translate('OpenLP.ThemeWizard', 'Theme name:')) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index 4ba258780..c5a969f9e 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -31,11 +31,10 @@ The :mod:``wizard`` module provides generic wizard tools for OpenLP. """ import logging import os -import sys from PyQt4 import QtGui -from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate +from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.lib.ui import add_welcome_page @@ -124,7 +123,7 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties): self.setModal(True) self.setOptions(QtGui.QWizard.IndependentPages | QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage) - if sys.platform == 'darwin': + if is_macosx(): self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) #self.resize(634, 386) add_welcome_page(self, image) From ceeb1b011876cf409177c0dfbb2affed469102ac Mon Sep 17 00:00:00 2001 From: Jonathan Springer Date: Thu, 4 Sep 2014 20:27:21 +0200 Subject: [PATCH 51/66] Removed icons from menu items on Mac OS X --- openlp/core/lib/ui.py | 4 +++- openlp/core/ui/mainwindow.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index cbc35e28d..af4b263d3 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -33,7 +33,7 @@ import logging from PyQt4 import QtCore, QtGui -from openlp.core.common import Registry, UiStrings, translate +from openlp.core.common import Registry, UiStrings, translate, is_macosx from openlp.core.lib import build_icon from openlp.core.utils.actions import ActionList @@ -247,6 +247,8 @@ def create_action(parent, name, **kwargs): """ action = QtGui.QAction(parent) action.setObjectName(name) + if is_macosx(): + action.setIconVisibleInMenu(False) if kwargs.get('text'): action.setText(kwargs.pop('text')) if kwargs.get('icon'): diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 1a6b688c7..be2902b9b 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -118,10 +118,12 @@ class Ui_MainWindow(object): self.recent_files_menu = QtGui.QMenu(self.file_menu) self.recent_files_menu.setObjectName('recentFilesMenu') self.file_import_menu = QtGui.QMenu(self.file_menu) - self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) + if not is_macosx(): + self.file_import_menu.setIcon(build_icon(u':/general/general_import.png')) self.file_import_menu.setObjectName('file_import_menu') self.file_export_menu = QtGui.QMenu(self.file_menu) - self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) + if not is_macosx(): + self.file_export_menu.setIcon(build_icon(u':/general/general_export.png')) self.file_export_menu.setObjectName('file_export_menu') # View Menu self.view_menu = QtGui.QMenu(self.menu_bar) From 496c12b2db478097b198d6c7f108c6ea43e1275e Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 4 Sep 2014 22:10:34 +0200 Subject: [PATCH 52/66] Reformatted a comment; Renamed NT to WIN; Made OpenLPMixin constructor slightly more robust. --- openlp.py | 7 +++---- openlp/core/__init__.py | 4 ++-- openlp/core/common/openlpmixin.py | 8 +++----- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/openlp.py b/openlp.py index 8a53fe965..5d507606d 100755 --- a/openlp.py +++ b/openlp.py @@ -36,10 +36,9 @@ if __name__ == '__main__': """ Instantiate and run the application. """ - # Mac OS X passes arguments like '-psn_XXXX' to gui application. - # This argument is process serial number. However, this causes - # conflict with other OpenLP arguments. Since we do not use this - # argument we can delete it to avoid any potential conflicts. + # Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number. + # However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it + # to avoid any potential conflicts. if sys.platform.startswith('darwin'): sys.argv = [x for x in sys.argv if not x.startswith('-psn')] main() diff --git a/openlp/core/__init__.py b/openlp/core/__init__.py index 32ef5c5a3..cb9105797 100644 --- a/openlp/core/__init__.py +++ b/openlp/core/__init__.py @@ -59,7 +59,7 @@ __all__ = ['OpenLP', 'main'] log = logging.getLogger() -NT_REPAIR_STYLESHEET = """ +WIN_REPAIR_STYLESHEET = """ QMainWindow::separator { border: none; @@ -127,7 +127,7 @@ class OpenLP(OpenLPMixin, QtGui.QApplication): 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n' application_stylesheet += alternate_rows_repair_stylesheet if is_win(): - application_stylesheet += NT_REPAIR_STYLESHEET + application_stylesheet += WIN_REPAIR_STYLESHEET if application_stylesheet: self.setStyleSheet(application_stylesheet) show_splash = Settings().value('core/show splash') diff --git a/openlp/core/common/openlpmixin.py b/openlp/core/common/openlpmixin.py index 1c7fe7d5a..3e8a8926a 100644 --- a/openlp/core/common/openlpmixin.py +++ b/openlp/core/common/openlpmixin.py @@ -33,6 +33,7 @@ import logging import inspect from openlp.core.common import trace_error_handler + DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed', 'preview_size_changed', 'resizeEvent'] @@ -41,11 +42,8 @@ class OpenLPMixin(object): """ Base Calling object for OpenLP classes. """ - def __init__(self, parent): - try: - super(OpenLPMixin, self).__init__(parent) - except TypeError: - super(OpenLPMixin, self).__init__() + def __init__(self, *args, **kwargs): + super(OpenLPMixin, self).__init__(*args, **kwargs) self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__)) if self.logger.getEffectiveLevel() == logging.DEBUG: for name, m in inspect.getmembers(self, inspect.ismethod): From 1f9c0a31f88f19d4a07abf632ceb0a05b8620de1 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Thu, 4 Sep 2014 22:25:23 +0200 Subject: [PATCH 53/66] Made wizard image bigger, and made Theme wizard bigger --- openlp/core/ui/themeform.py | 1 - openlp/core/ui/themewizard.py | 2 +- resources/images/openlp-osx-wizard.png | Bin 38669 -> 39507 bytes 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openlp/core/ui/themeform.py b/openlp/core/ui/themeform.py index dcf081f8b..46fd227dd 100644 --- a/openlp/core/ui/themeform.py +++ b/openlp/core/ui/themeform.py @@ -170,7 +170,6 @@ class ThemeForm(QtGui.QWizard, Ui_ThemeWizard, RegistryProperties): else: pixmap_width = int(pixmap_height * self.display_aspect_ratio + 0.5) self.preview_box_label.setFixedSize(pixmap_width + 2 * frame_width, pixmap_height + 2 * frame_width) - print(self.size()) def validateCurrentPage(self): """ diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index 0046e3e1d..c9c6f7e35 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -52,7 +52,7 @@ class Ui_ThemeWizard(object): QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) if is_macosx(): theme_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - theme_wizard.resize(646, 386) + theme_wizard.resize(646, 400) self.spacer = QtGui.QSpacerItem(10, 0, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) # Welcome Page add_welcome_page(theme_wizard, ':/wizards/wizard_createtheme.bmp') diff --git a/resources/images/openlp-osx-wizard.png b/resources/images/openlp-osx-wizard.png index e748ba4508371809f596c1cda5d25abac25f9491..79437b5ed3ab50cde8632397bcf452412c3030b5 100644 GIT binary patch literal 39507 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 DEDbyx literal 38669 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 Date: Thu, 4 Sep 2014 23:38:39 +0200 Subject: [PATCH 54/66] Make tabbed docks look better by using document mode --- openlp/core/ui/mainwindow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index be2902b9b..77a903c5f 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -92,6 +92,8 @@ class Ui_MainWindow(object): main_window.setObjectName('MainWindow') main_window.setWindowIcon(build_icon(':/icon/openlp-logo.svg')) main_window.setDockNestingEnabled(True) + if is_macosx(): + main_window.setDocumentMode(True) # Set up the main container, which contains all the other form widgets. self.main_content = QtGui.QWidget(main_window) self.main_content.setObjectName('main_content') From 6be3e0a816edc6d050279b98baddc2e3d7a30840 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 6 Sep 2014 20:33:01 +0100 Subject: [PATCH 55/66] Tried to fixed some DVD issues. Made the mouse cursor look busy. --- .../media/forms/mediaclipselectorform.py | 87 +++++++++++++------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 28d37f32e..ff0925f1e 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -28,31 +28,29 @@ ############################################################################### import os -if os.name == 'nt': - from win32com.client import Dispatch - import string -import sys - -if sys.platform.startswith('linux'): - import dbus import logging import re from time import sleep from datetime import datetime - from PyQt4 import QtCore, QtGui -from openlp.core.common import translate +from openlp.core.common import translate, Registry, is_win, is_linux, is_macosx from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box -from openlp.core.ui.media import format_milliseconds + +if is_win(): + from win32com.client import Dispatch + +if is_linux(): + import dbus + try: from openlp.core.ui.media.vendor import vlc except (ImportError, NameError, NotImplementedError): pass except OSError as e: - if sys.platform.startswith('win'): + if is_win(): if not isinstance(e, WindowsError) and e.winerror != 126: raise else: @@ -144,9 +142,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # You have to give the id of the QFrame (or similar object) # to vlc, different platforms have different functions for this. win_id = int(self.preview_frame.winId()) - if sys.platform == "win32": + if is_win(): self.vlc_media_player.set_hwnd(win_id) - elif sys.platform == "darwin": + elif is_macosx(): # We have to use 'set_nsobject' since Qt4 on OSX uses Cocoa # framework and not the old Carbon. self.vlc_media_player.set_nsobject(win_id) @@ -190,7 +188,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.audio_cd = True self.titles_combo_box.setDisabled(False) self.titles_combo_box.setCurrentIndex(0) - self.on_title_combo_box_currentIndexChanged(0) + self.on_titles_combo_box_currentIndexChanged(0) return True @@ -203,18 +201,21 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ log.debug('on_load_disc_button_clicked') self.disable_all() + self.application.set_busy_cursor() path = self.media_path_combobox.currentText() # Check if given path is non-empty and exists before starting VLC if not path: log.debug('no given path') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'No path was given')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() return if not os.path.exists(path): log.debug('Given path does not exists') critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'Given path does not exists')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() return # VLC behaves a bit differently on windows and linux when loading, which creates problems when trying to # detect if we're dealing with a DVD or CD, so we use different loading approaches depending on the OS. @@ -231,6 +232,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'An error happened during initialization of VLC player')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() return # put the media in the media player self.vlc_media_player.set_media(self.vlc_media) @@ -241,6 +243,8 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'VLC player failed playing the media')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) return self.vlc_media_player.audio_set_mute(True) if not self.media_state_wait(vlc.State.Playing): @@ -249,8 +253,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): critical_error_message_box(message=translate('MediaPlugin.MediaClipSelectorForm', 'VLC player failed playing the media')) self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) return - self.vlc_media_player.audio_set_mute(True) + # pause + self.vlc_media_player.set_time(0) + self.vlc_media_player.set_pause(1) + self.media_state_wait(vlc.State.Paused) + self.toggle_disable_load_media(False) + self.application.set_normal_cursor() + self.vlc_media_player.audio_set_mute(False) if not self.audio_cd: # Get titles, insert in combobox titles = self.vlc_media_player.video_get_title_description() @@ -260,12 +272,9 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # Main title is usually title #1 if len(titles) > 1: self.titles_combo_box.setCurrentIndex(1) - else: - self.titles_combo_box.setCurrentIndex(0) # Enable audio track combobox if anything is in it if len(titles) > 0: self.titles_combo_box.setDisabled(False) - self.toggle_disable_load_media(False) log.debug('load_disc_button end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) @QtCore.pyqtSlot(bool) @@ -378,6 +387,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if not self.vlc_media_player: log.error('vlc_media_player was None') return + self.application.set_busy_cursor() if self.audio_cd: self.vlc_media = self.audio_cd_tracks.item_at_index(index) self.vlc_media_player.set_media(self.vlc_media) @@ -385,14 +395,14 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.play() if not self.media_state_wait(vlc.State.Playing): log.error('Could not start playing audio cd, needed to get track info') + self.application.set_normal_cursor() return self.vlc_media_player.audio_set_mute(True) - # Sleep 1 second to make sure VLC has the needed metadata - sleep(1) # pause self.vlc_media_player.set_time(0) self.vlc_media_player.set_pause(1) self.vlc_media_player.audio_set_mute(False) + self.application.set_normal_cursor() self.toggle_disable_player(False) else: self.vlc_media_player.set_title(index) @@ -400,13 +410,20 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.play() if not self.media_state_wait(vlc.State.Playing): log.error('Could not start playing dvd, needed to get track info') + self.application.set_normal_cursor() return self.vlc_media_player.audio_set_mute(True) - # Sleep 1 second to make sure VLC has the needed metadata - sleep(1) - self.vlc_media_player.set_time(0) - # Get audio tracks, insert in combobox + # Get audio tracks audio_tracks = self.vlc_media_player.audio_get_track_description() + # Make sure we actually get the tracks, who has a DVD without audio? + loop_count = 0 + while len(audio_tracks) == 0 and loop_count < 100: + log.debug('In the audiotrack retry loop') + sleep(0.1) + audio_tracks = self.vlc_media_player.audio_get_track_description() + loop_count += 1 + log.debug('number of audio tracks: %d' % len(audio_tracks)) + # Clear the audio track combobox, insert new tracks self.audio_tracks_combobox.clear() for audio_track in audio_tracks: self.audio_tracks_combobox.addItem(audio_track[1].decode(), audio_track[0]) @@ -447,6 +464,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): self.vlc_media_player.set_pause(1) loop_count += 1 log.debug('titles_combo_box end - vlc_media_player state: %s' % self.vlc_media_player.get_state()) + self.application.set_normal_cursor() @QtCore.pyqtSlot(int) def on_audio_tracks_combobox_currentIndexChanged(self, index): @@ -595,7 +613,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): while media_state != self.vlc_media_player.get_state(): if self.vlc_media_player.get_state() == vlc.State.Error: return False - if (datetime.now() - start).seconds > 30: + if (datetime.now() - start).seconds > 15: return False return True @@ -606,7 +624,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): """ # Clear list first self.media_path_combobox.clear() - if os.name == 'nt': + if is_win(): # use win api to find optical drives fso = Dispatch('scripting.filesystemobject') for drive in fso.Drives: @@ -614,7 +632,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): # if type is 4, it is a cd-rom drive if drive.DriveType == 4: self.media_path_combobox.addItem('%s:\\' % drive.DriveLetter) - elif sys.platform.startswith('linux'): + elif is_linux(): # Get disc devices from dbus and find the ones that are optical bus = dbus.SystemBus() try: @@ -646,7 +664,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if chr(c) != '\x00': block_file += chr(c) self.media_path_combobox.addItem(block_file) - elif sys.platform.startswith('darwin'): + elif is_macosx(): # Look for DVD folders in devices to find optical devices volumes = os.listdir('/Volumes') candidates = list() @@ -663,3 +681,16 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if file.endswith('aiff'): self.media_path_combobox.addItem('/Volumes/' + volume) break + + @property + def application(self): + """ + Adds the openlp to the class dynamically. + Windows needs to access the application in a dynamic manner. + """ + if is_win(): + return Registry().get('application') + else: + if not hasattr(self, '_application'): + self._application = Registry().get('application') + return self._application From 3eeda06075ed7831199439d7933e48b33730e581 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Sat, 6 Sep 2014 21:10:44 +0100 Subject: [PATCH 56/66] Use the RegistryProperties class --- .../media/forms/mediaclipselectorform.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index ff0925f1e..8ab37f6ab 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -35,7 +35,7 @@ from datetime import datetime from PyQt4 import QtCore, QtGui -from openlp.core.common import translate, Registry, is_win, is_linux, is_macosx +from openlp.core.common import translate, is_win, is_linux, is_macosx, RegistryProperties from openlp.plugins.media.forms.mediaclipselectordialog import Ui_MediaClipSelector from openlp.core.lib.ui import critical_error_message_box @@ -59,7 +59,7 @@ except OSError as e: log = logging.getLogger(__name__) -class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): +class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryProperties): """ Class to manage the clip selection """ @@ -681,16 +681,3 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector): if file.endswith('aiff'): self.media_path_combobox.addItem('/Volumes/' + volume) break - - @property - def application(self): - """ - Adds the openlp to the class dynamically. - Windows needs to access the application in a dynamic manner. - """ - if is_win(): - return Registry().get('application') - else: - if not hasattr(self, '_application'): - self._application = Registry().get('application') - return self._application From bf488da5168b0d1a7a8367bc95327dcacda56500 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 8 Sep 2014 00:17:20 +0200 Subject: [PATCH 57/66] Added tests for with and without os x menu icon, renamed some tests to use our test name convention, fixed up and added more tests for the FormattingTagsForm. --- openlp/core/ui/formattingtagform.py | 6 ++ .../functional/openlp_core_lib/test_theme.py | 2 +- tests/functional/openlp_core_lib/test_ui.py | 65 +++++++++++++--- .../openlp_core_ui/test_firsttimeform.py | 4 +- .../test_formattingtagscontroller.py | 8 +- .../openlp_core_ui/test_formattingtagsform.py | 76 +++++++++++++------ 6 files changed, 119 insertions(+), 42 deletions(-) diff --git a/openlp/core/ui/formattingtagform.py b/openlp/core/ui/formattingtagform.py index 4f3d5d251..96e25c27d 100644 --- a/openlp/core/ui/formattingtagform.py +++ b/openlp/core/ui/formattingtagform.py @@ -60,6 +60,12 @@ class FormattingTagForm(QtGui.QDialog, Ui_FormattingTagDialog, FormattingTagCont """ super(FormattingTagForm, self).__init__(parent) self.setupUi(self) + self._setup() + + def _setup(self): + """ + Set up the class. This method is mocked out by the tests. + """ self.services = FormattingTagController() self.tag_table_widget.itemSelectionChanged.connect(self.on_row_selected) self.new_button.clicked.connect(self.on_new_clicked) diff --git a/tests/functional/openlp_core_lib/test_theme.py b/tests/functional/openlp_core_lib/test_theme.py index bcdced35f..7b09135dc 100644 --- a/tests/functional/openlp_core_lib/test_theme.py +++ b/tests/functional/openlp_core_lib/test_theme.py @@ -51,7 +51,7 @@ class TestTheme(TestCase): """ pass - def test_new_theme(self): + def new_theme_test(self): """ Test the theme creation - basic test """ diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py index 591762947..c17c4f8bf 100644 --- a/tests/functional/openlp_core_lib/test_ui.py +++ b/tests/functional/openlp_core_lib/test_ui.py @@ -29,10 +29,14 @@ """ Package to test the openlp.core.lib.ui package. """ -from PyQt4 import QtGui +from PyQt4 import QtCore, QtGui from unittest import TestCase -from openlp.core.lib.ui import * +from openlp.core.common import UiStrings, translate +from openlp.core.lib.ui import add_welcome_page, create_button_box, create_horizontal_adjusting_combo_box, \ + create_button, create_action, create_valign_selection_widgets, find_and_set_in_combo_box, create_widget_action, \ + set_case_insensitive_completer +from tests.functional import MagicMock, patch class TestUi(TestCase): @@ -40,7 +44,7 @@ class TestUi(TestCase): Test the functions in the ui module """ - def test_add_welcome_page(self): + def add_welcome_page_test(self): """ Test appending a welcome page to a wizard """ @@ -54,7 +58,7 @@ class TestUi(TestCase): self.assertEqual(1, len(wizard.pageIds()), 'The wizard should have one page.') self.assertIsInstance(wizard.page(0).pixmap(QtGui.QWizard.WatermarkPixmap), QtGui.QPixmap) - def test_create_button_box(self): + def create_button_box_test(self): """ Test creating a button box for a dialog """ @@ -82,7 +86,7 @@ class TestUi(TestCase): self.assertEqual(1, len(btnbox.buttons())) self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])) - def test_create_horizontal_adjusting_combo_box(self): + def create_horizontal_adjusting_combo_box_test(self): """ Test creating a horizontal adjusting combo box """ @@ -97,7 +101,7 @@ class TestUi(TestCase): self.assertEqual('combo1', combo.objectName()) self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy()) - def test_create_button(self): + def create_button_test(self): """ Test creating a button """ @@ -129,7 +133,7 @@ class TestUi(TestCase): self.assertEqual('my_btn', btn.objectName()) self.assertTrue(btn.isEnabled()) - def test_create_action(self): + def create_action_test(self): """ Test creating an action """ @@ -154,7 +158,44 @@ class TestUi(TestCase): self.assertEqual('my tooltip', action.toolTip()) self.assertEqual('my statustip', action.statusTip()) - def test_create_checked_enabled_visible_action(self): + def create_action_on_mac_osx_test(self): + """ + Test creating an action on OS X calls the correct method + """ + # GIVEN: A dialog and a mocked out is_macosx() method to always return True + with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \ + patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction: + mocked_is_macosx.return_value = True + mocked_action = MagicMock() + MockedQAction.return_value = mocked_action + dialog = QtGui.QDialog() + + # WHEN: An action is created + create_action(dialog, 'my_action') + + # THEN: setIconVisibleInMenu should be called + mocked_action.setIconVisibleInMenu.assert_called_with(False) + + def create_action_not_on_mac_osx_test(self): + """ + Test creating an action on something other than OS X doesn't call the method + """ + # GIVEN: A dialog and a mocked out is_macosx() method to always return True + with patch('openlp.core.lib.ui.is_macosx') as mocked_is_macosx, \ + patch('openlp.core.lib.ui.QtGui.QAction') as MockedQAction: + mocked_is_macosx.return_value = False + mocked_action = MagicMock() + MockedQAction.return_value = mocked_action + dialog = QtGui.QDialog() + + # WHEN: An action is created + create_action(dialog, 'my_action') + + # THEN: setIconVisibleInMenu should not be called + self.assertEqual(0, mocked_action.setIconVisibleInMenu.call_count, + 'setIconVisibleInMenu should not have been called') + + def create_checked_enabled_visible_action_test(self): """ Test creating an action with the 'checked', 'enabled' and 'visible' properties. """ @@ -169,7 +210,7 @@ class TestUi(TestCase): self.assertEqual(False, action.isEnabled()) self.assertEqual(False, action.isVisible()) - def test_create_valign_selection_widgets(self): + def create_valign_selection_widgets_test(self): """ Test creating a combo box for valign selection """ @@ -186,7 +227,7 @@ class TestUi(TestCase): for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]: self.assertTrue(combo.findText(text) >= 0) - def test_find_and_set_in_combo_box(self): + def find_and_set_in_combo_box_test(self): """ Test finding a string in a combo box and setting it as the selected item if present """ @@ -213,7 +254,7 @@ class TestUi(TestCase): # THEN: The index should have changed self.assertEqual(2, combo.currentIndex()) - def test_create_widget_action(self): + def create_widget_action_test(self): """ Test creating an action for a widget """ @@ -227,7 +268,7 @@ class TestUi(TestCase): self.assertIsInstance(action, QtGui.QAction) self.assertEqual(action.objectName(), 'some action') - def test_set_case_insensitive_completer(self): + def set_case_insensitive_completer_test(self): """ Test setting a case insensitive completer on a widget """ diff --git a/tests/functional/openlp_core_ui/test_firsttimeform.py b/tests/functional/openlp_core_ui/test_firsttimeform.py index 2e26c286a..35bd1675d 100644 --- a/tests/functional/openlp_core_ui/test_firsttimeform.py +++ b/tests/functional/openlp_core_ui/test_firsttimeform.py @@ -47,7 +47,7 @@ class TestFirstTimeForm(TestCase, TestMixin): Registry().register('application', self.app) self.first_time_form = FirstTimeForm(screens) - def test_access_to_config(self): + def access_to_config_test(self): """ Test if we can access the First Time Form's config file """ @@ -59,7 +59,7 @@ class TestFirstTimeForm(TestCase, TestMixin): self.assertTrue(self.first_time_form.web_access, 'First Time Wizard\'s web configuration file should be available') - def test_parsable_config(self): + def parsable_config_test(self): """ Test if the First Time Form's config file is parsable """ diff --git a/tests/functional/openlp_core_ui/test_formattingtagscontroller.py b/tests/functional/openlp_core_ui/test_formattingtagscontroller.py index 1d8512940..38cae0bf4 100644 --- a/tests/functional/openlp_core_ui/test_formattingtagscontroller.py +++ b/tests/functional/openlp_core_ui/test_formattingtagscontroller.py @@ -39,7 +39,7 @@ class TestFormattingTagController(TestCase): def setUp(self): self.services = FormattingTagController() - def test_strip(self): + def strip_test(self): """ Test that the _strip strips the correct chars """ @@ -52,7 +52,7 @@ class TestFormattingTagController(TestCase): # THEN: The tag should be returned with the wrappers removed. self.assertEqual(result, 'tag', 'FormattingTagForm._strip should return u\'tag\' when called with u\'{tag}\'') - def test_end_tag_changed_processes_correctly(self): + def end_tag_changed_processes_correctly_test(self): """ Test that the end html tags are generated correctly """ @@ -77,7 +77,7 @@ class TestFormattingTagController(TestCase): self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % error) - def test_start_tag_changed_processes_correctly(self): + def start_tag_changed_processes_correctly_test(self): """ Test that the end html tags are generated correctly """ @@ -100,7 +100,7 @@ class TestFormattingTagController(TestCase): self.assertTrue(error == test['valid'], 'Function should not generate unexpected error messages : %s ' % error) - def test_start_html_to_end_html(self): + def start_html_to_end_html_test(self): """ Test that the end html tags are generated correctly """ diff --git a/tests/functional/openlp_core_ui/test_formattingtagsform.py b/tests/functional/openlp_core_ui/test_formattingtagsform.py index e71a75651..736a306c3 100644 --- a/tests/functional/openlp_core_ui/test_formattingtagsform.py +++ b/tests/functional/openlp_core_ui/test_formattingtagsform.py @@ -29,17 +29,17 @@ """ Package to test the openlp.core.ui.formattingtagsform package. """ +from PyQt4 import QtGui from unittest import TestCase +from openlp.core.common import translate -from tests.functional import MagicMock, patch +from tests.functional import MagicMock, patch, call from openlp.core.ui.formattingtagform import FormattingTagForm # TODO: Tests Still TODO # __init__ # exec_ -# on_new_clicked -# on_delete_clicked # on_saved_clicked # _reloadTable @@ -47,30 +47,60 @@ from openlp.core.ui.formattingtagform import FormattingTagForm class TestFormattingTagForm(TestCase): def setUp(self): - self.init_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm.__init__') - self.qdialog_patcher = patch('openlp.core.ui.formattingtagform.QtGui.QDialog') - self.ui_formatting_tag_dialog_patcher = patch('openlp.core.ui.formattingtagform.Ui_FormattingTagDialog') - self.mocked_init = self.init_patcher.start() - self.mocked_qdialog = self.qdialog_patcher.start() - self.mocked_ui_formatting_tag_dialog = self.ui_formatting_tag_dialog_patcher.start() - self.mocked_init.return_value = None + """ + Mock out stuff for all the tests + """ + self.setup_patcher = patch('openlp.core.ui.formattingtagform.FormattingTagForm._setup') + self.setup_patcher.start() def tearDown(self): - self.init_patcher.stop() - self.qdialog_patcher.stop() - self.ui_formatting_tag_dialog_patcher.stop() - - def test_on_text_edited(self): """ - Test that the appropriate actions are preformed when on_text_edited is called + Remove the mocks + """ + self.setup_patcher.stop() + + def on_row_selected_test(self): + """ + Test that the appropriate actions are preformed when on_row_selected is called + """ + # GIVEN: An instance of the Formatting Tag Form and a mocked delete_button + form = FormattingTagForm(None) + form.delete_button = MagicMock() + + # WHEN: on_row_selected is called + form.on_row_selected() + + # THEN: setEnabled and should have been called on delete_button + form.delete_button.setEnabled.assert_called_with(True) + + def on_new_clicked_test(self): + """ + Test that clicking the Add a new tag button does the right thing """ - # GIVEN: An instance of the Formatting Tag Form and a mocked save_push_button - form = FormattingTagForm() - form.save_button = MagicMock() + # GIVEN: A formatting tag form and a mocked out tag table widget + form = FormattingTagForm(None) + form.tag_table_widget = MagicMock() + row_count = 5 + form.tag_table_widget.rowCount.return_value = row_count - # WHEN: on_text_edited is called with an arbitrary value - # form.on_text_edited('text') + # WHEN: on_new_clicked is run (i.e. the Add new button was clicked) + with patch('openlp.core.ui.formattingtagform.QtGui.QTableWidgetItem') as MockedQTableWidgetItem: + mocked_table_widget = MagicMock() + MockedQTableWidgetItem.return_value = mocked_table_widget + form.on_new_clicked() - # THEN: setEnabled and setDefault should have been called on save_push_button - # form.save_button.setEnabled.assert_called_with(True) + # THEN: A new row should be added to the table + form.tag_table_widget.rowCount.assert_called_with() + form.tag_table_widget.insertRow.assert_called_with(row_count) + expected_set_item_calls = [ + call(row_count, 0, mocked_table_widget), + call(row_count, 1, mocked_table_widget), + call(row_count, 2, mocked_table_widget), + call(row_count, 3, mocked_table_widget) + ] + self.assertEqual(expected_set_item_calls, form.tag_table_widget.setItem.call_args_list, + 'setItem should have been called correctly') + form.tag_table_widget.resizeRowsToContents.assert_called_with() + form.tag_table_widget.scrollToBottom.assert_called_with() + form.tag_table_widget.selectRow.assert_called_with(row_count) From 40edebdab0b5ef72d1f856dd72657be2114e9102 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 8 Sep 2014 00:30:21 +0200 Subject: [PATCH 58/66] Style fixes --- openlp/core/ui/themewizard.py | 2 +- openlp/core/ui/wizard.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openlp/core/ui/themewizard.py b/openlp/core/ui/themewizard.py index c9c6f7e35..50200313f 100644 --- a/openlp/core/ui/themewizard.py +++ b/openlp/core/ui/themewizard.py @@ -49,7 +49,7 @@ class Ui_ThemeWizard(object): theme_wizard.setWindowIcon(build_icon(u':/icon/openlp-logo.svg')) theme_wizard.setModal(True) theme_wizard.setOptions(QtGui.QWizard.IndependentPages | - QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) + QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.HaveCustomButton1) if is_macosx(): theme_wizard.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) theme_wizard.resize(646, 400) diff --git a/openlp/core/ui/wizard.py b/openlp/core/ui/wizard.py index c5a969f9e..7199d1742 100644 --- a/openlp/core/ui/wizard.py +++ b/openlp/core/ui/wizard.py @@ -125,7 +125,6 @@ class OpenLPWizard(QtGui.QWizard, RegistryProperties): QtGui.QWizard.NoBackButtonOnStartPage | QtGui.QWizard.NoBackButtonOnLastPage) if is_macosx(): self.setPixmap(QtGui.QWizard.BackgroundPixmap, QtGui.QPixmap(':/wizards/openlp-osx-wizard.png')) - #self.resize(634, 386) add_welcome_page(self, image) self.add_custom_pages() if self.with_progress_page: From 58f517b4d62212815cd56b53e279ee10d7c0fd5c Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 8 Sep 2014 20:16:31 +0100 Subject: [PATCH 59/66] More DVD fixes --- openlp/plugins/media/forms/mediaclipselectorform.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 8ab37f6ab..5bbb4aa6a 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -264,11 +264,15 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryPropert self.application.set_normal_cursor() self.vlc_media_player.audio_set_mute(False) if not self.audio_cd: + # Temporarily disable signals + self.blockSignals(True) # Get titles, insert in combobox titles = self.vlc_media_player.video_get_title_description() self.titles_combo_box.clear() for title in titles: self.titles_combo_box.addItem(title[1].decode(), title[0]) + # Re-enable signals + self.blockSignals(False) # Main title is usually title #1 if len(titles) > 1: self.titles_combo_box.setCurrentIndex(1) @@ -415,13 +419,6 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryPropert self.vlc_media_player.audio_set_mute(True) # Get audio tracks audio_tracks = self.vlc_media_player.audio_get_track_description() - # Make sure we actually get the tracks, who has a DVD without audio? - loop_count = 0 - while len(audio_tracks) == 0 and loop_count < 100: - log.debug('In the audiotrack retry loop') - sleep(0.1) - audio_tracks = self.vlc_media_player.audio_get_track_description() - loop_count += 1 log.debug('number of audio tracks: %d' % len(audio_tracks)) # Clear the audio track combobox, insert new tracks self.audio_tracks_combobox.clear() From 08bacf69b2000b1fee9e7ee4df1ef4a0b524a7cd Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Mon, 8 Sep 2014 22:43:21 +0200 Subject: [PATCH 60/66] Revert accidental commit of my local changes --- scripts/generate_resources.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate_resources.sh b/scripts/generate_resources.sh index 76ace1bb6..eb2cd7c46 100755 --- a/scripts/generate_resources.sh +++ b/scripts/generate_resources.sh @@ -44,7 +44,7 @@ mv openlp/core/resources.py openlp/core/resources.py.old # Create the new data from the updated qrc -pyrcc4-3.3 -py3 -o openlp/core/resources.py.new resources/images/openlp-2.qrc +pyrcc4 -py3 -o openlp/core/resources.py.new resources/images/openlp-2.qrc # Remove patch breaking lines cat openlp/core/resources.py.new | sed '/# Created: /d;/# by: /d' > openlp/core/resources.py From 79ef9315d568dab92b75e1e65205b760320c2ae1 Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 8 Sep 2014 21:49:33 +0100 Subject: [PATCH 61/66] Added a test --- .../media/forms/mediaclipselectorform.py | 15 +++++++++- .../openlp_plugins/media/__init__.py | 28 +++++++++++++++++++ .../openlp_plugins/media/forms/__init__.py | 28 +++++++++++++++++++ .../media/forms/test_mediaclipselectorform.py | 21 ++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 tests/interfaces/openlp_plugins/media/forms/__init__.py diff --git a/openlp/plugins/media/forms/mediaclipselectorform.py b/openlp/plugins/media/forms/mediaclipselectorform.py index 5bbb4aa6a..d63e8a8bb 100644 --- a/openlp/plugins/media/forms/mediaclipselectorform.py +++ b/openlp/plugins/media/forms/mediaclipselectorform.py @@ -550,7 +550,7 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryPropert """ Saves the current media and trackinfo as a clip to the mediamanager """ - log.debug('in on_save_button_clicked') + log.debug('in MediaClipSelectorForm.accept') start_time = self.start_position_edit.time() start_time_ms = start_time.hour() * 60 * 60 * 1000 + \ start_time.minute() * 60 * 1000 + \ @@ -565,10 +565,23 @@ class MediaClipSelectorForm(QtGui.QDialog, Ui_MediaClipSelector, RegistryPropert path = self.media_path_combobox.currentText() optical = '' if self.audio_cd: + # Check for load problems + if start_time_ms is None or end_time_ms is None or title is None: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'CD not loaded correctly'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The CD was not loaded correctly, please re-load and try again.')) + return optical = 'optical:%d:-1:-1:%d:%d:' % (title, start_time_ms, end_time_ms) else: audio_track = self.audio_tracks_combobox.itemData(self.audio_tracks_combobox.currentIndex()) subtitle_track = self.subtitle_tracks_combobox.itemData(self.subtitle_tracks_combobox.currentIndex()) + # Check for load problems + if start_time_ms is None or end_time_ms is None or title is None or audio_track is None\ + or subtitle_track is None: + critical_error_message_box(translate('MediaPlugin.MediaClipSelectorForm', 'DVD not loaded correctly'), + translate('MediaPlugin.MediaClipSelectorForm', + 'The DVD was not loaded correctly, please re-load and try again.')) + return optical = 'optical:%d:%d:%d:%d:%d:' % (title, audio_track, subtitle_track, start_time_ms, end_time_ms) # Ask for an alternative name for the mediaclip while True: diff --git a/tests/interfaces/openlp_plugins/media/__init__.py b/tests/interfaces/openlp_plugins/media/__init__.py index e69de29bb..6b241e7fc 100644 --- a/tests/interfaces/openlp_plugins/media/__init__.py +++ b/tests/interfaces/openlp_plugins/media/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 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 # +############################################################################### diff --git a/tests/interfaces/openlp_plugins/media/forms/__init__.py b/tests/interfaces/openlp_plugins/media/forms/__init__.py new file mode 100644 index 000000000..6b241e7fc --- /dev/null +++ b/tests/interfaces/openlp_plugins/media/forms/__init__.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2014 Raoul Snyman # +# Portions copyright (c) 2008-2014 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 # +############################################################################### diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index e97a06238..2346bba9c 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -60,6 +60,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): # Mock VLC so we don't actually use it self.vlc_patcher = patch('openlp.plugins.media.forms.mediaclipselectorform.vlc') self.vlc_patcher.start() + Registry().register('application', self.app) # Mock the media item self.mock_media_item = MagicMock() # create form to test @@ -67,6 +68,8 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): mock_media_state_wait = MagicMock() mock_media_state_wait.return_value = True self.form.media_state_wait = mock_media_state_wait + self.form.application.set_busy_cursor = MagicMock() + self.form.application.set_normal_cursor = MagicMock() def tearDown(self): """ @@ -155,3 +158,21 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.audio_tracks_combobox.itemData.assert_any_call(0) self.form.audio_tracks_combobox.itemData.assert_any_call(1) self.form.subtitle_tracks_combobox.itemData.assert_any_call(0) + + def click_save_button_test(self): + """ + Test that the correct function is called when save is clicked, and that it behaves as expected. + """ + # GIVEN: Mocked methods. + with patch('openlp.plugins.media.forms.mediaclipselectorform.critical_error_message_box') as \ + mocked_critical_error_message_box,\ + patch('PyQt4.QtGui.QDialog.exec_') as mocked_exec: + self.form.exec_() + + # WHEN: The save button is clicked with a NoneType in start_time_ms or end_time_ms + self.form.accept() + + # THEN: we should get an error message + mocked_critical_error_message_box.assert_called_with('DVD not loaded correctly', + 'The DVD was not loaded correctly, ' + 'please re-load and try again.') From 98c41ae5860936669ae6dd0b477236c4d68b4efe Mon Sep 17 00:00:00 2001 From: Tomas Groth Date: Mon, 8 Sep 2014 21:58:42 +0100 Subject: [PATCH 62/66] Mock device detection during test. --- .../openlp_plugins/media/forms/test_mediaclipselectorform.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py index 2346bba9c..89810aa50 100644 --- a/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py +++ b/tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py @@ -70,6 +70,7 @@ class TestMediaClipSelectorForm(TestCase, TestMixin): self.form.media_state_wait = mock_media_state_wait self.form.application.set_busy_cursor = MagicMock() self.form.application.set_normal_cursor = MagicMock() + self.form.find_optical_devices = MagicMock() def tearDown(self): """ From 3426fc37d2cac97104a08b97cf1777f70a822bc8 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sun, 14 Sep 2014 00:00:28 +0200 Subject: [PATCH 63/66] Fix author->songs relation Fixes: https://launchpad.net/bugs/1369139 --- openlp/plugins/songs/lib/db.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index a9206a397..895dba4b5 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -31,8 +31,6 @@ The :mod:`db` module provides the database and schema that is the backend for the Songs plugin """ -import re - from sqlalchemy import Column, ForeignKey, Table, types from sqlalchemy.orm import mapper, relation, reconstructor from sqlalchemy.sql.expression import func, text @@ -329,7 +327,9 @@ def init_schema(url): Column('topic_id', types.Integer(), ForeignKey('topics.id'), primary_key=True) ) - mapper(Author, authors_table) + mapper(Author, authors_table, properties={ + 'songs': relation(Song, secondary=authors_songs_table, viewonly=True) + }) mapper(AuthorSong, authors_songs_table, properties={ 'author': relation(Author) }) From aef7bb55c4f2d3c90ab32a08d36d983c88d17a97 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sun, 14 Sep 2014 00:00:43 +0200 Subject: [PATCH 64/66] Add test --- tests/functional/openlp_core_lib/test_ui.py | 28 +++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py index 591762947..6fe9a3938 100644 --- a/tests/functional/openlp_core_lib/test_ui.py +++ b/tests/functional/openlp_core_lib/test_ui.py @@ -154,6 +154,34 @@ class TestUi(TestCase): self.assertEqual('my tooltip', action.toolTip()) self.assertEqual('my statustip', action.statusTip()) + def test_create_action_2(self): + """ + Test creating an action + """ + # GIVEN: A dialog + dialog = QtGui.QDialog() + + # WHEN: We create an action with some properties + action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) + + # THEN: These properties should be set + self.assertTrue(action.isChecked()) + self.assertFalse(action.isEnabled()) + self.assertFalse(action.isVisible()) + + def test_create_action_separator(self): + """ + Test creating an action as separator + """ + # GIVEN: A dialog + dialog = QtGui.QDialog() + + # WHEN: We create an action as a separator + action = create_action(dialog, 'my_action', separator=True) + + # THEN: The action should be a separator + self.assertTrue(action.isSeparator()) + def test_create_checked_enabled_visible_action(self): """ Test creating an action with the 'checked', 'enabled' and 'visible' properties. From e6a6015c21e01aaeef8ba73f464e98e4d5590304 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sun, 14 Sep 2014 15:32:38 +0200 Subject: [PATCH 65/66] Fix tests --- tests/functional/openlp_core_lib/test_ui.py | 47 +++++++-------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/tests/functional/openlp_core_lib/test_ui.py b/tests/functional/openlp_core_lib/test_ui.py index 6fe9a3938..02f9ce4fa 100644 --- a/tests/functional/openlp_core_lib/test_ui.py +++ b/tests/functional/openlp_core_lib/test_ui.py @@ -40,7 +40,7 @@ class TestUi(TestCase): Test the functions in the ui module """ - def test_add_welcome_page(self): + def add_welcome_page_test(self): """ Test appending a welcome page to a wizard """ @@ -54,7 +54,7 @@ class TestUi(TestCase): self.assertEqual(1, len(wizard.pageIds()), 'The wizard should have one page.') self.assertIsInstance(wizard.page(0).pixmap(QtGui.QWizard.WatermarkPixmap), QtGui.QPixmap) - def test_create_button_box(self): + def create_button_box_test(self): """ Test creating a button box for a dialog """ @@ -82,7 +82,7 @@ class TestUi(TestCase): self.assertEqual(1, len(btnbox.buttons())) self.assertEqual(QtGui.QDialogButtonBox.HelpRole, btnbox.buttonRole(btnbox.buttons()[0])) - def test_create_horizontal_adjusting_combo_box(self): + def create_horizontal_adjusting_combo_box_test(self): """ Test creating a horizontal adjusting combo box """ @@ -97,7 +97,7 @@ class TestUi(TestCase): self.assertEqual('combo1', combo.objectName()) self.assertEqual(QtGui.QComboBox.AdjustToMinimumContentsLength, combo.sizeAdjustPolicy()) - def test_create_button(self): + def create_button_test(self): """ Test creating a button """ @@ -129,7 +129,7 @@ class TestUi(TestCase): self.assertEqual('my_btn', btn.objectName()) self.assertTrue(btn.isEnabled()) - def test_create_action(self): + def create_action_test(self): """ Test creating an action """ @@ -154,9 +154,9 @@ class TestUi(TestCase): self.assertEqual('my tooltip', action.toolTip()) self.assertEqual('my statustip', action.statusTip()) - def test_create_action_2(self): + def create_checked_disabled_invisible_action_test(self): """ - Test creating an action + Test that an invisible, disabled, checked action is created correctly """ # GIVEN: A dialog dialog = QtGui.QDialog() @@ -165,11 +165,11 @@ class TestUi(TestCase): action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) # THEN: These properties should be set - self.assertTrue(action.isChecked()) - self.assertFalse(action.isEnabled()) - self.assertFalse(action.isVisible()) + self.assertTrue(action.isChecked(), 'The action should be checked') + self.assertFalse(action.isEnabled(), 'The action should be disabled') + self.assertFalse(action.isVisible(), 'The action should be invisble') - def test_create_action_separator(self): + def create_action_separator_test(self): """ Test creating an action as separator """ @@ -180,24 +180,9 @@ class TestUi(TestCase): action = create_action(dialog, 'my_action', separator=True) # THEN: The action should be a separator - self.assertTrue(action.isSeparator()) + self.assertTrue(action.isSeparator(), 'The action should be a separator') - def test_create_checked_enabled_visible_action(self): - """ - Test creating an action with the 'checked', 'enabled' and 'visible' properties. - """ - # GIVEN: A dialog - dialog = QtGui.QDialog() - - # WHEN: We create an action with some properties - action = create_action(dialog, 'my_action', checked=True, enabled=False, visible=False) - - # THEN: These properties should be set - self.assertEqual(True, action.isChecked()) - self.assertEqual(False, action.isEnabled()) - self.assertEqual(False, action.isVisible()) - - def test_create_valign_selection_widgets(self): + def create_valign_selection_widgets_test(self): """ Test creating a combo box for valign selection """ @@ -214,7 +199,7 @@ class TestUi(TestCase): for text in [UiStrings().Top, UiStrings().Middle, UiStrings().Bottom]: self.assertTrue(combo.findText(text) >= 0) - def test_find_and_set_in_combo_box(self): + def find_and_set_in_combo_box_test(self): """ Test finding a string in a combo box and setting it as the selected item if present """ @@ -241,7 +226,7 @@ class TestUi(TestCase): # THEN: The index should have changed self.assertEqual(2, combo.currentIndex()) - def test_create_widget_action(self): + def create_widget_action_test(self): """ Test creating an action for a widget """ @@ -255,7 +240,7 @@ class TestUi(TestCase): self.assertIsInstance(action, QtGui.QAction) self.assertEqual(action.objectName(), 'some action') - def test_set_case_insensitive_completer(self): + def set_case_insensitive_completer_test(self): """ Test setting a case insensitive completer on a widget """ From 48abd1793baeaed79f9bfe224fb515f7f8bbfe53 Mon Sep 17 00:00:00 2001 From: Samuel Mehrbrodt Date: Sun, 14 Sep 2014 15:35:55 +0200 Subject: [PATCH 66/66] Fix loading authors Fixes: https://launchpad.net/bugs/1366198 --- openlp/plugins/songs/lib/db.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openlp/plugins/songs/lib/db.py b/openlp/plugins/songs/lib/db.py index 895dba4b5..3afba1b66 100644 --- a/openlp/plugins/songs/lib/db.py +++ b/openlp/plugins/songs/lib/db.py @@ -339,7 +339,8 @@ def init_schema(url): # Use the authors_songs relation when you need access to the 'author_type' attribute # or when creating new relations 'authors_songs': relation(AuthorSong, cascade="all, delete-orphan"), - 'authors': relation(Author, secondary=authors_songs_table, viewonly=True), + # Use lazy='joined' to always load authors when the song is fetched from the database (bug 1366198) + 'authors': relation(Author, secondary=authors_songs_table, viewonly=True, lazy='joined'), 'book': relation(Book, backref='songs'), 'media_files': relation(MediaFile, backref='songs', order_by=media_files_table.c.weight), 'topics': relation(Topic, backref='songs', secondary=songs_topics_table)