diff --git a/openlp/core/lib/__init__.py b/openlp/core/lib/__init__.py index a7cba33a0..b85070445 100644 --- a/openlp/core/lib/__init__.py +++ b/openlp/core/lib/__init__.py @@ -145,11 +145,13 @@ def build_icon(icon): return button_icon -def image_to_byte(image): +def image_to_byte(image, base_64=True): """ Resize an image to fit on the current screen for the web and returns it as a byte stream. :param image: The image to converted. + :param base_64: If True returns the image as Base64 bytes, otherwise the image is returned as a byte array. + To preserve original intention, this defaults to True """ log.debug('image_to_byte - start') byte_array = QtCore.QByteArray() @@ -158,6 +160,8 @@ def image_to_byte(image): buffie.open(QtCore.QIODevice.WriteOnly) image.save(buffie, "PNG") log.debug('image_to_byte - end') + if not base_64: + return byte_array # convert to base64 encoding so does not get missed! return bytes(byte_array.toBase64()).decode('utf-8') diff --git a/openlp/core/lib/imagemanager.py b/openlp/core/lib/imagemanager.py index cba393815..46fab10d2 100644 --- a/openlp/core/lib/imagemanager.py +++ b/openlp/core/lib/imagemanager.py @@ -106,7 +106,7 @@ class Image(object): """ secondary_priority = 0 - def __init__(self, path, source, background): + def __init__(self, path, source, background, width=-1, height=-1): """ Create an image for the :class:`ImageManager`'s cache. @@ -115,7 +115,8 @@ class Image(object): :class:`~openlp.core.lib.ImageSource` class. :param background: A ``QtGui.QColor`` object specifying the colour to be used to fill the gabs if the image's ratio does not match with the display ratio. - + :param width: The width of the image, defaults to -1 meaning that the screen width will be used. + :param height: The height of the image, defaults to -1 meaning that the screen height will be used. """ self.path = path self.image = None @@ -124,6 +125,8 @@ class Image(object): self.source = source self.background = background self.timestamp = 0 + self.width = width + self.height = height # FIXME: We assume that the path exist. The caller has to take care that it exists! if os.path.exists(path): self.timestamp = os.stat(path).st_mtime @@ -210,13 +213,13 @@ class ImageManager(QtCore.QObject): image.background = background self._reset_image(image) - def update_image_border(self, path, source, background): + def update_image_border(self, path, source, background, width=-1, height=-1): """ Border has changed so update the image affected. """ log.debug('update_image_border') # Mark the image as dirty for a rebuild by setting the image and byte stream to None. - image = self._cache[(path, source)] + image = self._cache[(path, source, width, height)] if image.source == source: image.background = background self._reset_image(image) @@ -237,12 +240,12 @@ class ImageManager(QtCore.QObject): if not self.image_thread.isRunning(): self.image_thread.start() - def get_image(self, path, source): + def get_image(self, path, source, width=-1, height=-1): """ Return the ``QImage`` from the cache. If not present wait for the background thread to process it. """ log.debug('getImage %s' % path) - image = self._cache[(path, source)] + image = self._cache[(path, source, width, height)] if image.image is None: self._conversion_queue.modify_priority(image, Priority.High) # make sure we are running and if not give it a kick @@ -257,12 +260,12 @@ class ImageManager(QtCore.QObject): self._conversion_queue.modify_priority(image, Priority.Low) return image.image - def get_image_bytes(self, path, source): + def get_image_bytes(self, path, source, width=-1, height=-1): """ Returns the byte string for an image. If not present wait for the background thread to process it. """ log.debug('get_image_bytes %s' % path) - image = self._cache[(path, source)] + image = self._cache[(path, source, width, height)] if image.image_bytes is None: self._conversion_queue.modify_priority(image, Priority.Urgent) # make sure we are running and if not give it a kick @@ -272,14 +275,14 @@ class ImageManager(QtCore.QObject): time.sleep(0.1) return image.image_bytes - def add_image(self, path, source, background): + def add_image(self, path, source, background, width=-1, height=-1): """ Add image to cache if it is not already there. """ log.debug('add_image %s' % path) - if not (path, source) in self._cache: - image = Image(path, source, background) - self._cache[(path, source)] = image + if not (path, source, width, height) in self._cache: + image = Image(path, source, background, width, height) + self._cache[(path, source, width, height)] = image self._conversion_queue.put((image.priority, image.secondary_priority, image)) # Check if the there are any images with the same path and check if the timestamp has changed. for image in list(self._cache.values()): @@ -308,7 +311,10 @@ class ImageManager(QtCore.QObject): image = self._conversion_queue.get()[2] # Generate the QImage for the image. if image.image is None: - image.image = resize_image(image.path, self.width, self.height, image.background) + # Let's see if the image was requested with specific dimensions + width = self.width if image.width == -1 else image.width + height = self.height if image.height == -1 else image.height + image.image = resize_image(image.path, width, height, image.background) # Set the priority to Lowest and stop here as we need to process more important images first. if image.priority == Priority.Normal: self._conversion_queue.modify_priority(image, Priority.Lowest) diff --git a/openlp/core/lib/serviceitem.py b/openlp/core/lib/serviceitem.py index ecd6ca5bd..4e7ff032a 100644 --- a/openlp/core/lib/serviceitem.py +++ b/openlp/core/lib/serviceitem.py @@ -39,8 +39,8 @@ import uuid from PyQt4 import QtGui -from openlp.core.common import RegistryProperties, Settings, translate -from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags +from openlp.core.common import RegistryProperties, Settings, translate, AppLocation +from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb log = logging.getLogger(__name__) @@ -112,7 +112,17 @@ class ItemCapabilities(object): The capability to edit the title of the item ``IsOptical`` - .Determines is the service_item is based on an optical device + Determines is the service_item is based on an optical device + + ``HasDisplayTitle`` + The item contains 'displaytitle' on every frame which should be + preferred over 'title' when displaying the item + + ``HasNotes`` + The item contains 'notes' + + ``HasThumbnails`` + The item has related thumbnails available """ CanPreview = 1 @@ -133,6 +143,9 @@ class ItemCapabilities(object): CanAutoStartForLive = 16 CanEditTitle = 17 IsOptical = 18 + HasDisplayTitle = 19 + HasNotes = 20 + HasThumbnails = 21 class ServiceItem(RegistryProperties): @@ -272,18 +285,22 @@ class ServiceItem(RegistryProperties): self.raw_footer = [] self.foot_text = '
'.join([_f for _f in self.raw_footer if _f]) - def add_from_image(self, path, title, background=None): + def add_from_image(self, path, title, background=None, thumbnail=None): """ Add an image slide to the service item. :param path: The directory in which the image file is located. :param title: A title for the slide in the service item. :param background: + :param thumbnail: Optional alternative thumbnail, used for remote thumbnails. """ if background: self.image_border = background self.service_item_type = ServiceItemType.Image - self._raw_frames.append({'title': title, 'path': path}) + if not thumbnail: + self._raw_frames.append({'title': title, 'path': path}) + else: + self._raw_frames.append({'title': title, 'path': path, 'image': thumbnail}) self.image_manager.add_image(path, ImageSource.ImagePlugin, self.image_border) self._new_item() @@ -301,16 +318,22 @@ class ServiceItem(RegistryProperties): self._raw_frames.append({'title': title, 'raw_slide': raw_slide, 'verseTag': verse_tag}) self._new_item() - def add_from_command(self, path, file_name, image): + def add_from_command(self, path, file_name, image, display_title=None, notes=None): """ Add a slide from a command. :param path: The title of the slide in the service item. :param file_name: The title of the slide in the service item. :param image: The command of/for the slide. + :param display_title: Title to show in gui/webinterface, optional. + :param notes: Notes to show in the webinteface, optional. """ self.service_item_type = ServiceItemType.Command - self._raw_frames.append({'title': file_name, 'image': image, 'path': path}) + # If the item should have a display title but this frame doesn't have one, we make one up + if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title: + display_title = translate('OpenLP.ServiceItem', '[slide %d]') % (len(self._raw_frames) + 1) + self._raw_frames.append({'title': file_name, 'image': image, 'path': path, + 'display_title': display_title, 'notes': notes}) self._new_item() def get_service_repr(self, lite_save): @@ -354,7 +377,8 @@ class ServiceItem(RegistryProperties): service_data = [slide['title'] for slide in self._raw_frames] elif self.service_item_type == ServiceItemType.Command: for slide in self._raw_frames: - service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path']}) + service_data.append({'title': slide['title'], 'image': slide['image'], 'path': slide['path'], + 'display_title': slide['display_title'], 'notes': slide['notes']}) return {'header': service_header, 'data': service_data} def set_from_service(self, service_item, path=None): @@ -425,7 +449,8 @@ class ServiceItem(RegistryProperties): 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']) + self.add_from_command(path, text_image['title'], text_image['image'], + text_image.get('display_title', ''), text_image.get('notes', '')) else: self.add_from_command(text_image['path'], text_image['title'], text_image['image']) self._new_item() diff --git a/openlp/core/ui/listpreviewwidget.py b/openlp/core/ui/listpreviewwidget.py index 831eb182b..3909c6a31 100644 --- a/openlp/core/ui/listpreviewwidget.py +++ b/openlp/core/ui/listpreviewwidget.py @@ -94,8 +94,8 @@ class ListPreviewWidget(QtGui.QTableWidget, RegistryProperties): Displays the given slide. """ self.service_item = service_item - self.clear() self.setRowCount(0) + self.clear() self.setColumnWidth(0, width) row = 0 text = [] diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 369137694..d0ac1560e 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -1281,7 +1281,11 @@ class ServiceManager(OpenLPMixin, RegistryMixin, QtGui.QWidget, Ui_ServiceManage # Add the children to their parent tree_widget_item. for count, frame in enumerate(service_item_from_item.get_frames()): child = QtGui.QTreeWidgetItem(tree_widget_item) - text = frame['title'].replace('\n', ' ') + # prefer to use a display_title + if service_item_from_item.is_capable(ItemCapabilities.HasDisplayTitle): + text = frame['display_title'].replace('\n', ' ') + else: + text = frame['title'].replace('\n', ' ') child.setText(0, text[:40]) child.setData(0, QtCore.Qt.UserRole, count) if service_item == item_count: diff --git a/openlp/core/ui/slidecontroller.py b/openlp/core/ui/slidecontroller.py index 44c28deb6..b12438679 100644 --- a/openlp/core/ui/slidecontroller.py +++ b/openlp/core/ui/slidecontroller.py @@ -873,6 +873,7 @@ class SlideController(DisplayController, RegistryProperties): if self.service_item.is_command(): Registry().execute('%s_slide' % self.service_item.name.lower(), [self.service_item, self.is_live, index]) self.update_preview() + self.selected_row = index else: self.preview_widget.change_slide(index) self.slide_selected() @@ -1042,8 +1043,8 @@ class SlideController(DisplayController, RegistryProperties): self.display.image(to_display) # reset the store used to display first image self.service_item.bg_image_bytes = None - self.update_preview() self.selected_row = row + self.update_preview() self.preview_widget.change_slide(row) self.display.setFocus() @@ -1055,6 +1056,7 @@ class SlideController(DisplayController, RegistryProperties): """ self.preview_widget.change_slide(row) self.update_preview() + self.selected_row = row def update_preview(self): """ diff --git a/openlp/plugins/bibles/lib/manager.py b/openlp/plugins/bibles/lib/manager.py index 7dab8b3d7..436a7e34b 100644 --- a/openlp/plugins/bibles/lib/manager.py +++ b/openlp/plugins/bibles/lib/manager.py @@ -85,7 +85,7 @@ class BibleFormat(object): BibleFormat.CSV, BibleFormat.OpenSong, BibleFormat.WebDownload, - BibleFormar.Zefania, + BibleFormat.Zefania, ] diff --git a/openlp/plugins/images/lib/mediaitem.py b/openlp/plugins/images/lib/mediaitem.py index 36df55dac..d5892c250 100644 --- a/openlp/plugins/images/lib/mediaitem.py +++ b/openlp/plugins/images/lib/mediaitem.py @@ -551,6 +551,7 @@ class ImageMediaItem(MediaManagerItem): service_item.add_capability(ItemCapabilities.CanLoop) service_item.add_capability(ItemCapabilities.CanAppend) service_item.add_capability(ItemCapabilities.CanEditTitle) + service_item.add_capability(ItemCapabilities.HasThumbnails) # force a nonexistent theme service_item.theme = -1 missing_items_file_names = [] @@ -589,7 +590,7 @@ class ImageMediaItem(MediaManagerItem): # Continue with the existing images. for filename in images_file_names: name = os.path.split(filename)[1] - service_item.add_from_image(filename, name, background) + service_item.add_from_image(filename, name, background, os.path.join(self.service_path, name)) return True def check_group_exists(self, new_group): diff --git a/openlp/plugins/presentations/lib/impresscontroller.py b/openlp/plugins/presentations/lib/impresscontroller.py index d032b9161..a6e52411b 100644 --- a/openlp/plugins/presentations/lib/impresscontroller.py +++ b/openlp/plugins/presentations/lib/impresscontroller.py @@ -65,7 +65,7 @@ from PyQt4 import QtCore from openlp.core.lib import ScreenList from openlp.core.utils import delete_file, get_uno_command, get_uno_instance -from .presentationcontroller import PresentationController, PresentationDocument +from .presentationcontroller import PresentationController, PresentationDocument, TextType log = logging.getLogger(__name__) @@ -257,6 +257,7 @@ class ImpressDocument(PresentationDocument): self.presentation.Display = ScreenList().current['number'] + 1 self.control = None self.create_thumbnails() + self.create_titles_and_notes() return True def create_thumbnails(self): @@ -450,22 +451,44 @@ class ImpressDocument(PresentationDocument): :param slide_no: The slide the notes are required for, starting at 1 """ - return self.__get_text_from_page(slide_no, True) + return self.__get_text_from_page(slide_no, TextType.Notes) - def __get_text_from_page(self, slide_no, notes=False): + def __get_text_from_page(self, slide_no, text_type=TextType.SlideText): """ Return any text extracted from the presentation page. :param slide_no: The slide the notes are required for, starting at 1 :param notes: A boolean. If set the method searches the notes of the slide. + :param text_type: A TextType. Enumeration of the types of supported text. """ text = '' - pages = self.document.getDrawPages() - page = pages.getByIndex(slide_no - 1) - if notes: - page = page.getNotesPage() - for index in range(page.getCount()): - shape = page.getByIndex(index) - if shape.supportsService("com.sun.star.drawing.Text"): - text += shape.getString() + '\n' + if TextType.Title <= text_type <= TextType.Notes: + pages = self.document.getDrawPages() + if 0 < slide_no <= pages.getCount(): + page = pages.getByIndex(slide_no - 1) + if text_type == TextType.Notes: + page = page.getNotesPage() + for index in range(page.getCount()): + shape = page.getByIndex(index) + shape_type = shape.getShapeType() + if shape.supportsService("com.sun.star.drawing.Text"): + # if they requested title, make sure it is the title + if text_type != TextType.Title or shape_type == "com.sun.star.presentation.TitleTextShape": + text += shape.getString() + '\n' return text + + def create_titles_and_notes(self): + """ + Writes the list of titles (one per slide) to 'titles.txt' and the notes to 'slideNotes[x].txt' + in the thumbnails directory + """ + titles = [] + notes = [] + pages = self.document.getDrawPages() + for slide_no in range(1, pages.getCount() + 1): + titles.append(self.__get_text_from_page(slide_no, TextType.Title).replace('\n', ' ') + '\n') + note = self.__get_text_from_page(slide_no, TextType.Notes) + if len(note) == 0: + note = ' ' + notes.append(note) + self.save_titles_and_notes(titles, notes) diff --git a/openlp/plugins/presentations/lib/mediaitem.py b/openlp/plugins/presentations/lib/mediaitem.py index 5b503d50f..5467d117d 100644 --- a/openlp/plugins/presentations/lib/mediaitem.py +++ b/openlp/plugins/presentations/lib/mediaitem.py @@ -288,13 +288,14 @@ class PresentationMediaItem(MediaManagerItem): os.path.join(doc.get_temp_folder(), 'mainslide001.png')): doc.load_presentation() i = 1 - image_file = 'mainslide%03d.png' % i - image = os.path.join(doc.get_temp_folder(), image_file) + image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i) + thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i) while os.path.isfile(image): - service_item.add_from_image(image, name) + service_item.add_from_image(image, name, thumbnail=thumbnail) i += 1 - image_file = 'mainslide%03d.png' % i - image = os.path.join(doc.get_temp_folder(), image_file) + image = os.path.join(doc.get_temp_folder(), 'mainslide%03d.png' % i) + thumbnail = os.path.join(doc.get_thumbnail_folder(), 'slide%d.png' % i) + service_item.add_capability(ItemCapabilities.HasThumbnails) doc.close_presentation() return True else: @@ -323,8 +324,21 @@ class PresentationMediaItem(MediaManagerItem): i = 1 img = doc.get_thumbnail_path(i, True) if img: + # Get titles and notes + titles, notes = doc.get_titles_and_notes() + service_item.add_capability(ItemCapabilities.HasDisplayTitle) + if notes.count('') != len(notes): + service_item.add_capability(ItemCapabilities.HasNotes) + service_item.add_capability(ItemCapabilities.HasThumbnails) while img: - service_item.add_from_command(path, name, img) + # Use title and note if available + title = '' + if titles and len(titles) >= i: + title = titles[i - 1] + note = '' + if notes and len(notes) >= i: + note = notes[i - 1] + service_item.add_from_command(path, name, img, title, note) i += 1 img = doc.get_thumbnail_path(i, True) doc.close_presentation() diff --git a/openlp/plugins/presentations/lib/powerpointcontroller.py b/openlp/plugins/presentations/lib/powerpointcontroller.py index f42e4f814..cf0f5bb99 100644 --- a/openlp/plugins/presentations/lib/powerpointcontroller.py +++ b/openlp/plugins/presentations/lib/powerpointcontroller.py @@ -27,7 +27,7 @@ # Temple Place, Suite 330, Boston, MA 02111-1307 USA # ############################################################################### """ -This modul is for controlling powerpiont. PPT API documentation: +This module is for controlling powerpoint. PPT API documentation: `http://msdn.microsoft.com/en-us/library/aa269321(office.10).aspx`_ """ import os @@ -37,16 +37,17 @@ from openlp.core.common import is_win if is_win(): from win32com.client import Dispatch + import win32com import winreg import win32ui import pywintypes from openlp.core.lib import ScreenList +from openlp.core.common import Registry from openlp.core.lib.ui import UiStrings, critical_error_message_box, translate from openlp.core.common import trace_error_handler from .presentationcontroller import PresentationController, PresentationDocument - log = logging.getLogger(__name__) @@ -136,6 +137,7 @@ class PowerpointDocument(PresentationDocument): self.controller.process.Presentations.Open(self.file_path, False, False, True) self.presentation = self.controller.process.Presentations(self.controller.process.Presentations.Count) self.create_thumbnails() + self.create_titles_and_notes() # Powerpoint 2013 pops up when loading a file, so we minimize it again if self.presentation.Application.Version == u'15.0': try: @@ -392,6 +394,28 @@ class PowerpointDocument(PresentationDocument): """ return _get_text_from_shapes(self.presentation.Slides(slide_no).NotesPage.Shapes) + def create_titles_and_notes(self): + """ + Writes the list of titles (one per slide) + to 'titles.txt' + and the notes to 'slideNotes[x].txt' + in the thumbnails directory + """ + titles = [] + notes = [] + for slide in self.presentation.Slides: + try: + text = slide.Shapes.Title.TextFrame.TextRange.Text + except Exception as e: + log.exception(e) + text = '' + titles.append(text.replace('\n', ' ').replace('\x0b', ' ') + '\n') + note = _get_text_from_shapes(slide.NotesPage.Shapes) + if len(note) == 0: + note = ' ' + notes.append(note) + self.save_titles_and_notes(titles, notes) + def show_error_msg(self): """ Stop presentation and display an error message. @@ -410,8 +434,8 @@ def _get_text_from_shapes(shapes): :param shapes: A set of shapes to search for text. """ text = '' - for index in range(shapes.Count): - shape = shapes(index + 1) - if shape.HasTextFrame: - text += shape.TextFrame.TextRange.Text + '\n' + for shape in shapes: + if shape.PlaceholderFormat.Type == 2: # 2 from is enum PpPlaceholderType.ppPlaceholderBody + if shape.HasTextFrame and shape.TextFrame.HasText: + text += shape.TextFrame.TextRange.Text + '\n' return text diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py index 7e03e322f..4aea501b4 100644 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ b/openlp/plugins/presentations/lib/pptviewcontroller.py @@ -29,6 +29,11 @@ import logging import os +import logging +import zipfile +import re +from xml.etree import ElementTree + from openlp.core.common import is_win @@ -127,14 +132,14 @@ class PptviewDocument(PresentationDocument): temp_folder = self.get_temp_folder() size = ScreenList().current['size'] rect = RECT(size.x(), size.y(), size.right(), size.bottom()) - file_path = os.path.normpath(self.file_path) + self.file_path = os.path.normpath(self.file_path) preview_path = os.path.join(temp_folder, 'slide') # Ensure that the paths are null terminated - file_path = file_path.encode('utf-16-le') + b'\0' + self.file_path = self.file_path.encode('utf-16-le') + b'\0' preview_path = preview_path.encode('utf-16-le') + b'\0' if not os.path.isdir(temp_folder): os.makedirs(temp_folder) - self.ppt_id = self.controller.process.OpenPPT(file_path, None, rect, preview_path) + self.ppt_id = self.controller.process.OpenPPT(self.file_path, None, rect, preview_path) if self.ppt_id >= 0: self.create_thumbnails() self.stop_presentation() @@ -154,6 +159,68 @@ class PptviewDocument(PresentationDocument): path = '%s\\slide%s.bmp' % (self.get_temp_folder(), str(idx + 1)) self.convert_thumbnail(path, idx + 1) + def create_titles_and_notes(self): + """ + Extracts the titles and notes from the zipped file + and writes the list of titles (one per slide) + to 'titles.txt' + and the notes to 'slideNotes[x].txt' + in the thumbnails directory + """ + titles = None + notes = None + filename = os.path.normpath(self.file_path) + # let's make sure we have a valid zipped presentation + if os.path.exists(filename) and zipfile.is_zipfile(filename): + namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main", + "a": "http://schemas.openxmlformats.org/drawingml/2006/main"} + # open the file + with zipfile.ZipFile(filename) as zip_file: + # find the presentation.xml to get the slide count + with zip_file.open('ppt/presentation.xml') as pres: + tree = ElementTree.parse(pres) + nodes = tree.getroot().findall(".//p:sldIdLst/p:sldId", namespaces=namespaces) + # initialize the lists + titles = ['' for i in range(len(nodes))] + notes = ['' for i in range(len(nodes))] + # loop thru the file list to find slides and notes + for zip_info in zip_file.infolist(): + node_type = '' + index = -1 + list_to_add = None + # check if it is a slide + match = re.search("slides/slide(.+)\.xml", zip_info.filename) + if match: + index = int(match.group(1))-1 + node_type = 'ctrTitle' + list_to_add = titles + # or a note + match = re.search("notesSlides/notesSlide(.+)\.xml", zip_info.filename) + if match: + index = int(match.group(1))-1 + node_type = 'body' + list_to_add = notes + # if it is one of our files, index shouldn't be -1 + if index >= 0: + with zip_file.open(zip_info) as zipped_file: + tree = ElementTree.parse(zipped_file) + text = '' + nodes = tree.getroot().findall(".//p:ph[@type='" + node_type + "']../../..//p:txBody//a:t", + namespaces=namespaces) + # if we found any content + if nodes and len(nodes) > 0: + for node in nodes: + if len(text) > 0: + text += '\n' + text += node.text + # Let's remove the \n from the titles and + # just add one at the end + if node_type == 'ctrTitle': + text = text.replace('\n', ' ').replace('\x0b', ' ') + '\n' + list_to_add[index] = text + # now let's write the files + self.save_titles_and_notes(titles, notes) + def close_presentation(self): """ Close presentation and clean up objects. Triggered by new object being added to SlideController or OpenLP being diff --git a/openlp/plugins/presentations/lib/pptviewlib/test.pptx b/openlp/plugins/presentations/lib/pptviewlib/test.pptx new file mode 100644 index 000000000..c8beab172 Binary files /dev/null and b/openlp/plugins/presentations/lib/pptviewlib/test.pptx differ diff --git a/openlp/plugins/presentations/lib/presentationcontroller.py b/openlp/plugins/presentations/lib/presentationcontroller.py index 3060bcdb0..c64d70016 100644 --- a/openlp/plugins/presentations/lib/presentationcontroller.py +++ b/openlp/plugins/presentations/lib/presentationcontroller.py @@ -293,6 +293,49 @@ class PresentationDocument(object): """ return '' + def get_titles_and_notes(self): + """ + Reads the titles from the titles file and + the notes files and returns the content in two lists + """ + titles = [] + notes = [] + titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') + if os.path.exists(titles_file): + try: + with open(titles_file) as fi: + titles = fi.read().splitlines() + except: + log.exception('Failed to open/read existing titles file') + titles = [] + for slide_no, title in enumerate(titles, 1): + notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no) + note = '' + if os.path.exists(notes_file): + try: + with open(notes_file) as fn: + note = fn.read() + except: + log.exception('Failed to open/read notes file') + note = '' + notes.append(note) + return titles, notes + + def save_titles_and_notes(self, titles, notes): + """ + Performs the actual persisting of titles to the titles.txt + and notes to the slideNote%.txt + """ + if titles: + titles_file = os.path.join(self.get_thumbnail_folder(), 'titles.txt') + with open(titles_file, mode='w') as fo: + fo.writelines(titles) + if notes: + for slide_no, note in enumerate(notes, 1): + notes_file = os.path.join(self.get_thumbnail_folder(), 'slideNotes%d.txt' % slide_no) + with open(notes_file, mode='w') as fn: + fn.write(note) + class PresentationController(object): """ @@ -427,3 +470,12 @@ class PresentationController(object): def close_presentation(self): pass + + +class TextType(object): + """ + Type Enumeration for Types of Text to request + """ + Title = 0 + SlideText = 1 + Notes = 2 diff --git a/openlp/plugins/remotes/html/openlp.js b/openlp/plugins/remotes/html/openlp.js index ffc9430c2..9f18c1552 100644 --- a/openlp/plugins/remotes/html/openlp.js +++ b/openlp/plugins/remotes/html/openlp.js @@ -87,16 +87,30 @@ window.OpenLP = { var ul = $("#slide-controller > div[data-role=content] > ul[data-role=listview]"); ul.html(""); for (idx in data.results.slides) { - var text = data.results.slides[idx]["tag"]; - if (text != "") text = text + ": "; - text = text + data.results.slides[idx]["text"]; + var indexInt = parseInt(idx,10); + var slide = data.results.slides[idx]; + var text = slide["tag"]; + if (text != "") { + text = text + ": "; + } + if (slide["title"]) { + text += slide["title"] + } else { + text += slide["text"]; + } + if (slide["notes"]) { + text += ("
" + slide["notes"] + "
"); + } text = text.replace(/\n/g, '
'); - var li = $("
  • ").append( - $("").attr("value", parseInt(idx, 10)).html(text)); - if (data.results.slides[idx]["selected"]) { + if (slide["img"]) { + text += ""; + } + var li = $("
  • ").append($("").html(text)); + if (slide["selected"]) { li.attr("data-theme", "e"); } li.children("a").click(OpenLP.setSlide); + li.find("*").attr("value", indexInt ); ul.append(li); } OpenLP.currentItem = data.results.item; diff --git a/openlp/plugins/remotes/html/stage.js b/openlp/plugins/remotes/html/stage.js index ec88706c1..e63025b80 100644 --- a/openlp/plugins/remotes/html/stage.js +++ b/openlp/plugins/remotes/html/stage.js @@ -102,7 +102,21 @@ window.OpenLP = { $("#verseorder span").removeClass("currenttag"); $("#tag" + OpenLP.currentTags[OpenLP.currentSlide]).addClass("currenttag"); var slide = OpenLP.currentSlides[OpenLP.currentSlide]; - var text = slide["text"]; + var text = ""; + // use title if available + if (slide["title"]) { + text = slide["title"]; + } else { + text = slide["text"]; + } + // use thumbnail if available + if (slide["img"]) { + text += "

    "; + } + // use notes if available + if (slide["notes"]) { + text += '
    ' + slide["notes"]; + } text = text.replace(/\n/g, "
    "); $("#currentslide").html(text); text = ""; @@ -110,7 +124,11 @@ window.OpenLP = { for (var idx = OpenLP.currentSlide + 1; idx < OpenLP.currentSlides.length; idx++) { if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) text = text + "

    "; - text = text + OpenLP.currentSlides[idx]["text"]; + if (OpenLP.currentSlides[idx]["title"]) { + text = text + OpenLP.currentSlides[idx]["title"]; + } else { + text = text + OpenLP.currentSlides[idx]["text"]; + } if (OpenLP.currentTags[idx] != OpenLP.currentTags[idx - 1]) text = text + "

    "; else diff --git a/openlp/plugins/remotes/lib/httprouter.py b/openlp/plugins/remotes/lib/httprouter.py index 4241b34dc..ff28de399 100644 --- a/openlp/plugins/remotes/lib/httprouter.py +++ b/openlp/plugins/remotes/lib/httprouter.py @@ -125,7 +125,7 @@ from mako.template import Template from PyQt4 import QtCore from openlp.core.common import Registry, RegistryProperties, AppLocation, Settings, translate -from openlp.core.lib import PluginStatus, StringContent, image_to_byte +from openlp.core.lib import PluginStatus, StringContent, image_to_byte, ItemCapabilities, create_thumb log = logging.getLogger(__name__) FILE_TYPES = { @@ -159,6 +159,7 @@ class HttpRouter(RegistryProperties): ('^/(stage)$', {'function': self.serve_file, 'secure': False}), ('^/(main)$', {'function': self.serve_file, 'secure': False}), (r'^/files/(.*)$', {'function': self.serve_file, 'secure': False}), + (r'^/(\w+)/thumbnails([^/]+)?/(.*)$', {'function': self.serve_thumbnail, 'secure': False}), (r'^/api/poll$', {'function': self.poll, 'secure': False}), (r'^/main/poll$', {'function': self.main_poll, 'secure': False}), (r'^/main/image$', {'function': self.main_image, 'secure': False}), @@ -328,7 +329,8 @@ class HttpRouter(RegistryProperties): 'no_results': translate('RemotePlugin.Mobile', 'No Results'), 'options': translate('RemotePlugin.Mobile', 'Options'), 'service': translate('RemotePlugin.Mobile', 'Service'), - 'slides': translate('RemotePlugin.Mobile', 'Slides') + 'slides': translate('RemotePlugin.Mobile', 'Slides'), + 'settings': translate('RemotePlugin.Mobile', 'Settings'), } def serve_file(self, file_name=None): @@ -380,6 +382,42 @@ class HttpRouter(RegistryProperties): content_type = FILE_TYPES.get(ext, 'text/plain') return ext, content_type + def serve_thumbnail(self, controller_name=None, dimensions=None, file_name=None): + """ + Serve an image file. If not found return 404. + """ + log.debug('serve thumbnail %s/thumbnails%s/%s' % (controller_name, dimensions, file_name)) + supported_controllers = ['presentations', 'images'] + # -1 means use the default dimension in ImageManager + width = -1 + height = -1 + if dimensions: + match = re.search('(\d+)x(\d+)', dimensions) + if match: + # let's make sure that the dimensions are within reason + width = sorted([10, int(match.group(1)), 1000])[1] + height = sorted([10, int(match.group(2)), 1000])[1] + content = '' + content_type = None + if controller_name and file_name: + if controller_name in supported_controllers: + full_path = urllib.parse.unquote(file_name) + if '..' not in full_path: # no hacking please + full_path = os.path.normpath(os.path.join(AppLocation.get_section_data_path(controller_name), + 'thumbnails/' + full_path)) + if os.path.exists(full_path): + path, just_file_name = os.path.split(full_path) + self.image_manager.add_image(full_path, just_file_name, None, width, height) + ext, content_type = self.get_content_type(full_path) + image = self.image_manager.get_image(full_path, just_file_name, width, height) + content = image_to_byte(image, False) + if len(content) == 0: + return self.do_not_found() + self.send_response(200) + self.send_header('Content-type', content_type) + self.end_headers() + return content + def poll(self): """ Poll OpenLP to determine the current slide number and item name. @@ -458,6 +496,7 @@ class HttpRouter(RegistryProperties): if current_item: for index, frame in enumerate(current_item.get_frames()): item = {} + # Handle text (songs, custom, bibles) if current_item.is_text(): if frame['verseTag']: item['tag'] = str(frame['verseTag']) @@ -465,11 +504,37 @@ class HttpRouter(RegistryProperties): item['tag'] = str(index + 1) item['text'] = str(frame['text']) item['html'] = str(frame['html']) - else: + # Handle images, unless a custom thumbnail is given or if thumbnails is disabled + elif current_item.is_image() and not frame.get('image', '') and Settings().value('remotes/thumbnails'): item['tag'] = str(index + 1) + thumbnail_path = os.path.join('images', 'thumbnails', frame['title']) + full_thumbnail_path = os.path.join(AppLocation.get_data_path(), thumbnail_path) + # Create thumbnail if it doesn't exists + if not os.path.exists(full_thumbnail_path): + create_thumb(current_item.get_frame_path(index), full_thumbnail_path, False) + item['img'] = urllib.request.pathname2url(os.path.sep + thumbnail_path) + item['text'] = str(frame['title']) + item['html'] = str(frame['title']) + else: + # Handle presentation etc. + item['tag'] = str(index + 1) + if current_item.is_capable(ItemCapabilities.HasDisplayTitle): + item['title'] = str(frame['display_title']) + if current_item.is_capable(ItemCapabilities.HasNotes): + item['notes'] = str(frame['notes']) + if current_item.is_capable(ItemCapabilities.HasThumbnails) and \ + Settings().value('remotes/thumbnails'): + # If the file is under our app directory tree send the portion after the match + data_path = AppLocation.get_data_path() + print(frame) + if frame['image'][0:len(data_path)] == data_path: + item['img'] = urllib.request.pathname2url(frame['image'][len(data_path):]) item['text'] = str(frame['title']) item['html'] = str(frame['title']) item['selected'] = (self.live_controller.selected_row == index) + if current_item.notes: + item['notes'] = item.get('notes', '') + '\n' + current_item.notes + print(item) data.append(item) json_data = {'results': {'slides': data}} if current_item: diff --git a/openlp/plugins/remotes/lib/httpserver.py b/openlp/plugins/remotes/lib/httpserver.py index 9a904090d..826b0530d 100644 --- a/openlp/plugins/remotes/lib/httpserver.py +++ b/openlp/plugins/remotes/lib/httpserver.py @@ -144,6 +144,7 @@ class OpenLPServer(RegistryProperties): try: self.httpd = server_class((address, port), CustomHandler) log.debug("Server started for class %s %s %d" % (server_class, address, port)) + break except OSError: log.debug("failed to start http server thread state %d %s" % (loop, self.http_thread.isRunning())) @@ -151,6 +152,8 @@ class OpenLPServer(RegistryProperties): time.sleep(0.1) except: log.error('Failed to start server ') + loop += 1 + time.sleep(0.1) def stop_server(self): """ diff --git a/openlp/plugins/remotes/lib/remotetab.py b/openlp/plugins/remotes/lib/remotetab.py index 4db25cfc2..45f13faac 100644 --- a/openlp/plugins/remotes/lib/remotetab.py +++ b/openlp/plugins/remotes/lib/remotetab.py @@ -62,6 +62,9 @@ class RemoteTab(SettingsTab): self.twelve_hour_check_box = QtGui.QCheckBox(self.server_settings_group_box) self.twelve_hour_check_box.setObjectName('twelve_hour_check_box') self.server_settings_layout.addRow(self.twelve_hour_check_box) + self.thumbnails_check_box = QtGui.QCheckBox(self.server_settings_group_box) + self.thumbnails_check_box.setObjectName('thumbnails_check_box') + self.server_settings_layout.addRow(self.thumbnails_check_box) self.left_layout.addWidget(self.server_settings_group_box) self.http_settings_group_box = QtGui.QGroupBox(self.left_column) self.http_settings_group_box.setObjectName('http_settings_group_box') @@ -163,6 +166,7 @@ class RemoteTab(SettingsTab): self.left_layout.addStretch() self.right_layout.addStretch() self.twelve_hour_check_box.stateChanged.connect(self.on_twelve_hour_check_box_changed) + self.thumbnails_check_box.stateChanged.connect(self.on_thumbnails_check_box_changed) self.address_edit.textChanged.connect(self.set_urls) self.port_spin_box.valueChanged.connect(self.set_urls) self.https_port_spin_box.valueChanged.connect(self.set_urls) @@ -176,6 +180,8 @@ class RemoteTab(SettingsTab): self.stage_url_label.setText(translate('RemotePlugin.RemoteTab', 'Stage view URL:')) self.live_url_label.setText(translate('RemotePlugin.RemoteTab', 'Live view URL:')) self.twelve_hour_check_box.setText(translate('RemotePlugin.RemoteTab', 'Display stage time in 12h format')) + self.thumbnails_check_box.setText(translate('RemotePlugin.RemoteTab', + 'Show thumbnails of non-text slides in remote and stage view.')) self.android_app_group_box.setTitle(translate('RemotePlugin.RemoteTab', 'Android App')) self.qr_description_label.setText( translate('RemotePlugin.RemoteTab', 'Scan the QR code or click