From f7169ee70801e00d09ee119fdbed489fe9afb306 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Fri, 2 Feb 2018 21:33:41 +0000 Subject: [PATCH] remove ppt viewer --- openlp/core/common/settings.py | 3 +- openlp/core/ui/aboutdialog.py | 2 +- .../presentations/lib/pptviewcontroller.py | 307 ------ .../presentations/lib/pptviewlib/README.TXT | 121 --- .../presentations/lib/pptviewlib/ppttest.py | 210 ---- .../lib/pptviewlib/pptviewlib.cpp | 920 ------------------ .../presentations/lib/pptviewlib/pptviewlib.h | 80 -- .../lib/pptviewlib/pptviewlib.vcproj | 202 ---- .../presentations/lib/pptviewlib/test.ppt | Bin 21504 -> 0 bytes .../presentations/lib/pptviewlib/test.pptx | Bin 46925 -> 0 bytes .../presentations/presentationplugin.py | 3 +- .../presentations/test_pptviewcontroller.py | 226 ----- 12 files changed, 4 insertions(+), 2070 deletions(-) delete mode 100644 openlp/plugins/presentations/lib/pptviewcontroller.py delete mode 100644 openlp/plugins/presentations/lib/pptviewlib/README.TXT delete mode 100644 openlp/plugins/presentations/lib/pptviewlib/ppttest.py delete mode 100644 openlp/plugins/presentations/lib/pptviewlib/pptviewlib.cpp delete mode 100644 openlp/plugins/presentations/lib/pptviewlib/pptviewlib.h delete mode 100644 openlp/plugins/presentations/lib/pptviewlib/pptviewlib.vcproj delete mode 100644 openlp/plugins/presentations/lib/pptviewlib/test.ppt delete mode 100644 openlp/plugins/presentations/lib/pptviewlib/test.pptx delete mode 100644 tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py diff --git a/openlp/core/common/settings.py b/openlp/core/common/settings.py index 1c1186feb..947a9ce6c 100644 --- a/openlp/core/common/settings.py +++ b/openlp/core/common/settings.py @@ -259,7 +259,8 @@ class Settings(QtCore.QSettings): ('media/last directory', 'media/last directory', [(str_to_path, None)]), ('songuasge/db password', 'songusage/db password', []), ('songuasge/db hostname', 'songusage/db hostname', []), - ('songuasge/db database', 'songusage/db database', []) + ('songuasge/db database', 'songusage/db database', []), + ('presentations / Powerpoint Viewer', '', []) ] @staticmethod diff --git a/openlp/core/ui/aboutdialog.py b/openlp/core/ui/aboutdialog.py index d60f0ee0c..b162d0586 100644 --- a/openlp/core/ui/aboutdialog.py +++ b/openlp/core/ui/aboutdialog.py @@ -98,7 +98,7 @@ class UiAboutDialog(object): 'OpenLP is free church presentation software, or lyrics ' 'projection software, used to display slides of songs, Bible ' 'verses, videos, images, and even presentations (if ' - 'Impress, PowerPoint or PowerPoint Viewer is installed) ' + 'Impress or PowerPoint is installed) ' 'for church worship using a computer and a data projector.\n' '\n' 'Find out more about OpenLP: http://openlp.org/\n' diff --git a/openlp/plugins/presentations/lib/pptviewcontroller.py b/openlp/plugins/presentations/lib/pptviewcontroller.py deleted file mode 100644 index 18fad273a..000000000 --- a/openlp/plugins/presentations/lib/pptviewcontroller.py +++ /dev/null @@ -1,307 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2018 OpenLP Developers # -# --------------------------------------------------------------------------- # -# 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 logging -import re -import zipfile -from xml.etree import ElementTree - -from openlp.core.common import is_win -from openlp.core.common.applocation import AppLocation -from openlp.core.display.screens import ScreenList -from openlp.plugins.presentations.lib.presentationcontroller import PresentationController, PresentationDocument - -if is_win(): - from ctypes import cdll - from ctypes.wintypes import RECT - -log = logging.getLogger(__name__) - - -class PptviewController(PresentationController): - """ - Class to control interactions with PowerPoint Viewer Presentations. It creates the runtime Environment , Loads the - and Closes the Presentation. As well as triggering the correct activities based on the users input - """ - log.info('PPTViewController loaded') - - def __init__(self, plugin): - """ - Initialise the class - """ - log.debug('Initialising') - self.process = None - super(PptviewController, self).__init__(plugin, 'Powerpoint Viewer', PptviewDocument) - self.supports = ['ppt', 'pps', 'pptx', 'ppsx', 'pptm'] - - def check_available(self): - """ - PPT Viewer is able to run on this machine. - """ - log.debug('check_available') - if not is_win(): - return False - return self.check_installed() - - if is_win(): - def check_installed(self): - """ - Check the viewer is installed. - """ - log.debug('Check installed') - try: - self.start_process() - return self.process.CheckInstalled() - except OSError: - return False - - def start_process(self): - """ - Loads the PPTVIEWLIB library. - """ - if self.process: - return - log.debug('start PPTView') - dll_path = AppLocation.get_directory(AppLocation.AppDir) \ - / 'plugins' / 'presentations' / 'lib' / 'pptviewlib' / 'pptviewlib.dll' - self.process = cdll.LoadLibrary(str(dll_path)) - if log.isEnabledFor(logging.DEBUG): - self.process.SetDebug(1) - - def kill(self): - """ - Called at system exit to clean up any running presentations - """ - log.debug('Kill pptviewer') - while self.docs: - self.docs[0].close_presentation() - - -class PptviewDocument(PresentationDocument): - """ - Class which holds information and controls a single presentation. - """ - def __init__(self, controller, document_path): - """ - Constructor, store information about the file and initialise. - - :param openlp.core.common.path.Path document_path: File path to the document to load - :rtype: None - """ - log.debug('Init Presentation PowerPoint') - super().__init__(controller, document_path) - self.presentation = None - self.ppt_id = None - self.blanked = False - self.hidden = False - - def load_presentation(self): - """ - Called when a presentation is added to the SlideController. It builds the environment, starts communication with - the background PptView task started earlier. - """ - log.debug('LoadPresentation') - temp_path = self.get_temp_folder() - size = ScreenList().current['size'] - rect = RECT(size.x(), size.y(), size.right(), size.bottom()) - preview_path = temp_path / 'slide' - # Ensure that the paths are null terminated - file_path_utf16 = str(self.file_path).encode('utf-16-le') + b'\0' - preview_path_utf16 = str(preview_path).encode('utf-16-le') + b'\0' - if not temp_path.is_dir(): - temp_path.mkdir(parents=True) - self.ppt_id = self.controller.process.OpenPPT(file_path_utf16, None, rect, preview_path_utf16) - if self.ppt_id >= 0: - self.create_thumbnails() - self.stop_presentation() - return True - else: - return False - - def create_thumbnails(self): - """ - PPTviewLib creates large BMP's, but we want small PNG's for consistency. Convert them here. - """ - log.debug('create_thumbnails') - if self.check_thumbnails(): - return - log.debug('create_thumbnails proceeding') - for idx in range(self.get_slide_count()): - path = self.get_temp_folder() / 'slide{index:d}.bmp'.format(index=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 - # let's make sure we have a valid zipped presentation - if self.file_path.exists() and zipfile.is_zipfile(str(self.file_path)): - namespaces = {"p": "http://schemas.openxmlformats.org/presentationml/2006/main", - "a": "http://schemas.openxmlformats.org/drawingml/2006/main"} - # open the file - with zipfile.ZipFile(str(self.file_path)) 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(r'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(r'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 - shut down. - """ - log.debug('ClosePresentation') - if self.controller.process: - self.controller.process.ClosePPT(self.ppt_id) - self.ppt_id = -1 - self.controller.remove_doc(self) - - def is_loaded(self): - """ - Returns true if a presentation is loaded. - """ - if self.ppt_id < 0: - return False - if self.get_slide_count() < 0: - return False - return True - - def is_active(self): - """ - Returns true if a presentation is currently active. - """ - return self.is_loaded() and not self.hidden - - def blank_screen(self): - """ - Blanks the screen. - """ - self.controller.process.Blank(self.ppt_id) - self.blanked = True - - def unblank_screen(self): - """ - Unblanks (restores) the presentation. - """ - self.controller.process.Unblank(self.ppt_id) - self.blanked = False - - def is_blank(self): - """ - Returns true if screen is blank. - """ - log.debug('is blank OpenOffice') - return self.blanked - - def stop_presentation(self): - """ - Stops the current presentation and hides the output. - """ - self.hidden = True - self.controller.process.Stop(self.ppt_id) - - def start_presentation(self): - """ - Starts a presentation from the beginning. - """ - if self.hidden: - self.hidden = False - self.controller.process.Resume(self.ppt_id) - else: - self.controller.process.RestartShow(self.ppt_id) - - def get_slide_number(self): - """ - Returns the current slide number. - """ - return self.controller.process.GetCurrentSlide(self.ppt_id) - - def get_slide_count(self): - """ - Returns total number of slides. - """ - return self.controller.process.GetSlideCount(self.ppt_id) - - def goto_slide(self, slide_no): - """ - Moves to a specific slide in the presentation. - - :param slide_no: The slide the text is required for, starting at 1 - """ - self.controller.process.GotoSlide(self.ppt_id, slide_no) - - def next_step(self): - """ - Triggers the next effect of slide on the running presentation. - """ - self.controller.process.NextStep(self.ppt_id) - - def previous_step(self): - """ - Triggers the previous slide on the running presentation. - """ - self.controller.process.PrevStep(self.ppt_id) diff --git a/openlp/plugins/presentations/lib/pptviewlib/README.TXT b/openlp/plugins/presentations/lib/pptviewlib/README.TXT deleted file mode 100644 index 85bbf7954..000000000 --- a/openlp/plugins/presentations/lib/pptviewlib/README.TXT +++ /dev/null @@ -1,121 +0,0 @@ - -PPTVIEWLIB - Control PowerPoint Viewer 2003/2007 (for openlp.org) -Copyright (C) 2008-2018 Jonathan Corwin (j@corwin.co.uk) - -This library wrappers the free Microsoft PowerPoint Viewer (2003/2007) program, -allowing it to be more easily controlled from another program. - -The PowerPoint Viewer must already be installed on the destination machine, and is -freely available at microsoft.com. - -The full Microsoft Office PowerPoint and PowerPoint Viewer 97 have a COM interface allowing -automation. This ability was removed from the 2003+ viewer offerings. - -To developers: I am not a C/C++ or Win32 API programmer as you can probably tell. -The code and API of this DLL could certainly do with some tidying up, and the -error trapping, where it exists, is very basic. I'll happily accept patches! - -This library is covered by the GPL (http://www.gnu.org/licenses/) -It is NOT covered by the LGPL, so can only be used in GPL compatable programs. -(http://www.gnu.org/licenses/why-not-lgpl.html) - -This README.TXT must be distributed with the pptviewlib.dll - -This library has a limit of 50 PowerPoints which can be opened simultaneously. - -This project can be built with the free Microsoft Visual C++ 2008 Express Edition. - -USAGE ------ -BOOL CheckInstalled(void); - Returns TRUE if PowerPointViewer is installed. FALSE if not. - -int OpenPPT(char *filename, HWND hParentWnd, RECT rect, char *previewpath); - - Opens the PowerPoint file, counts the number of slides, sizes and positions accordingly - and creates preview images of each slide. Note PowerPoint Viewer only allows the - slideshow to be resized whilst it is being loaded. It can be moved at any time however. - - The only way to count the number of slides is to step through the entire show. Therefore - there will be a delay whilst opening large presentations for the first time. - For pre XP/2003 systems, the slideshow will flicker as the screen snapshots are taken. - - filename: The PowerPoint file to be opened. Full path - hParentWnd: The window which will become the parent of the slideshow window. - Can be NULL. - rect: The location/dimensions of the slideshow window. - If all properties of this structure are zero, the dimensions of the hParentWnd - are used. - previewpath If specified, the prefix to use for snapshot images of each slide, in the - form: previewpath + n + ".bmp", where n is the slide number. - A file called previewpath + "info.txt" will also be created containing information - about the PPT file, to speed up future openings of the unmodified file. - Note it is up the calling program to directly access these images if they - are required. - - RETURNS: An unique identifier to pass to other methods in this library. - If < 0, then the PPT failed to open. - If >=0, ClosePPT must be called when the PPT is no longer being used - or when the calling program is closed to release resources/hooks. - -void ClosePPT(int id); - Closes the presentation, releasing any resources and hooks. - - id: The value returned from OpenPPT. - -int GetCurrentSlide(int id); - Returns the current slide number (from 1) - - id: The value returned from OpenPPT. - -int GetSlideCount(int id); - Returns the total number of slides. - - id: The value returned from OpenPPT. - -void NextStep(int id); - Advances one step (animation) through the slideshow. - - id: The value returned from OpenPPT. - -void PrevStep(int id); - Goes backwards one step (animation) through the slideshow. - - id: The value returned from OpenPPT. - -void GotoSlide(int id, int slideno); - Goes directly to a specific slide in the slideshow - - id: The value returned from OpenPPT. - slideno: The number of the slide (from 1) to go directly to. - - If the slide has already been displayed, then the completed slide with animations performed - will be shown. This is how the PowerPoint Viewer works so have no control over this. - -void RestartShow(int id); - Restarts the show from the beginning. To reset animations, behind the scenes it - has to travel to the end and step backwards though the entire show. Therefore - for large presentations there might be a delay. - - id: The value returned from OpenPPT. - -void Blank(int id); - Blanks the screen, colour black. - - id: The value returned from OpenPPT. - -void Unblank(int id) - Unblanks the screen, restoring it to it's pre-blank state. - - id: The value returned from OpenPPT. - -void Stop(int id) - Moves the slideshow off the screen. (There is no concept of stop show in the PowerPoint Viewer) - - id: The value returned from OpenPPT. - -void Resume(int id) - Moves the slideshow display back onto the screen following a Stop() - - id: The value returned from OpenPPT. - diff --git a/openlp/plugins/presentations/lib/pptviewlib/ppttest.py b/openlp/plugins/presentations/lib/pptviewlib/ppttest.py deleted file mode 100644 index 008e66164..000000000 --- a/openlp/plugins/presentations/lib/pptviewlib/ppttest.py +++ /dev/null @@ -1,210 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2018 OpenLP Developers # -# --------------------------------------------------------------------------- # -# 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 sys -from ctypes import * -from ctypes.wintypes import RECT - -from PyQt5 import QtWidgets - - -class PPTViewer(QtWidgets.QWidget): - """ - Standalone Test Harness for the pptviewlib library - """ - def __init__(self, parent=None): - super(PPTViewer, self).__init__(parent) - self.pptid = -1 - self.setWindowTitle('PowerPoint Viewer Test') - - ppt_label = QtWidgets.QLabel('Open PowerPoint file') - slide_label = QtWidgets.QLabel('Go to slide #') - self.pptEdit = QtWidgets.QLineEdit() - self.slideEdit = QtWidgets.QLineEdit() - x_label = QtWidgets.QLabel('X pos') - y_label = QtWidgets.QLabel('Y pos') - width_label = QtWidgets.QLabel('Width') - height_label = QtWidgets.QLabel('Height') - self.xEdit = QtWidgets.QLineEdit('100') - self.yEdit = QtWidgets.QLineEdit('100') - self.widthEdit = QtWidgets.QLineEdit('900') - self.heightEdit = QtWidgets.QLineEdit('700') - self.total = QtWidgets.QLabel() - ppt_btn = QtWidgets.QPushButton('Open') - ppt_dlg_btn = QtWidgets.QPushButton('...') - folder_label = QtWidgets.QLabel('Slide .bmp path') - self.folderEdit = QtWidgets.QLineEdit('slide') - slide_btn = QtWidgets.QPushButton('Go') - prev = QtWidgets.QPushButton('Prev') - next = QtWidgets.QPushButton('Next') - blank = QtWidgets.QPushButton('Blank') - unblank = QtWidgets.QPushButton('Unblank') - restart = QtWidgets.QPushButton('Restart') - close = QtWidgets.QPushButton('Close') - resume = QtWidgets.QPushButton('Resume') - stop = QtWidgets.QPushButton('Stop') - grid = QtWidgets.QGridLayout() - row = 0 - grid.addWidget(folder_label, 0, 0) - grid.addWidget(self.folderEdit, 0, 1) - row += 1 - grid.addWidget(x_label, row, 0) - grid.addWidget(self.xEdit, row, 1) - grid.addWidget(y_label, row, 2) - grid.addWidget(self.yEdit, row, 3) - row += 1 - grid.addWidget(width_label, row, 0) - grid.addWidget(self.widthEdit, row, 1) - grid.addWidget(height_label, row, 2) - grid.addWidget(self.heightEdit, row, 3) - row += 1 - grid.addWidget(ppt_label, row, 0) - grid.addWidget(self.pptEdit, row, 1) - grid.addWidget(ppt_dlg_btn, row, 2) - grid.addWidget(ppt_btn, row, 3) - row += 1 - grid.addWidget(slide_label, row, 0) - grid.addWidget(self.slideEdit, row, 1) - grid.addWidget(slide_btn, row, 2) - row += 1 - grid.addWidget(prev, row, 0) - grid.addWidget(next, row, 1) - row += 1 - grid.addWidget(blank, row, 0) - grid.addWidget(unblank, row, 1) - row += 1 - grid.addWidget(restart, row, 0) - grid.addWidget(close, row, 1) - row += 1 - grid.addWidget(stop, row, 0) - grid.addWidget(resume, row, 1) - ppt_btn.clicked.connect(self.openClick) - ppt_dlg_btn.clicked.connect(self.openDialog) - slide_btn.clicked.connect(self.gotoClick) - prev.clicked.connect(self.prevClick) - next.clicked.connect(self.nextClick) - blank.clicked.connect(self.blankClick) - unblank.clicked.connect(self.unblankClick) - restart.clicked.connect(self.restartClick) - close.clicked.connect(self.closeClick) - stop.clicked.connect(self.stopClick) - resume.clicked.connect(self.resumeClick) - self.setLayout(grid) - self.resize(300, 150) - - def prevClick(self): - if self.pptid < 0: - return - self.pptdll.PrevStep(self.pptid) - self.updateCurrSlide() - app.processEvents() - - def nextClick(self): - if self.pptid < 0: - return - self.pptdll.NextStep(self.pptid) - self.updateCurrSlide() - app.processEvents() - - def blankClick(self): - if self.pptid < 0: - return - self.pptdll.Blank(self.pptid) - app.processEvents() - - def unblankClick(self): - if self.pptid < 0: - return - self.pptdll.Unblank(self.pptid) - app.processEvents() - - def restartClick(self): - if self.pptid < 0: - return - self.pptdll.RestartShow(self.pptid) - self.updateCurrSlide() - app.processEvents() - - def stopClick(self): - if self.pptid < 0: - return - self.pptdll.Stop(self.pptid) - app.processEvents() - - def resumeClick(self): - if self.pptid < 0: - return - self.pptdll.Resume(self.pptid) - app.processEvents() - - def closeClick(self): - if self.pptid < 0: - return - self.pptdll.ClosePPT(self.pptid) - self.pptid = -1 - app.processEvents() - - def openClick(self): - oldid = self.pptid - rect = RECT(int(self.xEdit.text()), int(self.yEdit.text()), - int(self.widthEdit.text()), int(self.heightEdit.text())) - filename = str(self.pptEdit.text().replace('/', '\\')) - folder = str(self.folderEdit.text().replace('/', '\\')) - print(filename, folder) - self.pptid = self.pptdll.OpenPPT(filename, None, rect, folder) - print('id: ' + str(self.pptid)) - if oldid >= 0: - self.pptdll.ClosePPT(oldid) - slides = self.pptdll.GetSlideCount(self.pptid) - print('slidecount: ' + str(slides)) - self.total.setNum(self.pptdll.GetSlideCount(self.pptid)) - self.updateCurrSlide() - - def updateCurrSlide(self): - if self.pptid < 0: - return - slide = str(self.pptdll.GetCurrentSlide(self.pptid)) - print('currslide: ' + slide) - self.slideEdit.setText(slide) - app.processEvents() - - def gotoClick(self): - if self.pptid < 0: - return - print(self.slideEdit.text()) - self.pptdll.GotoSlide(self.pptid, int(self.slideEdit.text())) - self.updateCurrSlide() - app.processEvents() - - def openDialog(self): - self.pptEdit.setText(QtWidgets.QFileDialog.getOpenFileName(self, 'Open file')[0]) - - -if __name__ == '__main__': - pptdll = cdll.LoadLibrary(r'pptviewlib.dll') - pptdll.SetDebug(1) - print('Begin...') - app = QtWidgets.QApplication(sys.argv) - window = PPTViewer() - window.pptdll = pptdll - window.show() - sys.exit(app.exec()) diff --git a/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.cpp b/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.cpp deleted file mode 100644 index 8a7da02a2..000000000 --- a/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.cpp +++ /dev/null @@ -1,920 +0,0 @@ -/****************************************************************************** -* OpenLP - Open Source Lyrics Projection * -* --------------------------------------------------------------------------- * -* Copyright (c) 2008-2018 OpenLP Developers * -* --------------------------------------------------------------------------- * -* 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 * -******************************************************************************/ - -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "pptviewlib.h" - -// Because of the callbacks used by SetWindowsHookEx, the memory used needs to -// be sharable across processes (the callbacks are done from a different -// process) Therefore use data_seg with RWS memory. -// -// See http://msdn.microsoft.com/en-us/library/aa366551(VS.85).aspx for -// alternative method of holding memory, removing fixed limits which would allow -// dynamic number of items, rather than a fixed number. Use a Local\ mapping, -// since global has UAC issues in Vista. - -#pragma data_seg(".PPTVIEWLIB") -PPTVIEW pptView[MAX_PPTS] = {NULL}; -HHOOK globalHook = NULL; -BOOL debug = FALSE; -#pragma data_seg() -#pragma comment(linker, "/SECTION:.PPTVIEWLIB,RWS") - -HINSTANCE hInstance = NULL; - -BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReasonForCall, - LPVOID lpReserved) -{ - hInstance = (HINSTANCE)hModule; - switch(ulReasonForCall) - { - case DLL_PROCESS_ATTACH: - DEBUG(L"PROCESS_ATTACH\n"); - break; - case DLL_THREAD_ATTACH: - //DEBUG(L"THREAD_ATTACH\n"); - break; - case DLL_THREAD_DETACH: - //DEBUG(L"THREAD_DETACH\n"); - break; - case DLL_PROCESS_DETACH: - // Clean up... hopefully there is only the one process attached? - // We'll find out soon enough during tests! - DEBUG(L"PROCESS_DETACH\n"); - for (int i = 0; i < MAX_PPTS; i++) - ClosePPT(i); - break; - } - return TRUE; -} - -DllExport void SetDebug(BOOL onOff) -{ - printf("SetDebug\n"); - debug = onOff; - DEBUG(L"enabled\n"); -} - -DllExport BOOL CheckInstalled() -{ - wchar_t cmdLine[MAX_PATH * 2]; - - DEBUG(L"CheckInstalled\n"); - BOOL found = GetPPTViewerPath(cmdLine, sizeof(cmdLine)); - if(found) - { - DEBUG(L"Exe: %s\n", cmdLine); - } - return found; -} - -// Open the PointPoint, count the slides and take a snapshot of each slide -// for use in previews -// previewpath is a prefix for the location to put preview images of each slide. -// ".bmp" will be appended to complete the path. E.g. "c:\temp\slide" would -// create "c:\temp\slide1.bmp" slide2.bmp, slide3.bmp etc. -// It will also create a *info.txt containing information about the ppt -DllExport int OpenPPT(wchar_t *filename, HWND hParentWnd, RECT rect, - wchar_t *previewPath) -{ - STARTUPINFO si; - PROCESS_INFORMATION pi; - wchar_t cmdLine[MAX_PATH * 2]; - int id; - - DEBUG(L"OpenPPT start: %s; %s\n", filename, previewPath); - DEBUG(L"OpenPPT start: %u; %i, %i, %i, %i\n", hParentWnd, rect.top, - rect.left, rect.bottom, rect.right); - if (GetPPTViewerPath(cmdLine, sizeof(cmdLine)) == FALSE) - { - DEBUG(L"OpenPPT: GetPPTViewerPath failed\n"); - return -1; - } - id = -1; - for (int i = 0; i < MAX_PPTS; i++) - { - if (pptView[i].state == PPT_CLOSED) - { - id = i; - break; - } - } - if (id < 0) - { - DEBUG(L"OpenPPT: Too many PPTs\n"); - return -1; - } - memset(&pptView[id], 0, sizeof(PPTVIEW)); - wcscpy_s(pptView[id].filename, MAX_PATH, filename); - wcscpy_s(pptView[id].previewPath, MAX_PATH, previewPath); - pptView[id].state = PPT_CLOSED; - pptView[id].slideCount = 0; - pptView[id].currentSlide = 0; - pptView[id].firstSlideSteps = 0; - pptView[id].lastSlideSteps = 0; - pptView[id].guess = 0; - pptView[id].hParentWnd = hParentWnd; - pptView[id].hWnd = NULL; - pptView[id].hWnd2 = NULL; - for (int i = 0; i < MAX_SLIDES; i++) - { - pptView[id].slideNos[i] = 0; - } - if (hParentWnd != NULL && rect.top == 0 && rect.bottom == 0 - && rect.left == 0 && rect.right == 0) - { - LPRECT windowRect = NULL; - GetWindowRect(hParentWnd, windowRect); - pptView[id].rect.top = 0; - pptView[id].rect.left = 0; - pptView[id].rect.bottom = windowRect->bottom - windowRect->top; - pptView[id].rect.right = windowRect->right - windowRect->left; - } - else - { - pptView[id].rect.top = rect.top; - pptView[id].rect.left = rect.left; - pptView[id].rect.bottom = rect.bottom; - pptView[id].rect.right = rect.right; - } - wcscat_s(cmdLine, MAX_PATH * 2, L" /F /S \""); - wcscat_s(cmdLine, MAX_PATH * 2, filename); - wcscat_s(cmdLine, MAX_PATH * 2, L"\""); - memset(&si, 0, sizeof(si)); - memset(&pi, 0, sizeof(pi)); - BOOL gotInfo = GetPPTInfo(id); - /* - * I'd really like to just hook on the new threadid. However this always - * gives error 87. Perhaps I'm hooking to soon? No idea... however can't - * wait since I need to ensure I pick up the WM_CREATE as this is the only - * time the window can be resized in such away the content scales correctly - * - * hook = SetWindowsHookEx(WH_CBT,CbtProc,hInstance,pi.dwThreadId); - */ - if (globalHook != NULL) - { - UnhookWindowsHookEx(globalHook); - } - globalHook = SetWindowsHookEx(WH_CBT, CbtProc, hInstance, NULL); - if (globalHook == 0) - { - DEBUG(L"OpenPPT: SetWindowsHookEx failed\n"); - ClosePPT(id); - return -1; - } - pptView[id].state = PPT_STARTED; - Sleep(10); - if (!CreateProcess(NULL, cmdLine, NULL, NULL, FALSE, 0, 0, NULL, &si, &pi)) - { - DEBUG(L"OpenPPT: CreateProcess failed: %s\n", cmdLine); - ClosePPT(id); - return -1; - } - pptView[id].dwProcessId = pi.dwProcessId; - pptView[id].dwThreadId = pi.dwThreadId; - pptView[id].hThread = pi.hThread; - pptView[id].hProcess = pi.hProcess; - while (pptView[id].state == PPT_STARTED) - Sleep(10); - if (gotInfo) - { - DEBUG(L"OpenPPT: Info loaded, no refresh\n"); - pptView[id].state = PPT_LOADED; - Resume(id); - } - else - { - DEBUG(L"OpenPPT: Get info\n"); - pptView[id].steps = 0; - int steps = 0; - while (pptView[id].state == PPT_OPENED) - { - if (steps <= pptView[id].steps) - { - Sleep(100); - DEBUG(L"OpenPPT: Step %d/%d\n", steps, pptView[id].steps); - steps++; - NextStep(id); - } - Sleep(10); - } - DEBUG(L"OpenPPT: Slides %d, Steps %d, first slide steps %d\n", - pptView[id].slideCount, pptView[id].steps, - pptView[id].firstSlideSteps); - for(int i = 1; i <= pptView[id].slideCount; i++) - { - DEBUG(L"OpenPPT: Slide %d = %d\n", i, pptView[id].slideNos[i]); - } - SavePPTInfo(id); - if (pptView[id].state == PPT_CLOSING - || pptView[id].slideCount <= 0) - { - ClosePPT(id); - id=-1; - } - else - { - RestartShow(id); - } - } - if (id >= 0) - { - if (pptView[id].msgHook != NULL) - { - UnhookWindowsHookEx(pptView[id].msgHook); - } - pptView[id].msgHook = NULL; - } - DEBUG(L"OpenPPT: Exit: id=%i\n", id); - return id; -} -// Load information about the ppt from an info.txt file. -// Format: -// version -// filedate -// filesize -// slidecount -// first slide steps -BOOL GetPPTInfo(int id) -{ - struct _stat fileStats; - wchar_t info[MAX_PATH]; - FILE* pFile; - wchar_t buf[100]; - - DEBUG(L"GetPPTInfo: start\n"); - if (_wstat(pptView[id].filename, &fileStats) != 0) - { - return FALSE; - } - swprintf_s(info, MAX_PATH, L"%sinfo.txt", pptView[id].previewPath); - int err = _wfopen_s(&pFile, info, L"r"); - if (err != 0) - { - DEBUG(L"GetPPTInfo: file open failed - %d\n", err); - return FALSE; - } - fgetws(buf, 100, pFile); // version == 1 - fgetws(buf, 100, pFile); - if (fileStats.st_mtime != _wtoi(buf)) - { - DEBUG(L"GetPPTInfo: date changed\n"); - fclose (pFile); - return FALSE; - } - fgetws(buf, 100, pFile); - if (fileStats.st_size != _wtoi(buf)) - { - DEBUG(L"GetPPTInfo: size changed\n"); - fclose (pFile); - return FALSE; - } - fgetws(buf, 100, pFile); // slidecount - int slideCount = _wtoi(buf); - fgetws(buf, 100, pFile); // first slide steps - int firstSlideSteps = _wtoi(buf); - // check all the preview images still exist - for (int i = 1; i <= slideCount; i++) - { - swprintf_s(info, MAX_PATH, L"%s%i.bmp", pptView[id].previewPath, i); - if (GetFileAttributes(info) == INVALID_FILE_ATTRIBUTES) - { - DEBUG(L"GetPPTInfo: bmp not found\n"); - return FALSE; - } - } - fclose(pFile); - pptView[id].slideCount = slideCount; - pptView[id].firstSlideSteps = firstSlideSteps; - DEBUG(L"GetPPTInfo: exit ok\n"); - return TRUE; -} - -BOOL SavePPTInfo(int id) -{ - struct _stat fileStats; - wchar_t info[MAX_PATH]; - FILE* pFile; - - DEBUG(L"SavePPTInfo: start\n"); - if (_wstat(pptView[id].filename, &fileStats) != 0) - { - DEBUG(L"SavePPTInfo: stat of %s failed\n", pptView[id].filename); - return FALSE; - } - swprintf_s(info, MAX_PATH, L"%sinfo.txt", pptView[id].previewPath); - int err = _wfopen_s(&pFile, info, L"w"); - if (err != 0) - { - DEBUG(L"SavePPTInfo: fopen of %s failed%i\n", info, err); - return FALSE; - } - fprintf(pFile, "1\n"); - fprintf(pFile, "%u\n", fileStats.st_mtime); - fprintf(pFile, "%u\n", fileStats.st_size); - fprintf(pFile, "%u\n", pptView[id].slideCount); - fprintf(pFile, "%u\n", pptView[id].firstSlideSteps); - fclose(pFile); - DEBUG(L"SavePPTInfo: exit ok\n"); - return TRUE; -} - -// Get the path of the PowerPoint viewer from the registry -BOOL GetPPTViewerPath(wchar_t *pptViewerPath, int stringSize) -{ - wchar_t cwd[MAX_PATH]; - - DEBUG(L"GetPPTViewerPath: start\n"); - if(GetPPTViewerPathFromReg(pptViewerPath, stringSize)) - { - if(_waccess(pptViewerPath, 0) != -1) - { - DEBUG(L"GetPPTViewerPath: exit registry\n"); - return TRUE; - } - } - // This is where it gets ugly. PPT2007 it seems no longer stores its - // location in the registry. So we have to use the defaults which will - // upset those who like to put things somewhere else - - // Viewer 2007 in 64bit Windows: - if(_waccess(L"C:\\Program Files (x86)\\Microsoft Office\\Office12\\PPTVIEW.EXE", - 0) != -1) - { - wcscpy_s( - L"C:\\Program Files (x86)\\Microsoft Office\\Office12\\PPTVIEW.EXE", - stringSize, pptViewerPath); - DEBUG(L"GetPPTViewerPath: exit 64bit 2007\n"); - return TRUE; - } - // Viewer 2007 in 32bit Windows: - if(_waccess(L"C:\\Program Files\\Microsoft Office\\Office12\\PPTVIEW.EXE", 0) - != -1) - { - wcscpy_s(L"C:\\Program Files\\Microsoft Office\\Office12\\PPTVIEW.EXE", - stringSize, pptViewerPath); - DEBUG(L"GetPPTViewerPath: exit 32bit 2007\n"); - return TRUE; - } - // Give them the opportunity to place it in the same folder as the app - _wgetcwd(cwd, MAX_PATH); - wcscat_s(cwd, MAX_PATH, L"\\PPTVIEW.EXE"); - if(_waccess(cwd, 0) != -1) - { - wcscpy_s(pptViewerPath, stringSize, cwd); - DEBUG(L"GetPPTViewerPath: exit local\n"); - return TRUE; - } - DEBUG(L"GetPPTViewerPath: exit fail\n"); - return FALSE; -} -BOOL GetPPTViewerPathFromReg(wchar_t *pptViewerPath, int stringSize) -{ - HKEY hKey; - DWORD dwType, dwSize; - LRESULT lResult; - - // The following registry settings are for, respectively, (I think) - // PPT Viewer 2007 (older versions. Latest not in registry) & PPT Viewer 2010 - // PPT Viewer 2003 (recent versions) - // PPT Viewer 2003 (older versions) - // PPT Viewer 97 - if ((RegOpenKeyExW(HKEY_CLASSES_ROOT, - L"PowerPointViewer.Show.12\\shell\\Show\\command", 0, KEY_READ, &hKey) - != ERROR_SUCCESS) - && (RegOpenKeyExW(HKEY_CLASSES_ROOT, - L"PowerPointViewer.Show.11\\shell\\Show\\command", 0, KEY_READ, &hKey) - != ERROR_SUCCESS) - && (RegOpenKeyExW(HKEY_CLASSES_ROOT, - L"Applications\\PPTVIEW.EXE\\shell\\open\\command", 0, KEY_READ, &hKey) - != ERROR_SUCCESS) - && (RegOpenKeyExW(HKEY_CLASSES_ROOT, - L"Applications\\PPTVIEW.EXE\\shell\\Show\\command", 0, KEY_READ, &hKey) - != ERROR_SUCCESS)) - { - return FALSE; - } - dwType = REG_SZ; - dwSize = (DWORD)stringSize; - lResult = RegQueryValueEx(hKey, NULL, NULL, &dwType, (LPBYTE)pptViewerPath, - &dwSize); - RegCloseKey(hKey); - if (lResult != ERROR_SUCCESS) - { - return FALSE; - } - // remove "%1" from end of key value - pptViewerPath[wcslen(pptViewerPath) - 4] = '\0'; - return TRUE; -} - -// Unhook the Windows hook -void Unhook(int id) -{ - DEBUG(L"Unhook: start %d\n", id); - if (pptView[id].hook != NULL) - { - UnhookWindowsHookEx(pptView[id].hook); - } - if (pptView[id].msgHook != NULL) - { - UnhookWindowsHookEx(pptView[id].msgHook); - } - pptView[id].hook = NULL; - pptView[id].msgHook = NULL; - DEBUG(L"Unhook: exit ok\n"); -} - -// Close the PowerPoint viewer, release resources -DllExport void ClosePPT(int id) -{ - DEBUG(L"ClosePPT: start%d\n", id); - pptView[id].state = PPT_CLOSED; - Unhook(id); - if (pptView[id].hWnd == 0) - { - TerminateThread(pptView[id].hThread, 0); - } - else - { - PostMessage(pptView[id].hWnd, WM_CLOSE, 0, 0); - } - CloseHandle(pptView[id].hThread); - CloseHandle(pptView[id].hProcess); - memset(&pptView[id], 0, sizeof(PPTVIEW)); - DEBUG(L"ClosePPT: exit ok\n"); - return; -} -// Moves the show back onto the display -DllExport void Resume(int id) -{ - DEBUG(L"Resume: %d\n", id); - MoveWindow(pptView[id].hWnd, pptView[id].rect.left, - pptView[id].rect.top, - pptView[id].rect.right - pptView[id].rect.left, - pptView[id].rect.bottom - pptView[id].rect.top, TRUE); - Unblank(id); -} -// Moves the show off the screen so it can't be seen -DllExport void Stop(int id) -{ - DEBUG(L"Stop:%d\n", id); - MoveWindow(pptView[id].hWnd, -32000, -32000, - pptView[id].rect.right - pptView[id].rect.left, - pptView[id].rect.bottom - pptView[id].rect.top, TRUE); -} - -// Return the total number of slides -DllExport int GetSlideCount(int id) -{ - DEBUG(L"GetSlideCount:%d\n", id); - if (pptView[id].state == 0) - { - return -1; - } - else - { - return pptView[id].slideCount; - } -} - -// Return the number of the slide currently viewing -DllExport int GetCurrentSlide(int id) -{ - DEBUG(L"GetCurrentSlide:%d\n", id); - if (pptView[id].state == 0) - { - return -1; - } - else - { - return pptView[id].currentSlide; - } -} - -// Take a step forwards through the show -DllExport void NextStep(int id) -{ - DEBUG(L"NextStep:%d (%d)\n", id, pptView[id].currentSlide); - if (pptView[id].currentSlide > pptView[id].slideCount) return; - if (pptView[id].currentSlide < pptView[id].slideCount) - { - pptView[id].guess = pptView[id].currentSlide + 1; - } - PostMessage(pptView[id].hWnd2, WM_MOUSEWHEEL, MAKEWPARAM(0, -WHEEL_DELTA), - 0); -} - -// Take a step backwards through the show -DllExport void PrevStep(int id) -{ - DEBUG(L"PrevStep:%d (%d)\n", id, pptView[id].currentSlide); - if (pptView[id].currentSlide > 1) - { - pptView[id].guess = pptView[id].currentSlide - 1; - } - PostMessage(pptView[id].hWnd2, WM_MOUSEWHEEL, MAKEWPARAM(0, WHEEL_DELTA), - 0); -} - -// Blank the show (black screen) -DllExport void Blank(int id) -{ - // B just toggles blank on/off. However pressing any key unblanks. - // So send random unmapped letter first (say 'A'), then we can - // better guarantee B will blank instead of trying to guess - // whether it was already blank or not. - DEBUG(L"Blank:%d\n", id); - HWND h1 = GetForegroundWindow(); - HWND h2 = GetFocus(); - SetForegroundWindow(pptView[id].hWnd); - SetFocus(pptView[id].hWnd); - // slight pause, otherwise event triggering this call may grab focus back! - Sleep(50); - keybd_event((int)'A', 0, 0, 0); - keybd_event((int)'A', 0, KEYEVENTF_KEYUP, 0); - keybd_event((int)'B', 0, 0, 0); - keybd_event((int)'B', 0, KEYEVENTF_KEYUP, 0); - SetForegroundWindow(h1); - SetFocus(h2); -} -// Unblank the show -DllExport void Unblank(int id) -{ - DEBUG(L"Unblank:%d\n", id); - // Pressing any key resumes. - // For some reason SendMessage works for unblanking, but not blanking. - SendMessage(pptView[id].hWnd2, WM_CHAR, 'A', 0); -} - -// Go directly to a slide -DllExport void GotoSlide(int id, int slideNo) -{ - DEBUG(L"GotoSlide %i %i:\n", id, slideNo); - // Did try WM_KEYDOWN/WM_CHAR/WM_KEYUP with SendMessage but didn't work - // perhaps I was sending to the wrong window? No idea. - // Anyway fall back to keybd_event, which is OK as long we makesure - // the slideshow has focus first - char ch[10]; - - if (slideNo < 0) return; - pptView[id].guess = slideNo; - _itoa_s(slideNo, ch, 10, 10); - HWND h1 = GetForegroundWindow(); - HWND h2 = GetFocus(); - SetForegroundWindow(pptView[id].hWnd); - SetFocus(pptView[id].hWnd); - // slight pause, otherwise event triggering this call may grab focus back! - Sleep(50); - for (int i=0; i<10; i++) - { - if (ch[i] == '\0') break; - keybd_event((BYTE)ch[i], 0, 0, 0); - keybd_event((BYTE)ch[i], 0, KEYEVENTF_KEYUP, 0); - } - keybd_event(VK_RETURN, 0, 0, 0); - keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0); - SetForegroundWindow(h1); - SetFocus(h2); -} - -// Restart the show from the beginning -DllExport void RestartShow(int id) -{ - // If we just go direct to slide one, then it remembers that all other - // slides have been animated, so ends up just showing the completed slides - // of those slides that have been animated next time we advance. - // Only way I've found to get around this is to step backwards all the way - // through. Lets move the window out of the way first so the audience - // doesn't see this. - DEBUG(L"RestartShow:%d\n", id); - Stop(id); - GotoSlide(id, pptView[id].slideCount); - for (int i=0; i <= pptView[id].steps - pptView[id].lastSlideSteps; i++) - { - PrevStep(id); - Sleep(10); - } - int i = 0; - while ((pptView[id].currentSlide > 1) && (i++ < 30000)) - { - Sleep(10); - } - Resume(id); -} - -// This hook is started with the PPTVIEW.EXE process and waits for the -// WM_CREATEWND message. At this point (and only this point) can the -// window be resized to the correct size. -// Release the hook as soon as we're complete to free up resources -LRESULT CALLBACK CbtProc(int nCode, WPARAM wParam, LPARAM lParam) -{ - HHOOK hook = globalHook; - if (nCode == HCBT_CREATEWND) - { - wchar_t csClassName[32]; - HWND hCurrWnd = (HWND)wParam; - DWORD retProcId = NULL; - GetClassName(hCurrWnd, csClassName, sizeof(csClassName)); - if ((wcscmp(csClassName, L"paneClassDC") == 0) - ||(wcscmp(csClassName, L"screenClass") == 0)) - { - int id = -1; - DWORD windowThread = GetWindowThreadProcessId(hCurrWnd, NULL); - for (int i=0; i < MAX_PPTS; i++) - { - if (pptView[i].dwThreadId == windowThread) - { - id = i; - break; - } - } - if (id >= 0) - { - if (wcscmp(csClassName, L"paneClassDC") == 0) - { - pptView[id].hWnd2 = hCurrWnd; - } - else - { - pptView[id].hWnd = hCurrWnd; - CBT_CREATEWND* cw = (CBT_CREATEWND*)lParam; - if (pptView[id].hParentWnd != NULL) - { - cw->lpcs->hwndParent = pptView[id].hParentWnd; - } - cw->lpcs->cy = pptView[id].rect.bottom - - pptView[id].rect.top; - cw->lpcs->cx = pptView[id].rect.right - - pptView[id].rect.left; - cw->lpcs->y = -32000; - cw->lpcs->x = -32000; - } - if ((pptView[id].hWnd != NULL) && (pptView[id].hWnd2 != NULL)) - { - UnhookWindowsHookEx(globalHook); - globalHook = NULL; - pptView[id].hook = SetWindowsHookEx(WH_CALLWNDPROC, - CwpProc, hInstance, pptView[id].dwThreadId); - pptView[id].msgHook = SetWindowsHookEx(WH_GETMESSAGE, - GetMsgProc, hInstance, pptView[id].dwThreadId); - Sleep(10); - pptView[id].state = PPT_OPENED; - } - } - } - } - return CallNextHookEx(hook, nCode, wParam, lParam); -} - -// This hook exists whilst the slideshow is loading but only listens on the -// slideshows thread. It listens out for mousewheel events -LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam) -{ - HHOOK hook = NULL; - MSG *pMSG = (MSG *)lParam; - DWORD windowThread = GetWindowThreadProcessId(pMSG->hwnd, NULL); - int id = -1; - for (int i = 0; i < MAX_PPTS; i++) - { - if (pptView[i].dwThreadId == windowThread) - { - id = i; - hook = pptView[id].msgHook; - break; - } - } - if (id >= 0 && nCode == HC_ACTION && wParam == PM_REMOVE - && pMSG->message == WM_MOUSEWHEEL) - { - if (pptView[id].state != PPT_LOADED) - { - if (pptView[id].currentSlide == 1) - { - pptView[id].firstSlideSteps++; - } - pptView[id].steps++; - pptView[id].lastSlideSteps++; - } - } - return CallNextHookEx(hook, nCode, wParam, lParam); -} -// This hook exists whilst the slideshow is running but only listens on the -// slideshows thread. It listens out for slide changes, message WM_USER+22. -LRESULT CALLBACK CwpProc(int nCode, WPARAM wParam, LPARAM lParam){ - CWPSTRUCT *cwp; - cwp = (CWPSTRUCT *)lParam; - HHOOK hook = NULL; - wchar_t filename[MAX_PATH]; - - DWORD windowThread = GetWindowThreadProcessId(cwp->hwnd, NULL); - int id = -1; - for (int i = 0; i < MAX_PPTS; i++) - { - if (pptView[i].dwThreadId == windowThread) - { - id = i; - hook = pptView[id].hook; - break; - } - } - if ((id >= 0) && (nCode == HC_ACTION)) - { - if (cwp->message == WM_USER + 22) - { - if (pptView[id].state != PPT_LOADED) - { - if ((pptView[id].currentSlide > 0) - && (pptView[id].previewPath != NULL - && wcslen(pptView[id].previewPath) > 0)) - { - swprintf_s(filename, MAX_PATH, L"%s%i.bmp", - pptView[id].previewPath, - pptView[id].currentSlide); - CaptureAndSaveWindow(cwp->hwnd, filename); - } - if (((cwp->wParam == 0) - || (pptView[id].slideNos[1] == cwp->wParam)) - && (pptView[id].currentSlide > 0)) - { - pptView[id].state = PPT_LOADED; - pptView[id].currentSlide = pptView[id].slideCount + 1; - } - else - { - if (cwp->wParam > 0) - { - pptView[id].currentSlide = pptView[id].currentSlide + 1; - pptView[id].slideNos[pptView[id].currentSlide] - = cwp->wParam; - pptView[id].slideCount = pptView[id].currentSlide; - pptView[id].lastSlideSteps = 0; - } - } - } - else - { - if (cwp->wParam > 0) - { - if(pptView[id].guess > 0 - && pptView[id].slideNos[pptView[id].guess] == 0) - { - pptView[id].currentSlide = 0; - } - for(int i = 1; i <= pptView[id].slideCount; i++) - { - if(pptView[id].slideNos[i] == cwp->wParam) - { - pptView[id].currentSlide = i; - break; - } - } - if(pptView[id].currentSlide == 0) - { - pptView[id].slideNos[pptView[id].guess] = cwp->wParam; - pptView[id].currentSlide = pptView[id].guess; - } - pptView[id].guess = 0; - } - } - } - if ((pptView[id].state != PPT_CLOSED) - - &&(cwp->message == WM_CLOSE || cwp->message == WM_QUIT)) - { - pptView[id].state = PPT_CLOSING; - } - } - return CallNextHookEx(hook, nCode, wParam, lParam); -} - -VOID CaptureAndSaveWindow(HWND hWnd, wchar_t* filename) -{ - HBITMAP hBmp; - if ((hBmp = CaptureWindow(hWnd)) == NULL) - { - return; - } - RECT client; - GetClientRect(hWnd, &client); - UINT uiBytesPerRow = 3 * client.right; // RGB takes 24 bits - UINT uiRemainderForPadding; - - if ((uiRemainderForPadding = uiBytesPerRow % sizeof(DWORD)) > 0) - uiBytesPerRow += (sizeof(DWORD) - uiRemainderForPadding); - - UINT uiBytesPerAllRows = uiBytesPerRow * client.bottom; - PBYTE pDataBits; - - if ((pDataBits = new BYTE[uiBytesPerAllRows]) != NULL) - { - BITMAPINFOHEADER bmi = {0}; - BITMAPFILEHEADER bmf = {0}; - - // Prepare to get the data out of HBITMAP: - bmi.biSize = sizeof(bmi); - bmi.biPlanes = 1; - bmi.biBitCount = 24; - bmi.biHeight = client.bottom; - bmi.biWidth = client.right; - - // Get it: - HDC hDC = GetDC(hWnd); - GetDIBits(hDC, hBmp, 0, client.bottom, pDataBits, (BITMAPINFO*) &bmi, - DIB_RGB_COLORS); - ReleaseDC(hWnd, hDC); - - // Fill the file header: - bmf.bfOffBits = sizeof(bmf) + sizeof(bmi); - bmf.bfSize = bmf.bfOffBits + uiBytesPerAllRows; - bmf.bfType = 0x4D42; - - // Writing: - FILE* pFile; - int err = _wfopen_s(&pFile, filename, L"wb"); - if (err == 0) - { - fwrite(&bmf, sizeof(bmf), 1, pFile); - fwrite(&bmi, sizeof(bmi), 1, pFile); - fwrite(pDataBits, sizeof(BYTE), uiBytesPerAllRows, pFile); - fclose(pFile); - } - delete [] pDataBits; - } - DeleteObject(hBmp); -} -HBITMAP CaptureWindow(HWND hWnd) -{ - HDC hDC; - BOOL bOk = FALSE; - HBITMAP hImage = NULL; - - hDC = GetDC(hWnd); - RECT rcClient; - GetClientRect(hWnd, &rcClient); - if ((hImage = CreateCompatibleBitmap(hDC, rcClient.right, rcClient.bottom)) - != NULL) - { - HDC hMemDC; - HBITMAP hDCBmp; - - if ((hMemDC = CreateCompatibleDC(hDC)) != NULL) - { - hDCBmp = (HBITMAP)SelectObject(hMemDC, hImage); - HMODULE hLib = LoadLibrary(L"User32"); - // PrintWindow works for windows outside displayable area - // but was only introduced in WinXP. BitBlt requires the window to - // be topmost and within the viewable area of the display - if (GetProcAddress(hLib, "PrintWindow") == NULL) - { - SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE); - BitBlt(hMemDC, 0, 0, rcClient.right, rcClient.bottom, hDC, 0, - 0, SRCCOPY); - SetWindowPos(hWnd, HWND_NOTOPMOST, -32000, -32000, 0, 0, - SWP_NOSIZE); - } - else - { - PrintWindow(hWnd, hMemDC, 0); - } - SelectObject(hMemDC, hDCBmp); - DeleteDC(hMemDC); - bOk = TRUE; - } - } - ReleaseDC(hWnd, hDC); - if (!bOk) - { - if (hImage) - { - DeleteObject(hImage); - hImage = NULL; - } - } - return hImage; -} diff --git a/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.h b/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.h deleted file mode 100644 index 6411ae828..000000000 --- a/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.h +++ /dev/null @@ -1,80 +0,0 @@ -/****************************************************************************** -* OpenLP - Open Source Lyrics Projection * -* --------------------------------------------------------------------------- * -* Copyright (c) 2008-2018 OpenLP Developers * -* --------------------------------------------------------------------------- * -* 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 * -******************************************************************************/ - -#define DllExport extern "C" __declspec( dllexport ) - -#define DEBUG(...) if (debug) wprintf(__VA_ARGS__) - -enum PPTVIEWSTATE {PPT_CLOSED, PPT_STARTED, PPT_OPENED, PPT_LOADED, - PPT_CLOSING}; - -DllExport int OpenPPT(wchar_t *filename, HWND hParentWnd, RECT rect, - wchar_t *previewPath); -DllExport BOOL CheckInstalled(); -DllExport void ClosePPT(int id); -DllExport int GetCurrentSlide(int id); -DllExport int GetSlideCount(int id); -DllExport void NextStep(int id); -DllExport void PrevStep(int id); -DllExport void GotoSlide(int id, int slide_no); -DllExport void RestartShow(int id); -DllExport void Blank(int id); -DllExport void Unblank(int id); -DllExport void Stop(int id); -DllExport void Resume(int id); -DllExport void SetDebug(BOOL onOff); - -LRESULT CALLBACK CbtProc(int nCode, WPARAM wParam, LPARAM lParam); -LRESULT CALLBACK CwpProc(int nCode, WPARAM wParam, LPARAM lParam); -LRESULT CALLBACK GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam); -BOOL GetPPTViewerPath(wchar_t *pptViewerPath, int stringSize); -BOOL GetPPTViewerPathFromReg(wchar_t *pptViewerPath, int stringSize); -HBITMAP CaptureWindow(HWND hWnd); -VOID SaveBitmap(wchar_t* filename, HBITMAP hBmp) ; -VOID CaptureAndSaveWindow(HWND hWnd, wchar_t* filename); -BOOL GetPPTInfo(int id); -BOOL SavePPTInfo(int id); -void Unhook(int id); - -#define MAX_PPTS 16 -#define MAX_SLIDES 256 - -struct PPTVIEW -{ - HHOOK hook; - HHOOK msgHook; - HWND hWnd; - HWND hWnd2; - HWND hParentWnd; - HANDLE hProcess; - HANDLE hThread; - DWORD dwProcessId; - DWORD dwThreadId; - RECT rect; - int slideCount; - int currentSlide; - int firstSlideSteps; - int lastSlideSteps; - int steps; - int guess; - wchar_t filename[MAX_PATH]; - wchar_t previewPath[MAX_PATH]; - int slideNos[MAX_SLIDES]; - PPTVIEWSTATE state; -}; diff --git a/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.vcproj b/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.vcproj deleted file mode 100644 index d2843e510..000000000 --- a/openlp/plugins/presentations/lib/pptviewlib/pptviewlib.vcproj +++ /dev/null @@ -1,202 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/openlp/plugins/presentations/lib/pptviewlib/test.ppt b/openlp/plugins/presentations/lib/pptviewlib/test.ppt deleted file mode 100644 index 1d90168b16898c8c783c9ac77361fc66ebd84a57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21504 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 arQLPH*5-w@bP$O|>E{XkZ{7RA`ScghAm`oy diff --git a/openlp/plugins/presentations/presentationplugin.py b/openlp/plugins/presentations/presentationplugin.py index b0fd8d7cf..fb0cbee94 100644 --- a/openlp/plugins/presentations/presentationplugin.py +++ b/openlp/plugins/presentations/presentationplugin.py @@ -44,7 +44,6 @@ __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, 'presentations/pdf_program': None, 'presentations/Impress': QtCore.Qt.Checked, 'presentations/Powerpoint': QtCore.Qt.Checked, - 'presentations/Powerpoint Viewer': QtCore.Qt.Checked, 'presentations/Pdf': QtCore.Qt.Checked, 'presentations/presentations files': [], 'presentations/thumbnail_scheme': '', @@ -57,7 +56,7 @@ __default_settings__ = {'presentations/override app': QtCore.Qt.Unchecked, class PresentationPlugin(Plugin): """ This plugin allowed a Presentation to be opened, controlled and displayed on the output display. The plugin controls - third party applications such as OpenOffice.org Impress, Microsoft PowerPoint and the PowerPoint viewer. + third party applications such as OpenOffice.org Impress, and Microsoft PowerPoint. """ log = logging.getLogger('PresentationPlugin') diff --git a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py b/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py deleted file mode 100644 index 5e6a7abdd..000000000 --- a/tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py +++ /dev/null @@ -1,226 +0,0 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2018 OpenLP Developers # -# --------------------------------------------------------------------------- # -# 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 pptviewcontroller module of the Presentations plugin. -""" -import shutil -from tempfile import mkdtemp -from unittest import TestCase, skipIf -from unittest.mock import MagicMock, patch - -from openlp.core.common import is_win -from openlp.core.common.path import Path -from openlp.plugins.presentations.lib.pptviewcontroller import PptviewDocument, PptviewController -from tests.helpers.testmixin import TestMixin -from tests.utils.constants import RESOURCE_PATH - - -class TestPptviewController(TestCase, TestMixin): - """ - Test the PptviewController Class - """ - def setUp(self): - """ - Set up the patches and mocks need for all tests. - """ - self.setup_application() - self.build_settings() - self.mock_plugin = MagicMock() - self.temp_folder = mkdtemp() - self.mock_plugin.settings_section = self.temp_folder - - def tearDown(self): - """ - Stop the patches - """ - self.destroy_settings() - shutil.rmtree(self.temp_folder) - - def test_constructor(self): - """ - Test the Constructor from the PptViewController - """ - # GIVEN: No presentation controller - controller = None - - # WHEN: The presentation controller object is created - controller = PptviewController(plugin=self.mock_plugin) - - # THEN: The name of the presentation controller should be correct - assert 'Powerpoint Viewer' == controller.name, 'The name of the presentation controller should be correct' - - def test_check_available(self): - """ - Test check_available / check_installed - """ - # GIVEN: A mocked dll loader and a controller - with patch('ctypes.cdll.LoadLibrary') as mocked_load_library: - mocked_process = MagicMock() - mocked_process.CheckInstalled.return_value = True - mocked_load_library.return_value = mocked_process - controller = PptviewController(plugin=self.mock_plugin) - - # WHEN: check_available is called - available = controller.check_available() - - # THEN: On windows it should return True, on other platforms False - if is_win(): - assert available is True, 'check_available should return True on windows.' - else: - assert available is False, 'check_available should return False when not on windows.' - - -class TestPptviewDocument(TestCase): - """ - Test the PptviewDocument Class - """ - def setUp(self): - """ - Set up the patches and mocks need for all tests. - """ - self.pptview_document_create_thumbnails_patcher = patch( - 'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.create_thumbnails') - self.pptview_document_stop_presentation_patcher = patch( - 'openlp.plugins.presentations.lib.pptviewcontroller.PptviewDocument.stop_presentation') - self.presentation_document_get_temp_folder_patcher = patch( - 'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument.get_temp_folder') - self.presentation_document_setup_patcher = patch( - 'openlp.plugins.presentations.lib.pptviewcontroller.PresentationDocument._setup') - self.screen_list_patcher = patch('openlp.plugins.presentations.lib.pptviewcontroller.ScreenList') - self.rect_patcher = MagicMock() - self.mock_pptview_document_create_thumbnails = self.pptview_document_create_thumbnails_patcher.start() - self.mock_pptview_document_stop_presentation = self.pptview_document_stop_presentation_patcher.start() - self.mock_presentation_document_get_temp_folder = self.presentation_document_get_temp_folder_patcher.start() - self.mock_presentation_document_setup = self.presentation_document_setup_patcher.start() - self.mock_rect = self.rect_patcher.start() - self.mock_screen_list = self.screen_list_patcher.start() - self.mock_controller = MagicMock() - self.mock_presentation = MagicMock() - self.temp_folder = mkdtemp() - self.mock_presentation_document_get_temp_folder.return_value = self.temp_folder - - def tearDown(self): - """ - Stop the patches - """ - self.pptview_document_create_thumbnails_patcher.stop() - self.pptview_document_stop_presentation_patcher.stop() - self.presentation_document_get_temp_folder_patcher.stop() - self.presentation_document_setup_patcher.stop() - self.rect_patcher.stop() - self.screen_list_patcher.stop() - shutil.rmtree(self.temp_folder) - - @skipIf(not is_win(), 'Not Windows') - def test_load_presentation_succesful(self): - """ - Test the PptviewDocument.load_presentation() method when the PPT is successfully opened - """ - # GIVEN: A reset mocked_os - self.mock_controller.process.OpenPPT.return_value = 0 - instance = PptviewDocument(self.mock_controller, self.mock_presentation) - instance.file_path = 'test\path.ppt' - - # WHEN: The temporary directory exists and OpenPPT returns successfully (not -1) - result = instance.load_presentation() - - # THEN: PptviewDocument.load_presentation should return True - assert result is True - - @skipIf(not is_win(), 'Not Windows') - def test_load_presentation_un_succesful(self): - """ - Test the PptviewDocument.load_presentation() method when the temporary directory does not exist and the PPT is - not successfully opened - """ - # GIVEN: A reset mock_os_isdir - self.mock_controller.process.OpenPPT.return_value = -1 - instance = PptviewDocument(self.mock_controller, self.mock_presentation) - instance.file_path = 'test\path.ppt' - - # WHEN: The temporary directory does not exist and OpenPPT returns unsuccessfully (-1) - with patch.object(instance, 'get_temp_folder') as mocked_get_folder: - mocked_get_folder.return_value = MagicMock(spec=Path) - result = instance.load_presentation() - - # THEN: The temp folder should be created and PptviewDocument.load_presentation should return False - assert result is False - - def test_create_titles_and_notes(self): - """ - Test PowerpointController.create_titles_and_notes - """ - # GIVEN: mocked PresentationController.save_titles_and_notes and a pptx file - doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = RESOURCE_PATH / 'presentations' / 'test.pptx' - doc.save_titles_and_notes = MagicMock() - - # WHEN reading the titles and notes - doc.create_titles_and_notes() - - # THEN save_titles_and_notes should have been called once with empty arrays - doc.save_titles_and_notes.assert_called_once_with(['Test 1\n', '\n', 'Test 2\n', 'Test 4\n', 'Test 3\n'], - ['Notes for slide 1', 'Inserted', 'Notes for slide 2', - 'Notes \nfor slide 4', 'Notes for slide 3']) - - def test_create_titles_and_notes_nonexistent_file(self): - """ - Test PowerpointController.create_titles_and_notes with nonexistent file - """ - # GIVEN: mocked PresentationController.save_titles_and_notes and an nonexistent file - with patch('builtins.open') as mocked_open, \ - patch.object(Path, 'exists') as mocked_path_exists, \ - patch('openlp.plugins.presentations.lib.presentationcontroller.create_paths') as \ - mocked_dir_exists: - mocked_path_exists.return_value = False - mocked_dir_exists.return_value = False - doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = Path('Idontexist.pptx') - doc.save_titles_and_notes = MagicMock() - - # WHEN: Reading the titles and notes - doc.create_titles_and_notes() - - # THEN: File existens should have been checked, and not have been opened. - doc.save_titles_and_notes.assert_called_once_with(None, None) - mocked_path_exists.assert_called_with() - assert mocked_open.call_count == 0, 'There should be no calls to open a file.' - - def test_create_titles_and_notes_invalid_file(self): - """ - Test PowerpointController.create_titles_and_notes with invalid file - """ - # GIVEN: mocked PresentationController.save_titles_and_notes and an invalid file - with patch('builtins.open') as mocked_open, \ - patch('openlp.plugins.presentations.lib.pptviewcontroller.zipfile.is_zipfile') as mocked_is_zf: - mocked_is_zf.return_value = False - mocked_open.filesize = 10 - doc = PptviewDocument(self.mock_controller, self.mock_presentation) - doc.file_path = RESOURCE_PATH / 'presentations' / 'test.ppt' - doc.save_titles_and_notes = MagicMock() - - # WHEN: reading the titles and notes - doc.create_titles_and_notes() - - # THEN: - doc.save_titles_and_notes.assert_called_once_with(None, None) - assert mocked_is_zf.call_count == 1, 'is_zipfile should have been called once'