openlp-core team mailing list archive
-
openlp-core team
-
Mailing list archive
-
Message #29099
[Merge] lp:~trb143/openlp/refactor26 into lp:openlp/android2
Tim Bentley has proposed merging lp:~trb143/openlp/refactor26 into lp:openlp/android2.
Requested reviews:
OpenLP Core (openlp-core)
For more details, see:
https://code.launchpad.net/~trb143/openlp/refactor26/+merge/291039
More moving code around.
Utils directory is removed but more tidy ups are needed.
Changed enough code to warrant a merge.
lp:~trb143/openlp/refactor26 (revision 2670)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1406/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1324/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1263/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1084/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/675/
[SUCCESS] https://ci.openlp.io/job/Branch-05a-Code_Analysis/742/
[SUCCESS] https://ci.openlp.io/job/Branch-05b-Test_Coverage/610/
--
The attached diff has been truncated due to its size.
Your team OpenLP Core is requested to review the proposed merge of lp:~trb143/openlp/refactor26 into lp:openlp/android2.
=== added file '.bzrignore'
--- .bzrignore 1970-01-01 00:00:00 +0000
+++ .bzrignore 2016-04-05 20:22:40 +0000
@@ -0,0 +1,47 @@
+*.pyc
+*.*~
+\#*\#
+*.eric4project
+*.eric5project
+*.ropeproject
+*.e4*
+.eric4project
+.komodotools
+*.komodoproject
+list
+openlp.org 2.0.e4*
+documentation/build/html
+documentation/build/doctrees
+*.log*
+dist
+OpenLP.egg-info
+build
+resources/innosetup/Output
+_eric4project
+.pylint.d
+*.qm
+openlp/core/resources.py.old
+*.qm
+resources/windows/warnOpenLP.txt
+openlp.cfg
+.idea
+openlp.pro
+.kdev4
+tests.kdev4
+*.nja
+*.orig
+__pycache__
+*.dll
+.directory
+*.kate-swp
+# Git files
+.git
+.gitignore
+# Rejected diff's
+*.rej
+*.~\?~
+.coverage
+cover
+*.kdev4
+coverage
+tags
=== renamed file '.bzrignore' => '.bzrignore.moved'
=== added file '.coveragerc'
--- .coveragerc 1970-01-01 00:00:00 +0000
+++ .coveragerc 2016-04-05 20:22:40 +0000
@@ -0,0 +1,5 @@
+[run]
+source = openlp
+
+[html]
+directory = coverage
=== added file 'LICENSE'
--- LICENSE 1970-01-01 00:00:00 +0000
+++ LICENSE 2016-04-05 20:22:40 +0000
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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; either version 2 of the License, or
+ (at your option) any later version.
+
+ 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.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
=== added file 'MANIFEST.in'
--- MANIFEST.in 1970-01-01 00:00:00 +0000
+++ MANIFEST.in 2016-04-05 20:22:40 +0000
@@ -0,0 +1,16 @@
+recursive-include openlp *.py
+recursive-include openlp *.sqlite
+recursive-include openlp *.csv
+recursive-include openlp *.html
+recursive-include openlp *.js
+recursive-include openlp *.css
+recursive-include openlp *.png
+recursive-include openlp *.ps
+recursive-include openlp *.json
+recursive-include documentation *
+recursive-include resources *
+recursive-include scripts *
+include copyright.txt
+include LICENSE
+include README.txt
+include openlp/.version
=== added file 'README.txt'
--- README.txt 1970-01-01 00:00:00 +0000
+++ README.txt 2016-04-05 20:22:40 +0000
@@ -0,0 +1,15 @@
+OpenLP
+======
+
+You're probably reading this because you've just downloaded the source code for
+OpenLP. If you are looking for the installer file, please go to the download
+page on the web site::
+
+ http://openlp.org/download
+
+If you're looking for how to contribute to OpenLP, then please look at the
+OpenLP wiki::
+
+ http://wiki.openlp.org/
+
+Thanks for downloading OpenLP!
=== added file 'copyright.txt'
--- copyright.txt 1970-01-01 00:00:00 +0000
+++ copyright.txt 2016-04-05 20:22:40 +0000
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
=== added directory 'documentation'
=== added file 'documentation/manual.txt'
--- documentation/manual.txt 1970-01-01 00:00:00 +0000
+++ documentation/manual.txt 2016-04-05 20:22:40 +0000
@@ -0,0 +1,7 @@
+OpenLP Manual
+=============
+
+If you're reading this file, you're probably looking for the OpenLP manual. The
+manual is hosted online at http://manual.openlp.org/. If you want to help with
+the manual, contact the OpenLP team via IRC in the #openlp.org channel on the
+Freenode network.
=== added file 'documentation/openlp.1'
--- documentation/openlp.1 1970-01-01 00:00:00 +0000
+++ documentation/openlp.1 2016-04-05 20:22:40 +0000
@@ -0,0 +1,47 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.40.9.
+.TH OPENLP "1" "May 2012" "OpenLP 1.9.9" "User Commands"
+.SH NAME
+OpenLP \- Church worship presentation software
+.SH SYNOPSIS
+.B openlp
+[\fIoptions\fR] [\fIqt-options\fR]
+.SH OPTIONS
+.TP
+\fB\-\-version\fR
+show program's version number and exit
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.TP
+\fB\-e\fR, \fB\-\-no\-error\-form\fR
+Disable the error notification form.
+.TP
+\fB\-l\fR LEVEL, \fB\-\-log\-level\fR=\fILEVEL\fR
+Set logging to LEVEL level. Valid values are "debug",
+"info", "warning".
+.TP
+\fB\-p\fR, \fB\-\-portable\fR
+Specify if this should be run as a portable app, off a
+USB flash drive (not implemented).
+.TP
+\fB\-d\fR, \fB\-\-dev\-version\fR
+Ignore the version file and pull the version directly
+from Bazaar
+.TP
+\fB\-s\fR STYLE, \fB\-\-style\fR=\fISTYLE\fR
+Set the Qt5 style (passed directly to Qt5).
+.TP
+\fB\-\-testing\fR
+Run by testing framework
+.SH "SEE ALSO"
+The full documentation for
+.B OpenLP
+is maintained as a Texinfo manual. If the
+.B info
+and
+.B OpenLP
+programs are properly installed at your site, the command
+.IP
+.B info OpenLP
+.PP
+should give you access to the complete manual.
=== added directory 'openlp'
=== added file 'openlp.py'
--- openlp.py 1970-01-01 00:00:00 +0000
+++ openlp.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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
+import multiprocessing
+
+from openlp.core.common import is_win, is_macosx
+from openlp.core import main
+
+
+if __name__ == '__main__':
+ """
+ Instantiate and run the application.
+ """
+ # Add support for using multiprocessing from frozen Windows executable (built using PyInstaller),
+ # see https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support
+ if is_win():
+ multiprocessing.freeze_support()
+ # Mac OS X passes arguments like '-psn_XXXX' to the application. This argument is actually a process serial number.
+ # However, this causes a conflict with other OpenLP arguments. Since we do not use this argument we can delete it
+ # to avoid any potential conflicts.
+ if is_macosx():
+ sys.argv = [x for x in sys.argv if not x.startswith('-psn')]
+ main()
=== added file 'openlp/.version'
--- openlp/.version 1970-01-01 00:00:00 +0000
+++ openlp/.version 2016-04-05 20:22:40 +0000
@@ -0,0 +1,1 @@
+2.4
=== added file 'openlp/__init__.py'
--- openlp/__init__.py 1970-01-01 00:00:00 +0000
+++ openlp/__init__.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`openlp` module contains all the project produced OpenLP functionality
+"""
+
+from openlp import core, plugins
+
+__all__ = ['core', 'plugins']
=== added directory 'openlp/core'
=== added file 'openlp/core/__init__.py'
--- openlp/core/__init__.py 1970-01-01 00:00:00 +0000
+++ openlp/core/__init__.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,391 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+
+"""
+The :mod:`core` module provides all core application functions
+
+All the core functions of the OpenLP application including the GUI, settings,
+logging and a plugin framework are contained within the openlp.core module.
+"""
+
+import argparse
+import logging
+import os
+import shutil
+import sys
+import time
+from traceback import format_exception
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
+ check_directory_exists, is_macosx, is_win, translate
+from openlp.core.common.versionchecker import VersionThread, get_application_version
+from openlp.core.lib import ScreenList
+from openlp.core.resources import qInitResources
+from openlp.core.ui import SplashScreen
+from openlp.core.ui.exceptionform import ExceptionForm
+from openlp.core.ui.firsttimeform import FirstTimeForm
+from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
+from openlp.core.ui.mainwindow import MainWindow
+
+__all__ = ['OpenLP', 'main']
+
+
+log = logging.getLogger()
+
+WIN_REPAIR_STYLESHEET = """
+QMainWindow::separator
+{
+ border: none;
+}
+
+QDockWidget::title
+{
+ border: 1px solid palette(dark);
+ padding-left: 5px;
+ padding-top: 2px;
+ margin: 1px 0;
+}
+
+QToolBar
+{
+ border: none;
+ margin: 0;
+ padding: 0;
+}
+"""
+
+
+class OpenLP(OpenLPMixin, QtWidgets.QApplication):
+ """
+ The core application class. This class inherits from Qt's QApplication
+ class in order to provide the core of the application.
+ """
+
+ args = []
+
+ def exec(self):
+ """
+ Override exec method to allow the shared memory to be released on exit
+ """
+ self.is_event_loop_active = True
+ result = QtWidgets.QApplication.exec()
+ self.shared_memory.detach()
+ return result
+
+ def run(self, args):
+ """
+ Run the OpenLP application.
+
+ :param args: Some Args
+ """
+ self.is_event_loop_active = False
+ # On Windows, the args passed into the constructor are ignored. Not very handy, so set the ones we want to use.
+ # On Linux and FreeBSD, in order to set the WM_CLASS property for X11, we pass "OpenLP" in as a command line
+ # argument. This interferes with files being passed in as command line arguments, so we remove it from the list.
+ if 'OpenLP' in args:
+ args.remove('OpenLP')
+ self.args.extend(args)
+ # Decide how many screens we have and their size
+ screens = ScreenList.create(self.desktop())
+ # First time checks in settings
+ has_run_wizard = Settings().value('core/has run wizard')
+ if not has_run_wizard:
+ ftw = FirstTimeForm()
+ ftw.initialize(screens)
+ if ftw.exec() == QtWidgets.QDialog.Accepted:
+ Settings().setValue('core/has run wizard', True)
+ elif ftw.was_cancelled:
+ QtCore.QCoreApplication.exit()
+ sys.exit()
+ # Correct stylesheet bugs
+ application_stylesheet = ''
+ if not Settings().value('advanced/alternate rows'):
+ base_color = self.palette().color(QtGui.QPalette.Active, QtGui.QPalette.Base)
+ alternate_rows_repair_stylesheet = \
+ 'QTableWidget, QListWidget, QTreeWidget {alternate-background-color: ' + base_color.name() + ';}\n'
+ application_stylesheet += alternate_rows_repair_stylesheet
+ if is_win():
+ application_stylesheet += WIN_REPAIR_STYLESHEET
+ if application_stylesheet:
+ self.setStyleSheet(application_stylesheet)
+ show_splash = Settings().value('core/show splash')
+ if show_splash:
+ self.splash = SplashScreen()
+ self.splash.show()
+ # make sure Qt really display the splash screen
+ self.processEvents()
+ # Check if OpenLP has been upgrade and if a backup of data should be created
+ self.backup_on_upgrade(has_run_wizard)
+ # start the main app window
+ self.main_window = MainWindow()
+ Registry().execute('bootstrap_initialise')
+ Registry().execute('bootstrap_post_set_up')
+ Registry().initialise = False
+ self.main_window.show()
+ if show_splash:
+ # now kill the splashscreen
+ self.splash.finish(self.main_window)
+ log.debug('Splashscreen closed')
+ # make sure Qt really display the splash screen
+ self.processEvents()
+ self.main_window.repaint()
+ self.processEvents()
+ if not has_run_wizard:
+ self.main_window.first_time()
+ # update_check = Settings().value('core/update check')
+ # if update_check:
+ # version = VersionThread(self.main_window)
+ # version.start()
+ self.main_window.is_display_blank()
+ self.main_window.app_startup()
+ return self.exec()
+
+ def is_already_running(self):
+ """
+ Look to see if OpenLP is already running and ask if a 2nd instance is to be started.
+ """
+ self.shared_memory = QtCore.QSharedMemory('OpenLP')
+ if self.shared_memory.attach():
+ status = QtWidgets.QMessageBox.critical(None, UiStrings().Error, UiStrings().OpenLPStart,
+ QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
+ QtWidgets.QMessageBox.No))
+ if status == QtWidgets.QMessageBox.No:
+ return True
+ return False
+ else:
+ self.shared_memory.create(1)
+ return False
+
+ def hook_exception(self, exc_type, value, traceback):
+ """
+ Add an exception hook so that any uncaught exceptions are displayed in this window rather than somewhere where
+ users cannot see it and cannot report when we encounter these problems.
+
+ :param exc_type: The class of exception.
+ :param value: The actual exception object.
+ :param traceback: A traceback object with the details of where the exception occurred.
+ """
+ # We can't log.exception here because the last exception no longer exists, we're actually busy handling it.
+ log.critical(''.join(format_exception(exc_type, value, traceback)))
+ if not hasattr(self, 'exception_form'):
+ self.exception_form = ExceptionForm()
+ self.exception_form.exception_text_edit.setPlainText(''.join(format_exception(exc_type, value, traceback)))
+ self.set_normal_cursor()
+ self.exception_form.exec()
+
+ def backup_on_upgrade(self, has_run_wizard):
+ """
+ Check if OpenLP has been upgraded, and ask if a backup of data should be made
+
+ :param has_run_wizard: OpenLP has been run before
+ """
+ data_version = Settings().value('core/application version')
+ openlp_version = get_application_version()['version']
+ # New installation, no need to create backup
+ if not has_run_wizard:
+ Settings().setValue('core/application version', openlp_version)
+ # If data_version is different from the current version ask if we should backup the data folder
+ elif data_version != openlp_version:
+ if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
+ translate('OpenLP', 'OpenLP has been upgraded, do you want to create '
+ 'a backup of OpenLPs data folder?'),
+ QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
+ QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
+ # Create copy of data folder
+ data_folder_path = AppLocation.get_data_path()
+ timestamp = time.strftime("%Y%m%d-%H%M%S")
+ data_folder_backup_path = data_folder_path + '-' + timestamp
+ try:
+ shutil.copytree(data_folder_path, data_folder_backup_path)
+ except OSError:
+ QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'),
+ translate('OpenLP', 'Backup of the data folder failed!'))
+ return
+ QtWidgets.QMessageBox.information(None, translate('OpenLP', 'Backup'),
+ translate('OpenLP',
+ 'A backup of the data folder has been created at %s')
+ % data_folder_backup_path)
+ # Update the version in the settings
+ Settings().setValue('core/application version', openlp_version)
+
+ def process_events(self):
+ """
+ Wrapper to make ProcessEvents visible and named correctly
+ """
+ self.processEvents()
+
+ def set_busy_cursor(self):
+ """
+ Sets the Busy Cursor for the Application
+ """
+ self.setOverrideCursor(QtCore.Qt.BusyCursor)
+ self.processEvents()
+
+ def set_normal_cursor(self):
+ """
+ Sets the Normal Cursor for the Application
+ """
+ self.restoreOverrideCursor()
+ self.processEvents()
+
+ def event(self, event):
+ """
+ Enables platform specific event handling i.e. direct file opening on OS X
+
+ :param event: The event
+ """
+ if event.type() == QtCore.QEvent.FileOpen:
+ file_name = event.file()
+ log.debug('Got open file event for %s!', file_name)
+ self.args.insert(0, file_name)
+ return True
+ # Mac OS X should restore app window when user clicked on the OpenLP icon
+ # in the Dock bar. However, OpenLP consists of multiple windows and this
+ # does not work. This workaround fixes that.
+ # The main OpenLP window is restored when it was previously minimized.
+ elif event.type() == QtCore.QEvent.ApplicationActivate:
+ if is_macosx() and hasattr(self, 'main_window'):
+ if self.main_window.isMinimized():
+ # Copied from QWidget.setWindowState() docs on how to restore and activate a minimized window
+ # while preserving its maximized and/or full-screen state.
+ self.main_window.setWindowState(self.main_window.windowState() & ~QtCore.Qt.WindowMinimized |
+ QtCore.Qt.WindowActive)
+ return True
+ return QtWidgets.QApplication.event(self, event)
+
+
+def parse_options(args):
+ """
+ Parse the command line arguments
+
+ :param args: list of command line arguments
+ :return: a tuple of parsed options of type optparse.Value and a list of remaining argsZ
+ """
+ # Set up command line options.
+ parser = argparse.ArgumentParser(prog='openlp.py')
+ parser.add_argument('-e', '--no-error-form', dest='no_error_form', action='store_true',
+ help='Disable the error notification form.')
+ parser.add_argument('-l', '--log-level', dest='loglevel', default='warning', metavar='LEVEL',
+ help='Set logging to LEVEL level. Valid values are "debug", "info", "warning".')
+ parser.add_argument('-p', '--portable', dest='portable', action='store_true',
+ help='Specify if this should be run as a portable app, '
+ 'off a USB flash drive (not implemented).')
+ parser.add_argument('-d', '--dev-version', dest='dev_version', action='store_true',
+ help='Ignore the version file and pull the version directly from Bazaar')
+ parser.add_argument('-s', '--style', dest='style', help='Set the Qt5 style (passed directly to Qt5).')
+ parser.add_argument('rargs', nargs='?', default=[])
+ # Parse command line options and deal with them. Use args supplied pragmatically if possible.
+ return parser.parse_args(args) if args else parser.parse_args()
+
+
+def set_up_logging(log_path):
+ """
+ Setup our logging using log_path
+
+ :param log_path: the path
+ """
+ check_directory_exists(log_path, True)
+ filename = os.path.join(log_path, 'openlp.log')
+ logfile = logging.FileHandler(filename, 'w', encoding="UTF-8")
+ logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
+ log.addHandler(logfile)
+ if log.isEnabledFor(logging.DEBUG):
+ print('Logging to: %s' % filename)
+
+
+def main(args=None):
+ """
+ The main function which parses command line options and then runs
+
+ :param args: Some args
+ """
+ args = parse_options(args)
+ qt_args = []
+ if args and args.loglevel.lower() in ['d', 'debug']:
+ log.setLevel(logging.DEBUG)
+ elif args and args.loglevel.lower() in ['w', 'warning']:
+ log.setLevel(logging.WARNING)
+ else:
+ log.setLevel(logging.INFO)
+ if args and args.style:
+ qt_args.extend(['-style', args.style])
+ # Throw the rest of the arguments at Qt, just in case.
+ qt_args.extend(args.rargs)
+ # Bug #1018855: Set the WM_CLASS property in X11
+ if not is_win() and not is_macosx():
+ qt_args.append('OpenLP')
+ # Initialise the resources
+ qInitResources()
+ # Now create and actually run the application.
+ application = OpenLP(qt_args)
+ application.setOrganizationName('OpenLP')
+ application.setOrganizationDomain('openlp.org')
+ application.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True)
+ if args and args.portable:
+ application.setApplicationName('OpenLPPortable')
+ Settings.setDefaultFormat(Settings.IniFormat)
+ # Get location OpenLPPortable.ini
+ application_path = AppLocation.get_directory(AppLocation.AppDir)
+ set_up_logging(os.path.abspath(os.path.join(application_path, '..', '..', 'Other')))
+ log.info('Running portable')
+ portable_settings_file = os.path.abspath(os.path.join(application_path, '..', '..', 'Data', 'OpenLP.ini'))
+ # Make this our settings file
+ log.info('INI file: %s', portable_settings_file)
+ Settings.set_filename(portable_settings_file)
+ portable_settings = Settings()
+ # Set our data path
+ data_path = os.path.abspath(os.path.join(application_path, '..', '..', 'Data',))
+ log.info('Data path: %s', data_path)
+ # Point to our data path
+ portable_settings.setValue('advanced/data path', data_path)
+ portable_settings.setValue('advanced/is portable', True)
+ portable_settings.sync()
+ else:
+ application.setApplicationName('OpenLP')
+ set_up_logging(AppLocation.get_directory(AppLocation.CacheDir))
+ Registry.create()
+ Registry().register('application', application)
+ application.setApplicationVersion(get_application_version()['version'])
+ # Instance check
+ if application.is_already_running():
+ sys.exit()
+ # Remove/convert obsolete settings.
+ Settings().remove_obsolete_settings()
+ # First time checks in settings
+ if not Settings().value('core/has run wizard'):
+ if not FirstTimeLanguageForm().exec():
+ # if cancel then stop processing
+ sys.exit()
+ # i18n Set Language
+ language = LanguageManager.get_language()
+ application_translator, default_translator = LanguageManager.get_translator(language)
+ if not application_translator.isEmpty():
+ application.installTranslator(application_translator)
+ if not default_translator.isEmpty():
+ application.installTranslator(default_translator)
+ else:
+ log.debug('Could not find default_translator.')
+ if args and not args.no_error_form:
+ sys.excepthook = application.hook_exception
+ sys.exit(application.run(qt_args))
=== added directory 'openlp/core/common'
=== added file 'openlp/core/common/__init__.py'
--- openlp/core/common/__init__.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/__init__.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,373 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`common` module contains most of the components and libraries that make
+OpenLP work.
+"""
+import hashlib
+import logging
+import os
+import re
+import sys
+import traceback
+from ipaddress import IPv4Address, IPv6Address, AddressValueError
+from shutil import which
+
+from PyQt5 import QtCore, QtGui
+from PyQt5.QtCore import QCryptographicHash as QHash
+
+log = logging.getLogger(__name__ + '.__init__')
+
+
+FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
+SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
+CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
+INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
+IMAGES_FILTER = None
+
+
+def trace_error_handler(logger):
+ """
+ Log the calling path of an exception
+
+ :param logger: logger to use so traceback is logged to correct class
+ """
+ log_string = "OpenLP Error trace"
+ for tb in traceback.extract_stack():
+ log_string = '%s\n File %s at line %d \n\t called %s' % (log_string, tb[0], tb[1], tb[3])
+ logger.error(log_string)
+
+
+def check_directory_exists(directory, do_not_log=False):
+ """
+ Check a theme directory exists and if not create it
+
+ :param directory: The directory to make sure exists
+ :param do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
+ """
+ if not do_not_log:
+ log.debug('check_directory_exists %s' % directory)
+ try:
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ except IOError as e:
+ if not do_not_log:
+ log.exception('failed to check if directory exists or create directory')
+
+
+def get_frozen_path(frozen_option, non_frozen_option):
+ """
+ Return a path based on the system status.
+
+ :param frozen_option:
+ :param non_frozen_option:
+ """
+ if hasattr(sys, 'frozen') and sys.frozen == 1:
+ return frozen_option
+ return non_frozen_option
+
+
+class ThemeLevel(object):
+ """
+ Provides an enumeration for the level a theme applies to
+ """
+ Global = 1
+ Service = 2
+ Song = 3
+
+
+def translate(context, text, comment=None, qt_translate=QtCore.QCoreApplication.translate):
+ """
+ A special shortcut method to wrap around the Qt5 translation functions. This abstracts the translation procedure so
+ that we can change it if at a later date if necessary, without having to redo the whole of OpenLP.
+
+ :param context: The translation context, used to give each string a context or a namespace.
+ :param text: The text to put into the translation tables for translation.
+ :param comment: An identifying string for when the same text is used in different roles within the same context.
+ :param qt_translate:
+ """
+ return qt_translate(context, text, comment)
+
+
+class SlideLimits(object):
+ """
+ Provides an enumeration for behaviour of OpenLP at the end limits of each service item when pressing the up/down
+ arrow keys
+ """
+ End = 1
+ Wrap = 2
+ Next = 3
+
+
+def de_hump(name):
+ """
+ Change any Camel Case string to python string
+ """
+ sub_name = FIRST_CAMEL_REGEX.sub(r'\1_\2', name)
+ return SECOND_CAMEL_REGEX.sub(r'\1_\2', sub_name).lower()
+
+
+def is_win():
+ """
+ Returns true if running on a system with a nt kernel e.g. Windows, Wine
+
+ :return: True if system is running a nt kernel false otherwise
+ """
+ return os.name.startswith('nt')
+
+
+def is_macosx():
+ """
+ Returns true if running on a system with a darwin kernel e.g. Mac OS X
+
+ :return: True if system is running a darwin kernel false otherwise
+ """
+ return sys.platform.startswith('darwin')
+
+
+def is_linux():
+ """
+ Returns true if running on a system with a linux kernel e.g. Ubuntu, Debian, etc
+
+ :return: True if system is running a linux kernel false otherwise
+ """
+ return sys.platform.startswith('linux')
+
+
+def verify_ipv4(addr):
+ """
+ Validate an IPv4 address
+
+ :param addr: Address to validate
+ :returns: bool
+ """
+ try:
+ valid = IPv4Address(addr)
+ return True
+ except AddressValueError:
+ return False
+
+
+def verify_ipv6(addr):
+ """
+ Validate an IPv6 address
+
+ :param addr: Address to validate
+ :returns: bool
+ """
+ try:
+ valid = IPv6Address(addr)
+ return True
+ except AddressValueError:
+ return False
+
+
+def verify_ip_address(addr):
+ """
+ Validate an IP address as either IPv4 or IPv6
+
+ :param addr: Address to validate
+ :returns: bool
+ """
+ return True if verify_ipv4(addr) else verify_ipv6(addr)
+
+
+def md5_hash(salt, data=None):
+ """
+ Returns the hashed output of md5sum on salt,data
+ using Python3 hashlib
+
+ :param salt: Initial salt
+ :param data: OPTIONAL Data to hash
+ :returns: str
+ """
+ log.debug('md5_hash(salt="%s")' % salt)
+ hash_obj = hashlib.new('md5')
+ hash_obj.update(salt)
+ if data:
+ hash_obj.update(data)
+ hash_value = hash_obj.hexdigest()
+ log.debug('md5_hash() returning "%s"' % hash_value)
+ return hash_value
+
+
+def qmd5_hash(salt, data=None):
+ """
+ Returns the hashed output of MD5Sum on salt, data
+ using PyQt5.QCryptographicHash.
+
+ :param salt: Initial salt
+ :param data: OPTIONAL Data to hash
+ :returns: str
+ """
+ log.debug('qmd5_hash(salt="%s"' % salt)
+ hash_obj = QHash(QHash.Md5)
+ hash_obj.addData(salt)
+ hash_obj.addData(data)
+ hash_value = hash_obj.result().toHex()
+ log.debug('qmd5_hash() returning "%s"' % hash_value)
+ return hash_value.data()
+
+
+def clean_button_text(button_text):
+ """
+ Clean the & and other characters out of button text
+
+ :param button_text: The text to clean
+ """
+ return button_text.replace('&', '').replace('< ', '').replace(' >', '')
+
+
+from .openlpmixin import OpenLPMixin
+from .registry import Registry
+from .registrymixin import RegistryMixin
+from .registryproperties import RegistryProperties
+from .uistrings import UiStrings
+from .settings import Settings
+from .applocation import AppLocation
+from .actions import ActionList
+from .languagemanager import LanguageManager
+
+
+def add_actions(target, actions):
+ """
+ Adds multiple actions to a menu or toolbar in one command.
+
+ :param target: The menu or toolbar to add actions to
+ :param actions: The actions to be added. An action consisting of the keyword ``None``
+ will result in a separator being inserted into the target.
+ """
+ for action in actions:
+ if action is None:
+ target.addSeparator()
+ else:
+ target.addAction(action)
+
+
+def get_uno_command(connection_type='pipe'):
+ """
+ Returns the UNO command to launch an libreoffice.org instance.
+ """
+ for command in ['libreoffice', 'soffice']:
+ if which(command):
+ break
+ else:
+ raise FileNotFoundError('Command not found')
+
+ OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard'
+ if connection_type == 'pipe':
+ CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"'
+ else:
+ CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"'
+ return '%s %s %s' % (command, OPTIONS, CONNECTION)
+
+
+def get_uno_instance(resolver, connection_type='pipe'):
+ """
+ Returns a running libreoffice.org instance.
+
+ :param resolver: The UNO resolver to use to find a running instance.
+ """
+ log.debug('get UNO Desktop Openoffice - resolve')
+ if connection_type == 'pipe':
+ return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
+ else:
+ return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
+
+
+def get_filesystem_encoding():
+ """
+ Returns the name of the encoding used to convert Unicode filenames into system file names.
+ """
+ encoding = sys.getfilesystemencoding()
+ if encoding is None:
+ encoding = sys.getdefaultencoding()
+ return encoding
+
+
+def split_filename(path):
+ """
+ Return a list of the parts in a given path.
+ """
+ path = os.path.abspath(path)
+ if not os.path.isfile(path):
+ return path, ''
+ else:
+ return os.path.split(path)
+
+
+def delete_file(file_path_name):
+ """
+ Deletes a file from the system.
+
+ :param file_path_name: The file, including path, to delete.
+ """
+ if not file_path_name:
+ return False
+ try:
+ if os.path.exists(file_path_name):
+ os.remove(file_path_name)
+ return True
+ except (IOError, OSError):
+ log.exception("Unable to delete file %s" % file_path_name)
+ return False
+
+
+def get_images_filter():
+ """
+ Returns a filter string for a file dialog containing all the supported image formats.
+ """
+ global IMAGES_FILTER
+ if not IMAGES_FILTER:
+ log.debug('Generating images filter.')
+ formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
+ visible_formats = '(*.%s)' % '; *.'.join(formats)
+ actual_formats = '(*.%s)' % ' *.'.join(formats)
+ IMAGES_FILTER = '%s %s %s' % (translate('OpenLP', 'Image Files'), visible_formats, actual_formats)
+ return IMAGES_FILTER
+
+
+def is_not_image_file(file_name):
+ """
+ Validate that the file is not an image file.
+
+ :param file_name: File name to be checked.
+ """
+ if not file_name:
+ return True
+ else:
+ formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
+ file_part, file_extension = os.path.splitext(str(file_name))
+ if file_extension[1:].lower() in formats and os.path.exists(file_name):
+ return False
+ return True
+
+
+def clean_filename(filename):
+ """
+ Removes invalid characters from the given ``filename``.
+
+ :param filename: The "dirty" file name to clean.
+ """
+ if not isinstance(filename, str):
+ filename = str(filename, 'utf-8')
+ return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
=== added file 'openlp/core/common/actions.py'
--- openlp/core/common/actions.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/actions.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,388 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`~openlp.core.utils.actions` module provides action list classes used
+by the shortcuts system.
+"""
+import logging
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Settings
+
+
+log = logging.getLogger(__name__)
+
+
+class ActionCategory(object):
+ """
+ The :class:`~openlp.core.utils.ActionCategory` class encapsulates a category for the
+ :class:`~openlp.core.utils.CategoryList` class.
+ """
+ def __init__(self, name, weight=0):
+ """
+ Constructor
+ """
+ self.name = name
+ self.weight = weight
+ self.actions = CategoryActionList()
+
+
+class CategoryActionList(object):
+ """
+ The :class:`~openlp.core.utils.CategoryActionList` class provides a sorted list of actions within a category.
+ """
+ def __init__(self):
+ """
+ Constructor
+ """
+ self.index = 0
+ self.actions = []
+
+ def __contains__(self, key):
+ """
+ Implement the __contains__() method to make this class a dictionary type
+ """
+ for weight, action in self.actions:
+ if action == key:
+ return True
+ return False
+
+ def __len__(self):
+ """
+ Implement the __len__() method to make this class a dictionary type
+ """
+ return len(self.actions)
+
+ def __iter__(self):
+ """
+ Implement the __getitem__() method to make this class iterable
+ """
+ return self
+
+ def __next__(self):
+ """
+ Python 3 "next" method.
+ """
+ if self.index >= len(self.actions):
+ self.index = 0
+ raise StopIteration
+ else:
+ self.index += 1
+ return self.actions[self.index - 1][1]
+
+ def append(self, action):
+ """
+ Append an action
+ """
+ weight = 0
+ if self.actions:
+ weight = self.actions[-1][0] + 1
+ self.add(action, weight)
+
+ def add(self, action, weight=0):
+ """
+ Add an action.
+ """
+ self.actions.append((weight, action))
+ self.actions.sort(key=lambda act: act[0])
+
+ def remove(self, action):
+ """
+ Remove an action
+ """
+ for item in self.actions:
+ if item[1] == action:
+ self.actions.remove(item)
+ return
+ raise ValueError('Action "%s" does not exist.' % action)
+
+
+class CategoryList(object):
+ """
+ The :class:`~openlp.core.utils.CategoryList` class encapsulates a category list for the
+ :class:`~openlp.core.utils.ActionList` class and provides an iterator interface for walking through the list of
+ actions in this category.
+ """
+
+ def __init__(self):
+ """
+ Constructor
+ """
+ self.index = 0
+ self.categories = []
+
+ def __getitem__(self, key):
+ """
+ Implement the __getitem__() method to make this class like a dictionary
+ """
+ for category in self.categories:
+ if category.name == key:
+ return category
+ raise KeyError('Category "%s" does not exist.' % key)
+
+ def __len__(self):
+ """
+ Implement the __len__() method to make this class like a dictionary
+ """
+ return len(self.categories)
+
+ def __iter__(self):
+ """
+ Implement the __iter__() method to make this class like a dictionary
+ """
+ return self
+
+ def __next__(self):
+ """
+ Python 3 "next" method for iterator.
+ """
+ if self.index >= len(self.categories):
+ self.index = 0
+ raise StopIteration
+ else:
+ self.index += 1
+ return self.categories[self.index - 1]
+
+ def __contains__(self, key):
+ """
+ Implement the __contains__() method to make this class like a dictionary
+ """
+ for category in self.categories:
+ if category.name == key:
+ return True
+ return False
+
+ def append(self, name, actions=None):
+ """
+ Append a category
+ """
+ weight = 0
+ if self.categories:
+ weight = self.categories[-1].weight + 1
+ self.add(name, weight, actions)
+
+ def add(self, name, weight=0, actions=None):
+ """
+ Add a category
+ """
+ category = ActionCategory(name, weight)
+ if actions:
+ for action in actions:
+ if isinstance(action, tuple):
+ category.actions.add(action[0], action[1])
+ else:
+ category.actions.append(action)
+ self.categories.append(category)
+ self.categories.sort(key=lambda cat: cat.weight)
+
+ def remove(self, name):
+ """
+ Remove a category
+ """
+ for category in self.categories:
+ if category.name == name:
+ self.categories.remove(category)
+ return
+ raise ValueError('Category "%s" does not exist.' % name)
+
+
+class ActionList(object):
+ """
+ The :class:`~openlp.core.utils.ActionList` class contains a list of menu actions and categories associated with
+ those actions. Each category also has a weight by which it is sorted when iterating through the list of actions or
+ categories.
+ """
+ instance = None
+ shortcut_map = {}
+
+ def __init__(self):
+ """
+ Constructor
+ """
+ self.categories = CategoryList()
+
+ @staticmethod
+ def get_instance():
+ """
+ Get the instance of this class.
+ """
+ if ActionList.instance is None:
+ ActionList.instance = ActionList()
+ return ActionList.instance
+
+ def add_action(self, action, category=None, weight=None):
+ """
+ Add an action to the list of actions.
+
+ **Note**: The action's objectName must be set when you want to add it!
+
+ :param action: The action to add (QAction). **Note**, the action must not have an empty ``objectName``.
+ :param category: The category this action belongs to. The category has to be a python string. . **Note**,
+ if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
+ if they are added, it is possible to avoid assigning shortcuts twice, which is important.
+ :param weight: The weight specifies how important a category is. However, this only has an impact on the order
+ the categories are displayed.
+ """
+ if category not in self.categories:
+ self.categories.append(category)
+ settings = Settings()
+ settings.beginGroup('shortcuts')
+ # Get the default shortcut from the config.
+ action.default_shortcuts = settings.get_default_value(action.objectName())
+ if weight is None:
+ self.categories[category].actions.append(action)
+ else:
+ self.categories[category].actions.add(action, weight)
+ # Load the shortcut from the config.
+ shortcuts = settings.value(action.objectName())
+ settings.endGroup()
+ if not shortcuts:
+ action.setShortcuts([])
+ return
+ # We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
+ # which is only done when we convert the strings in this way (QKeySequencet -> uncode).
+ shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
+ # Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
+ # after removing the (initial) primary shortcut due to conflicts.
+ if len(shortcuts) == 2:
+ existing_actions = ActionList.shortcut_map.get(shortcuts[1], [])
+ # Check for conflicts with other actions considering the shortcut context.
+ if self._is_shortcut_available(existing_actions, action):
+ actions = ActionList.shortcut_map.get(shortcuts[1], [])
+ actions.append(action)
+ ActionList.shortcut_map[shortcuts[1]] = actions
+ else:
+ log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
+ (shortcuts[1], action.objectName()))
+ shortcuts.remove(shortcuts[1])
+ # Check the primary shortcut.
+ existing_actions = ActionList.shortcut_map.get(shortcuts[0], [])
+ # Check for conflicts with other actions considering the shortcut context.
+ if self._is_shortcut_available(existing_actions, action):
+ actions = ActionList.shortcut_map.get(shortcuts[0], [])
+ actions.append(action)
+ ActionList.shortcut_map[shortcuts[0]] = actions
+ else:
+ log.warning('Shortcut "%s" is removed from "%s" because another action already uses this shortcut.' %
+ (shortcuts[0], action.objectName()))
+ shortcuts.remove(shortcuts[0])
+ action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts])
+
+ def remove_action(self, action, category=None):
+ """
+ This removes an action from its category. Empty categories are automatically removed.
+
+ :param action: The ``QAction`` object to be removed.
+ :param category: The name (unicode string) of the category, which contains the action. Defaults to None.
+ """
+ if category not in self.categories:
+ return
+ self.categories[category].actions.remove(action)
+ # Remove empty categories.
+ if not self.categories[category].actions:
+ self.categories.remove(category)
+ shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
+ for shortcut in shortcuts:
+ # Remove action from the list of actions which are using this shortcut.
+ ActionList.shortcut_map[shortcut].remove(action)
+ # Remove empty entries.
+ if not ActionList.shortcut_map[shortcut]:
+ del ActionList.shortcut_map[shortcut]
+
+ def add_category(self, name, weight):
+ """
+ Add an empty category to the list of categories. This is only convenient for categories with a given weight.
+
+ :param name: The category's name.
+ :param weight: The category's weight (int).
+ """
+ if name in self.categories:
+ # Only change the weight and resort the categories again.
+ for category in self.categories:
+ if category.name == name:
+ category.weight = weight
+ self.categories.categories.sort(key=lambda cat: cat.weight)
+ return
+ self.categories.add(name, weight)
+
+ def update_shortcut_map(self, action, old_shortcuts):
+ """
+ Remove the action for the given ``old_shortcuts`` from the ``shortcut_map`` to ensure its up-to-dateness.
+ **Note**: The new action's shortcuts **must** be assigned to the given ``action`` **before** calling this
+ method.
+
+ :param action: The action whose shortcuts are supposed to be updated in the ``shortcut_map``.
+ :param old_shortcuts: A list of unicode key sequences.
+ """
+ for old_shortcut in old_shortcuts:
+ # Remove action from the list of actions which are using this shortcut.
+ ActionList.shortcut_map[old_shortcut].remove(action)
+ # Remove empty entries.
+ if not ActionList.shortcut_map[old_shortcut]:
+ del ActionList.shortcut_map[old_shortcut]
+ new_shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
+ # Add the new shortcuts to the map.
+ for new_shortcut in new_shortcuts:
+ existing_actions = ActionList.shortcut_map.get(new_shortcut, [])
+ existing_actions.append(action)
+ ActionList.shortcut_map[new_shortcut] = existing_actions
+
+ def _is_shortcut_available(self, existing_actions, action):
+ """
+ Checks if the given ``action`` may use its assigned shortcut(s) or not. Returns ``True`` or ``False.
+
+ :param existing_actions: A list of actions which already use a particular shortcut.
+ :param action: The action which wants to use a particular shortcut.
+ """
+ global_context = action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]
+ affected_actions = []
+ if global_context:
+ affected_actions = [a for a in self.get_all_child_objects(action.parent()) if isinstance(a,
+ QtWidgets.QAction)]
+ for existing_action in existing_actions:
+ if action is existing_action:
+ continue
+ if existing_action in affected_actions:
+ return False
+ if existing_action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]:
+ return False
+ elif action in self.get_all_child_objects(existing_action.parent()):
+ return False
+ return True
+
+ def get_all_child_objects(self, qobject):
+ """
+ Goes recursively through the children of ``qobject`` and returns a list of all child objects.
+ """
+ children = qobject.children()
+ # Append the children's children.
+ children.extend(list(map(self.get_all_child_objects, children)))
+ return children
+
+
+class CategoryOrder(object):
+ """
+ An enumeration class for category weights.
+ """
+ standard_menu = -20
+ standard_toolbar = -10
=== added file 'openlp/core/common/applocation.py'
--- openlp/core/common/applocation.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/applocation.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`openlp.core.common.applocation` module provides an utility for OpenLP receiving the data path etc.
+"""
+import logging
+import os
+import sys
+
+from openlp.core.common import Settings, is_win, is_macosx
+
+
+if not is_win() and not is_macosx():
+ try:
+ from xdg import BaseDirectory
+ XDG_BASE_AVAILABLE = True
+ except ImportError:
+ XDG_BASE_AVAILABLE = False
+
+import openlp
+from openlp.core.common import check_directory_exists, get_frozen_path
+
+
+log = logging.getLogger(__name__)
+
+
+class AppLocation(object):
+ """
+ The :class:`AppLocation` class is a static class which retrieves a directory based on the directory type.
+ """
+ AppDir = 1
+ DataDir = 2
+ PluginsDir = 3
+ VersionDir = 4
+ CacheDir = 5
+ LanguageDir = 6
+
+ # Base path where data/config/cache dir is located
+ BaseDir = None
+
+ @staticmethod
+ def get_directory(dir_type=AppDir):
+ """
+ Return the appropriate directory according to the directory type.
+
+ :param dir_type: The directory type you want, for instance the data directory. Default *AppLocation.AppDir*
+ """
+ if dir_type == AppLocation.AppDir:
+ return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
+ elif dir_type == AppLocation.PluginsDir:
+ app_path = os.path.abspath(os.path.dirname(sys.argv[0]))
+ return get_frozen_path(os.path.join(app_path, 'plugins'),
+ os.path.join(os.path.dirname(openlp.__file__), 'plugins'))
+ elif dir_type == AppLocation.VersionDir:
+ return get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), os.path.dirname(openlp.__file__))
+ elif dir_type == AppLocation.LanguageDir:
+ app_path = get_frozen_path(os.path.abspath(os.path.dirname(sys.argv[0])), _get_os_dir_path(dir_type))
+ return os.path.join(app_path, 'i18n')
+ elif dir_type == AppLocation.DataDir and AppLocation.BaseDir:
+ return os.path.join(AppLocation.BaseDir, 'data')
+ else:
+ return _get_os_dir_path(dir_type)
+
+ @staticmethod
+ def get_data_path():
+ """
+ Return the path OpenLP stores all its data under.
+ """
+ # Check if we have a different data location.
+ if Settings().contains('advanced/data path'):
+ path = Settings().value('advanced/data path')
+ else:
+ path = AppLocation.get_directory(AppLocation.DataDir)
+ check_directory_exists(path)
+ return os.path.normpath(path)
+
+ @staticmethod
+ def get_files(section=None, extension=None):
+ """
+ Get a list of files from the data files path.
+
+ :param section: Defaults to *None*. The section of code getting the files - used to load from a section's
+ data subdirectory.
+ :param extension:
+ Defaults to *None*. The extension to search for. For example::
+
+ '.png'
+ """
+ path = AppLocation.get_data_path()
+ if section:
+ path = os.path.join(path, section)
+ try:
+ files = os.listdir(path)
+ except OSError:
+ return []
+ if extension:
+ return [filename for filename in files if extension == os.path.splitext(filename)[1]]
+ else:
+ # no filtering required
+ return files
+
+ @staticmethod
+ def get_section_data_path(section):
+ """
+ Return the path a particular module stores its data under.
+ """
+ data_path = AppLocation.get_data_path()
+ path = os.path.join(data_path, section)
+ check_directory_exists(path)
+ return path
+
+
+def _get_os_dir_path(dir_type):
+ """
+ Return a path based on which OS and environment we are running in.
+ """
+ # If running from source, return the language directory from the source directory
+ if dir_type == AppLocation.LanguageDir:
+ directory = os.path.abspath(os.path.join(os.path.dirname(openlp.__file__), '..', 'resources'))
+ if os.path.exists(directory):
+ return directory
+ if is_win():
+ if dir_type == AppLocation.DataDir:
+ return os.path.join(str(os.getenv('APPDATA')), 'openlp', 'data')
+ elif dir_type == AppLocation.LanguageDir:
+ return os.path.dirname(openlp.__file__)
+ return os.path.join(str(os.getenv('APPDATA')), 'openlp')
+ elif is_macosx():
+ if dir_type == AppLocation.DataDir:
+ return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp', 'Data')
+ elif dir_type == AppLocation.LanguageDir:
+ return os.path.dirname(openlp.__file__)
+ return os.path.join(str(os.getenv('HOME')), 'Library', 'Application Support', 'openlp')
+ else:
+ if dir_type == AppLocation.LanguageDir:
+ for prefix in ['/usr/local', '/usr']:
+ directory = os.path.join(prefix, 'share', 'openlp')
+ if os.path.exists(directory):
+ return directory
+ return os.path.join('/usr', 'share', 'openlp')
+ if XDG_BASE_AVAILABLE:
+ if dir_type == AppLocation.DataDir:
+ return os.path.join(str(BaseDirectory.xdg_data_home), 'openlp')
+ elif dir_type == AppLocation.CacheDir:
+ return os.path.join(str(BaseDirectory.xdg_cache_home), 'openlp')
+ if dir_type == AppLocation.DataDir:
+ return os.path.join(str(os.getenv('HOME')), '.openlp', 'data')
+ return os.path.join(str(os.getenv('HOME')), '.openlp')
=== added file 'openlp/core/common/db.py'
--- openlp/core/common/db.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/db.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`db` module provides helper functions for database related methods.
+"""
+import sqlalchemy
+import logging
+
+from copy import deepcopy
+
+log = logging.getLogger(__name__)
+
+
+def drop_column(op, tablename, columnname):
+ drop_columns(op, tablename, [columnname])
+
+
+def drop_columns(op, tablename, columns):
+ """
+ Column dropping functionality for SQLite, as there is no DROP COLUMN support in SQLite
+
+ From https://github.com/klugjohannes/alembic-sqlite
+ """
+
+ # get the db engine and reflect database tables
+ engine = op.get_bind()
+ meta = sqlalchemy.MetaData(bind=engine)
+ meta.reflect()
+
+ # create a select statement from the old table
+ old_table = meta.tables[tablename]
+ select = sqlalchemy.sql.select([c for c in old_table.c if c.name not in columns])
+
+ # get remaining columns without table attribute attached
+ remaining_columns = [deepcopy(c) for c in old_table.columns if c.name not in columns]
+ for column in remaining_columns:
+ column.table = None
+
+ # create a temporary new table
+ new_tablename = '{0}_new'.format(tablename)
+ op.create_table(new_tablename, *remaining_columns)
+ meta.reflect()
+ new_table = meta.tables[new_tablename]
+
+ # copy data from old table
+ insert = sqlalchemy.sql.insert(new_table).from_select([c.name for c in remaining_columns], select)
+ engine.execute(insert)
+
+ # drop the old table and rename the new table to take the old tables
+ # position
+ op.drop_table(tablename)
+ op.rename_table(new_tablename, tablename)
=== added file 'openlp/core/common/languagemanager.py'
--- openlp/core/common/languagemanager.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/languagemanager.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`languagemanager` module provides all the translation settings and language file loading for OpenLP.
+"""
+import locale
+import logging
+import re
+
+from PyQt5 import QtCore, QtWidgets
+
+
+from openlp.core.common import AppLocation, Settings, translate, is_win, is_macosx
+
+log = logging.getLogger(__name__)
+
+ICU_COLLATOR = None
+DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
+
+
+class LanguageManager(object):
+ """
+ Helper for Language selection
+ """
+ __qm_list__ = {}
+ auto_language = False
+
+ @staticmethod
+ def get_translator(language):
+ """
+ Set up a translator to use in this instance of OpenLP
+
+ :param language: The language to load into the translator
+ """
+ if LanguageManager.auto_language:
+ language = QtCore.QLocale.system().name()
+ lang_path = AppLocation.get_directory(AppLocation.LanguageDir)
+ app_translator = QtCore.QTranslator()
+ app_translator.load(language, lang_path)
+ # A translator for buttons and other default strings provided by Qt.
+ if not is_win() and not is_macosx():
+ lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)
+ default_translator = QtCore.QTranslator()
+ default_translator.load('qt_%s' % language, lang_path)
+ return app_translator, default_translator
+
+ @staticmethod
+ def find_qm_files():
+ """
+ Find all available language files in this OpenLP install
+ """
+ log.debug('Translation files: %s', AppLocation.get_directory(AppLocation.LanguageDir))
+ trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir))
+ file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
+ # Remove qm files from the list which start with "qt_".
+ file_names = [file_ for file_ in file_names if not file_.startswith('qt_')]
+ return list(map(trans_dir.filePath, file_names))
+
+ @staticmethod
+ def language_name(qm_file):
+ """
+ Load the language name from a language file
+
+ :param qm_file: The file to obtain the name from
+ """
+ translator = QtCore.QTranslator()
+ translator.load(qm_file)
+ return translator.translate('OpenLP.MainWindow', 'English', 'Please add the name of your language here')
+
+ @staticmethod
+ def get_language():
+ """
+ Retrieve a saved language to use from settings
+ """
+ language = Settings().value('core/language')
+ language = str(language)
+ log.info('Language file: \'%s\' Loaded from conf file' % language)
+ if re.match(r'[[].*[]]', language):
+ LanguageManager.auto_language = True
+ language = re.sub(r'[\[\]]', '', language)
+ return language
+
+ @staticmethod
+ def set_language(action, message=True):
+ """
+ Set the language to translate OpenLP into
+
+ :param action: The language menu option
+ :param message: Display the message option
+ """
+ language = 'en'
+ if action:
+ action_name = str(action.objectName())
+ if action_name == 'autoLanguageItem':
+ LanguageManager.auto_language = True
+ else:
+ LanguageManager.auto_language = False
+ qm_list = LanguageManager.get_qm_list()
+ language = str(qm_list[action_name])
+ if LanguageManager.auto_language:
+ language = '[%s]' % language
+ Settings().setValue('core/language', language)
+ log.info('Language file: \'%s\' written to conf file' % language)
+ if message:
+ QtWidgets.QMessageBox.information(None,
+ translate('OpenLP.LanguageManager', 'Language'),
+ translate('OpenLP.LanguageManager',
+ 'Please restart OpenLP to use your new language setting.'))
+
+ @staticmethod
+ def init_qm_list():
+ """
+ Initialise the list of available translations
+ """
+ LanguageManager.__qm_list__ = {}
+ qm_files = LanguageManager.find_qm_files()
+ for counter, qmf in enumerate(qm_files):
+ reg_ex = QtCore.QRegExp("^.*i18n/(.*).qm")
+ if reg_ex.exactMatch(qmf):
+ name = '%s' % reg_ex.cap(1)
+ LanguageManager.__qm_list__['%#2i %s' % (counter + 1, LanguageManager.language_name(qmf))] = name
+
+ @staticmethod
+ def get_qm_list():
+ """
+ Return the list of available translations
+ """
+ if not LanguageManager.__qm_list__:
+ LanguageManager.init_qm_list()
+ return LanguageManager.__qm_list__
+
+
+def format_time(text, local_time):
+ """
+ Workaround for Python built-in time formatting function time.strftime().
+
+ time.strftime() accepts only ascii characters. This function accepts
+ unicode string and passes individual % placeholders to time.strftime().
+ This ensures only ascii characters are passed to time.strftime().
+
+ :param text: The text to be processed.
+ :param local_time: The time to be used to add to the string. This is a time object
+ """
+
+ def match_formatting(match):
+ """
+ Format the match
+ """
+ return local_time.strftime(match.group())
+
+ return re.sub('\%[a-zA-Z]', match_formatting, text)
+
+
+def get_locale_key(string):
+ """
+ Creates a key for case insensitive, locale aware string sorting.
+
+ :param string: The corresponding string.
+ """
+ string = string.lower()
+ # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
+ global ICU_COLLATOR
+ try:
+ if ICU_COLLATOR is None:
+ import icu
+ language = LanguageManager.get_language()
+ icu_locale = icu.Locale(language)
+ ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
+ return ICU_COLLATOR.getSortKey(string)
+ except:
+ return locale.strxfrm(string).encode()
+
+
+def get_natural_key(string):
+ """
+ Generate a key for locale aware natural string sorting.
+
+ :param string: string to be sorted by
+ Returns a list of string compare keys and integers.
+ """
+ key = DIGITS_OR_NONDIGITS.findall(string)
+ key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
+ # Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
+ # and int.
+ if string and string[0].isdigit():
+ return [b''] + key
+ return key
=== added file 'openlp/core/common/openlpmixin.py'
--- openlp/core/common/openlpmixin.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/openlpmixin.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide Error Handling and login Services
+"""
+import logging
+import inspect
+
+from openlp.core.common import trace_error_handler
+
+DO_NOT_TRACE_EVENTS = ['timerEvent', 'paintEvent', 'drag_enter_event', 'drop_event', 'on_controller_size_changed',
+ 'preview_size_changed', 'resizeEvent']
+
+
+class OpenLPMixin(object):
+ """
+ Base Calling object for OpenLP classes.
+ """
+ def __init__(self, *args, **kwargs):
+ super(OpenLPMixin, self).__init__(*args, **kwargs)
+ self.logger = logging.getLogger("%s.%s" % (self.__module__, self.__class__.__name__))
+ if self.logger.getEffectiveLevel() == logging.DEBUG:
+ for name, m in inspect.getmembers(self, inspect.ismethod):
+ if name not in DO_NOT_TRACE_EVENTS:
+ if not name.startswith("_") and not name.startswith("log"):
+ setattr(self, name, self.logging_wrapper(m, self))
+
+ def logging_wrapper(self, func, parent):
+ """
+ Code to added debug wrapper to work on called functions within a decorated class.
+ """
+ def wrapped(*args, **kwargs):
+ parent.logger.debug("Entering %s" % func.__name__)
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ if parent.logger.getEffectiveLevel() <= logging.ERROR:
+ parent.logger.error('Exception in %s : %s' % (func.__name__, e))
+ raise e
+ return wrapped
+
+ def log_debug(self, message):
+ """
+ Common log debug handler
+ """
+ self.logger.debug(message)
+
+ def log_info(self, message):
+ """
+ Common log info handler
+ """
+ self.logger.info(message)
+
+ def log_error(self, message):
+ """
+ Common log error handler which prints the calling path
+ """
+ trace_error_handler(self.logger)
+ self.logger.error(message)
+
+ def log_exception(self, message):
+ """
+ Common log exception handler which prints the calling path
+ """
+ trace_error_handler(self.logger)
+ self.logger.exception(message)
=== added file 'openlp/core/common/registry.py'
--- openlp/core/common/registry.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/registry.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,147 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide Registry Services
+"""
+import logging
+import sys
+
+from openlp.core.common import trace_error_handler
+
+log = logging.getLogger(__name__)
+
+
+class Registry(object):
+ """
+ This is the Component Registry. It is a singleton object and is used to provide a look up service for common
+ objects.
+ """
+ log.info('Registry loaded')
+ __instance__ = None
+
+ def __new__(cls):
+ """
+ Re-implement the __new__ method to make sure we create a true singleton.
+ """
+ if not cls.__instance__:
+ cls.__instance__ = object.__new__(cls)
+ return cls.__instance__
+
+ @classmethod
+ def create(cls):
+ """
+ The constructor for the component registry providing a single registry of objects.
+ """
+ log.info('Registry Initialising')
+ registry = cls()
+ registry.service_list = {}
+ registry.functions_list = {}
+ # Allow the tests to remove Registry entries but not the live system
+ registry.running_under_test = 'nose' in sys.argv[0]
+ registry.initialising = True
+ return registry
+
+ def get(self, key):
+ """
+ Extracts the registry value from the list based on the key passed in
+
+ :param key: The service to be retrieved.
+ """
+ if key in self.service_list:
+ return self.service_list[key]
+ else:
+ if not self.initialising:
+ trace_error_handler(log)
+ log.error('Service %s not found in list' % key)
+ raise KeyError('Service %s not found in list' % key)
+
+ def register(self, key, reference):
+ """
+ Registers a component against a key.
+
+ :param key: The service to be created this is usually a major class like "renderer" or "main_window" .
+ :param reference: The service address to be saved.
+ """
+ if key in self.service_list:
+ trace_error_handler(log)
+ log.error('Duplicate service exception %s' % key)
+ raise KeyError('Duplicate service exception %s' % key)
+ else:
+ self.service_list[key] = reference
+
+ def remove(self, key):
+ """
+ Removes the registry value from the list based on the key passed in (Only valid and active for testing
+ framework).
+
+ :param key: The service to be deleted.
+ """
+ if key in self.service_list:
+ del self.service_list[key]
+
+ def register_function(self, event, function):
+ """
+ Register an event and associated function to be called
+
+ :param event: The function description like "live_display_hide" where a number of places in the code
+ will/may need to respond to a single action and the caller does not need to understand or know about the
+ recipients.
+ :param function: The function to be called when the event happens.
+ """
+ if event in self.functions_list:
+ self.functions_list[event].append(function)
+ else:
+ self.functions_list[event] = [function]
+
+ def remove_function(self, event, function):
+ """
+ Remove an event and associated handler
+
+ :param event: The function description..
+ :param function: The function to be called when the event happens.
+ """
+ if event in self.functions_list:
+ self.functions_list[event].remove(function)
+
+ def execute(self, event, *args, **kwargs):
+ """
+ Execute all the handlers associated with the event and return an array of results.
+
+ :param event: The function to be processed
+ :param args: Parameters to be passed to the function.
+ :param kwargs: Parameters to be passed to the function.
+ """
+ results = []
+ if event in self.functions_list:
+ for function in self.functions_list[event]:
+ try:
+ result = function(*args, **kwargs)
+ if result:
+ results.append(result)
+ except TypeError:
+ # Who has called me can help in debugging
+ trace_error_handler(log)
+ log.exception('Exception for function %s', function)
+ else:
+ trace_error_handler(log)
+ log.error("Event %s called but not registered" % event)
+ return results
=== added file 'openlp/core/common/registrymixin.py'
--- openlp/core/common/registrymixin.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/registrymixin.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide Registry Services
+"""
+from openlp.core.common import Registry, de_hump
+
+
+class RegistryMixin(object):
+ """
+ This adds registry components to classes to use at run time.
+ """
+ def __init__(self, parent):
+ """
+ Register the class and bootstrap hooks.
+ """
+ try:
+ super(RegistryMixin, self).__init__(parent)
+ except TypeError:
+ super(RegistryMixin, self).__init__()
+ Registry().register(de_hump(self.__class__.__name__), self)
+ Registry().register_function('bootstrap_initialise', self.bootstrap_initialise)
+ Registry().register_function('bootstrap_post_set_up', self.bootstrap_post_set_up)
+
+ def bootstrap_initialise(self):
+ """
+ Dummy method to be overridden
+ """
+ pass
+
+ def bootstrap_post_set_up(self):
+ """
+ Dummy method to be overridden
+ """
+ pass
=== added file 'openlp/core/common/registryproperties.py'
--- openlp/core/common/registryproperties.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/registryproperties.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide Registry values for adding to classes
+"""
+from openlp.core.common import Registry, is_win
+
+
+class RegistryProperties(object):
+ """
+ This adds registry components to classes to use at run time.
+ """
+
+ @property
+ def application(self):
+ """
+ Adds the openlp to the class dynamically.
+ Windows needs to access the application in a dynamic manner.
+ """
+ if is_win():
+ return Registry().get('application')
+ else:
+ if not hasattr(self, '_application') or not self._application:
+ self._application = Registry().get('application')
+ return self._application
+
+ @property
+ def plugin_manager(self):
+ """
+ Adds the plugin manager to the class dynamically
+ """
+ if not hasattr(self, '_plugin_manager') or not self._plugin_manager:
+ self._plugin_manager = Registry().get('plugin_manager')
+ return self._plugin_manager
+
+ @property
+ def image_manager(self):
+ """
+ Adds the image manager to the class dynamically
+ """
+ if not hasattr(self, '_image_manager') or not self._image_manager:
+ self._image_manager = Registry().get('image_manager')
+ return self._image_manager
+
+ @property
+ def media_controller(self):
+ """
+ Adds the media controller to the class dynamically
+ """
+ if not hasattr(self, '_media_controller') or not self._media_controller:
+ self._media_controller = Registry().get('media_controller')
+ return self._media_controller
+
+ @property
+ def service_manager(self):
+ """
+ Adds the service manager to the class dynamically
+ """
+ if not hasattr(self, '_service_manager') or not self._service_manager:
+ self._service_manager = Registry().get('service_manager')
+ return self._service_manager
+
+ @property
+ def preview_controller(self):
+ """
+ Adds the preview controller to the class dynamically
+ """
+ if not hasattr(self, '_preview_controller') or not self._preview_controller:
+ self._preview_controller = Registry().get('preview_controller')
+ return self._preview_controller
+
+ @property
+ def live_controller(self):
+ """
+ Adds the live controller to the class dynamically
+ """
+ if not hasattr(self, '_live_controller') or not self._live_controller:
+ self._live_controller = Registry().get('live_controller')
+ return self._live_controller
+
+ @property
+ def main_window(self):
+ """
+ Adds the main window to the class dynamically
+ """
+ if not hasattr(self, '_main_window') or not self._main_window:
+ self._main_window = Registry().get('main_window')
+ return self._main_window
+
+ @property
+ def renderer(self):
+ """
+ Adds the Renderer to the class dynamically
+ """
+ if not hasattr(self, '_renderer') or not self._renderer:
+ self._renderer = Registry().get('renderer')
+ return self._renderer
+
+ @property
+ def theme_manager(self):
+ """
+ Adds the theme manager to the class dynamically
+ """
+ if not hasattr(self, '_theme_manager') or not self._theme_manager:
+ self._theme_manager = Registry().get('theme_manager')
+ return self._theme_manager
+
+ @property
+ def settings_form(self):
+ """
+ Adds the settings form to the class dynamically
+ """
+ if not hasattr(self, '_settings_form') or not self._settings_form:
+ self._settings_form = Registry().get('settings_form')
+ return self._settings_form
+
+ @property
+ def alerts_manager(self):
+ """
+ Adds the alerts manager to the class dynamically
+ """
+ if not hasattr(self, '_alerts_manager') or not self._alerts_manager:
+ self._alerts_manager = Registry().get('alerts_manager')
+ return self._alerts_manager
+
+ @property
+ def projector_manager(self):
+ """
+ Adds the projector manager to the class dynamically
+ """
+ if not hasattr(self, '_projector_manager') or not self._projector_manager:
+ self._projector_manager = Registry().get('projector_manager')
+ return self._projector_manager
=== added file 'openlp/core/common/settings.py'
--- openlp/core/common/settings.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/settings.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,496 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 class contains the core default settings.
+"""
+import datetime
+import logging
+import os
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
+
+
+log = logging.getLogger(__name__)
+
+
+# Fix for bug #1014422.
+X11_BYPASS_DEFAULT = True
+if is_linux():
+ # Default to False on Gnome.
+ X11_BYPASS_DEFAULT = bool(not os.environ.get('GNOME_DESKTOP_SESSION_ID'))
+ # Default to False on Xfce.
+ if os.environ.get('DESKTOP_SESSION') == 'xfce':
+ X11_BYPASS_DEFAULT = False
+
+
+def recent_files_conv(value):
+ """
+ If the value is not a list convert it to a list
+ :param value: Value to convert
+ :return: value as a List
+ """
+ if isinstance(value, list):
+ return value
+ elif isinstance(value, str):
+ return [value]
+ elif isinstance(value, bytes):
+ return [value.decode()]
+ return []
+
+
+def media_players_conv(string):
+ """
+ If phonon is in the setting string replace it with system
+ :param string: String to convert
+ :return: Converted string
+ """
+ values = string.split(',')
+ for index, value in enumerate(values):
+ if value == 'phonon':
+ values[index] = 'system'
+ string = ','.join(values)
+ return string
+
+
+class Settings(QtCore.QSettings):
+ """
+ Class to wrap QSettings.
+
+ * Exposes all the methods of QSettings.
+ * Adds functionality for OpenLP Portable. If the ``defaultFormat`` is set to
+ ``IniFormat``, and the path to the Ini file is set using ``set_filename``,
+ then the Settings constructor (without any arguments) will create a Settings
+ object for accessing settings stored in that Ini file.
+
+ ``__default_settings__``
+ This dict contains all core settings with their default values.
+
+ ``__obsolete_settings__``
+ Each entry is structured in the following way::
+
+ ('general/enable slide loop', 'advanced/slide limits', [(SlideLimits.Wrap, True), (SlideLimits.End, False)])
+
+ The first entry is the *old key*; it will be removed.
+
+ The second entry is the *new key*; we will add it to the config. If this is just an empty string, we just remove
+ the old key. The last entry is a list containing two-pair tuples. If the list is empty, no conversion is made.
+ If the first value is callable i.e. a function, the function will be called with the old setting's value.
+ Otherwise each pair describes how to convert the old setting's value::
+
+ (SlideLimits.Wrap, True)
+
+ This means, that if the value of ``general/enable slide loop`` is equal (``==``) ``True`` then we set
+ ``advanced/slide limits`` to ``SlideLimits.Wrap``. **NOTE**, this means that the rules have to cover all cases!
+ So, if the type of the old value is bool, then there must be two rules.
+ """
+ __default_settings__ = {
+ 'advanced/add page break': False,
+ 'advanced/alternate rows': not is_win(),
+ 'advanced/current media plugin': -1,
+ 'advanced/data path': '',
+ 'advanced/default color': '#ffffff',
+ 'advanced/default image': ':/graphics/openlp-splash-screen.png',
+ # 7 stands for now, 0 to 6 is Monday to Sunday.
+ 'advanced/default service day': 7,
+ 'advanced/default service enabled': True,
+ 'advanced/default service hour': 11,
+ 'advanced/default service minute': 0,
+ 'advanced/default service name': UiStrings().DefaultServiceName,
+ 'advanced/display size': 0,
+ 'advanced/double click live': False,
+ 'advanced/enable exit confirmation': True,
+ 'advanced/expand service item': False,
+ 'advanced/slide max height': 0,
+ 'advanced/hide mouse': True,
+ 'advanced/is portable': False,
+ 'advanced/max recent files': 20,
+ 'advanced/print file meta data': False,
+ 'advanced/print notes': False,
+ 'advanced/print slide text': False,
+ 'advanced/recent file count': 4,
+ 'advanced/save current plugin': False,
+ 'advanced/slide limits': SlideLimits.End,
+ 'advanced/single click preview': False,
+ 'advanced/single click service preview': False,
+ 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
+ 'advanced/search as type': True,
+ 'crashreport/last directory': '',
+ 'formattingTags/html_tags': '',
+ 'core/audio repeat list': False,
+ 'core/auto open': False,
+ 'core/auto preview': False,
+ 'core/audio start paused': True,
+ 'core/auto unblank': False,
+ 'core/blank warning': False,
+ 'core/ccli number': '',
+ 'core/has run wizard': False,
+ 'core/language': '[en]',
+ 'core/last version test': '',
+ 'core/loop delay': 5,
+ 'core/recent files': [],
+ 'core/save prompt': False,
+ 'core/screen blank': False,
+ 'core/show splash': True,
+ 'core/songselect password': '',
+ 'core/songselect username': '',
+ 'core/update check': True,
+ 'core/view mode': 'default',
+ # The other display settings (display position and dimensions) are defined in the ScreenList class due to a
+ # circular dependency.
+ 'core/display on monitor': True,
+ 'core/override position': False,
+ 'core/application version': '0.0',
+ 'images/background color': '#000000',
+ 'media/players': 'system,webkit',
+ 'media/override player': QtCore.Qt.Unchecked,
+ 'players/background color': '#000000',
+ 'servicemanager/last directory': '',
+ 'servicemanager/last file': '',
+ 'servicemanager/service theme': '',
+ 'SettingsImport/file_date_created': datetime.datetime.now().strftime("%Y-%m-%d %H:%M"),
+ 'SettingsImport/Make_Changes': 'At_Own_RISK',
+ 'SettingsImport/type': 'OpenLP_settings_export',
+ 'SettingsImport/version': '',
+ 'themes/global theme': '',
+ 'themes/last directory': '',
+ 'themes/last directory export': '',
+ 'themes/last directory import': '',
+ 'themes/theme level': ThemeLevel.Song,
+ 'themes/wrap footer': False,
+ 'user interface/live panel': True,
+ 'user interface/live splitter geometry': QtCore.QByteArray(),
+ 'user interface/lock panel': False,
+ 'user interface/main window geometry': QtCore.QByteArray(),
+ 'user interface/main window position': QtCore.QPoint(0, 0),
+ 'user interface/main window splitter geometry': QtCore.QByteArray(),
+ 'user interface/main window state': QtCore.QByteArray(),
+ 'user interface/preview panel': True,
+ 'user interface/preview splitter geometry': QtCore.QByteArray(),
+ 'projector/db type': 'sqlite',
+ 'projector/db username': '',
+ 'projector/db password': '',
+ 'projector/db hostname': '',
+ 'projector/db database': '',
+ 'projector/enable': True,
+ 'projector/connect on start': False,
+ 'projector/last directory import': '',
+ 'projector/last directory export': '',
+ 'projector/poll time': 20, # PJLink timeout is 30 seconds
+ 'projector/socket timeout': 5, # 5 second socket timeout
+ 'projector/source dialog type': 0 # Source select dialog box type
+ }
+ __file_path__ = ''
+ __obsolete_settings__ = [
+ # Changed during 2.2.x development.
+ # ('advanced/stylesheet fix', '', []),
+ # ('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
+ ('songs/search as type', 'advanced/search as type', []),
+ ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system
+ ('media/players_temp', 'media/players', []) # Move temp setting from above to correct setting
+ ]
+
+ @staticmethod
+ def extend_default_settings(default_values):
+ """
+ Static method to merge the given ``default_values`` with the ``Settings.__default_settings__``.
+
+ :param default_values: A dict with setting keys and their default values.
+ """
+ Settings.__default_settings__.update(default_values)
+
+ @staticmethod
+ def set_filename(ini_file):
+ """
+ Sets the complete path to an Ini file to be used by Settings objects.
+
+ Does not affect existing Settings objects.
+ """
+ Settings.__file_path__ = ini_file
+
+ @staticmethod
+ def set_up_default_values():
+ """
+ This static method is called on start up. It is used to perform any operation on the __default_settings__ dict.
+ """
+ # Make sure the string is translated (when building the dict the string is not translated because the translate
+ # function was not set up as this stage).
+ Settings.__default_settings__['advanced/default service name'] = UiStrings().DefaultServiceName
+
+ def __init__(self, *args):
+ """
+ Constructor which checks if this should be a native settings object, or an INI file.
+ """
+ if not args and Settings.__file_path__ and Settings.defaultFormat() == Settings.IniFormat:
+ QtCore.QSettings.__init__(self, Settings.__file_path__, Settings.IniFormat)
+ else:
+ QtCore.QSettings.__init__(self, *args)
+ # Add shortcuts here so QKeySequence has a QApplication instance to use.
+ Settings.__default_settings__.update({
+ 'shortcuts/aboutItem': [QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_F1)],
+ 'shortcuts/addToService': [],
+ 'shortcuts/audioPauseItem': [],
+ 'shortcuts/displayTagItem': [],
+ 'shortcuts/blankScreen': [QtGui.QKeySequence(QtCore.Qt.Key_Period)],
+ 'shortcuts/collapse': [QtGui.QKeySequence(QtCore.Qt.Key_Minus)],
+ 'shortcuts/desktopScreen': [QtGui.QKeySequence(QtCore.Qt.Key_D)],
+ 'shortcuts/delete': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+ 'shortcuts/down': [QtGui.QKeySequence(QtCore.Qt.Key_Down)],
+ 'shortcuts/editSong': [],
+ 'shortcuts/escapeItem': [QtGui.QKeySequence(QtCore.Qt.Key_Escape)],
+ 'shortcuts/expand': [QtGui.QKeySequence(QtCore.Qt.Key_Plus)],
+ 'shortcuts/exportThemeItem': [],
+ 'shortcuts/fileNewItem': [QtGui.QKeySequence(QtGui.QKeySequence.New)],
+ 'shortcuts/fileSaveAsItem': [QtGui.QKeySequence(QtGui.QKeySequence.SaveAs)],
+ 'shortcuts/fileExitItem': [QtGui.QKeySequence(QtGui.QKeySequence.Quit)],
+ 'shortcuts/fileSaveItem': [QtGui.QKeySequence(QtGui.QKeySequence.Save)],
+ 'shortcuts/fileOpenItem': [QtGui.QKeySequence(QtGui.QKeySequence.Open)],
+ 'shortcuts/goLive': [],
+ 'shortcuts/importThemeItem': [],
+ 'shortcuts/importBibleItem': [],
+ 'shortcuts/listViewBiblesDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+ 'shortcuts/listViewBiblesPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewBiblesLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewBiblesServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+ QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+ 'shortcuts/listViewCustomDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+ 'shortcuts/listViewCustomPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewCustomLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewCustomServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+ QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+ 'shortcuts/listViewImagesDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+ 'shortcuts/listViewImagesPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewImagesLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewImagesServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+ QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+ 'shortcuts/listViewMediaDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+ 'shortcuts/listViewMediaPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewMediaLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewMediaServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+ QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+ 'shortcuts/listViewPresentationsDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+ 'shortcuts/listViewPresentationsPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewPresentationsLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewPresentationsServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+ QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+ 'shortcuts/listViewSongsDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
+ 'shortcuts/listViewSongsPreviewItem': [QtGui.QKeySequence(QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewSongsLiveItem': [QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Return),
+ QtGui.QKeySequence(QtCore.Qt.SHIFT + QtCore.Qt.Key_Enter)],
+ 'shortcuts/listViewSongsServiceItem': [QtGui.QKeySequence(QtCore.Qt.Key_Plus),
+ QtGui.QKeySequence(QtCore.Qt.Key_Equal)],
+ 'shortcuts/lockPanel': [],
+ 'shortcuts/modeDefaultItem': [],
+ 'shortcuts/modeLiveItem': [],
+ 'shortcuts/make_live': [QtGui.QKeySequence(QtCore.Qt.Key_Return), QtGui.QKeySequence(QtCore.Qt.Key_Enter)],
+ 'shortcuts/moveUp': [QtGui.QKeySequence(QtCore.Qt.Key_PageUp)],
+ 'shortcuts/moveTop': [QtGui.QKeySequence(QtCore.Qt.Key_Home)],
+ 'shortcuts/modeSetupItem': [],
+ 'shortcuts/moveBottom': [QtGui.QKeySequence(QtCore.Qt.Key_End)],
+ 'shortcuts/moveDown': [QtGui.QKeySequence(QtCore.Qt.Key_PageDown)],
+ 'shortcuts/nextTrackItem': [],
+ 'shortcuts/nextItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Down),
+ QtGui.QKeySequence(QtCore.Qt.Key_PageDown)],
+ 'shortcuts/nextItem_preview': [QtGui.QKeySequence(QtCore.Qt.Key_Down),
+ QtGui.QKeySequence(QtCore.Qt.Key_PageDown)],
+ 'shortcuts/nextService': [QtGui.QKeySequence(QtCore.Qt.Key_Right)],
+ 'shortcuts/newService': [],
+ 'shortcuts/offlineHelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)],
+ 'shortcuts/onlineHelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)],
+ 'shortcuts/openService': [],
+ 'shortcuts/saveService': [],
+ 'shortcuts/previousItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Up),
+ QtGui.QKeySequence(QtCore.Qt.Key_PageUp)],
+ 'shortcuts/playbackPause': [],
+ 'shortcuts/playbackPlay': [],
+ 'shortcuts/playbackStop': [],
+ 'shortcuts/playSlidesLoop': [],
+ 'shortcuts/playSlidesOnce': [],
+ 'shortcuts/previousService': [QtGui.QKeySequence(QtCore.Qt.Key_Left)],
+ 'shortcuts/previousItem_preview': [QtGui.QKeySequence(QtCore.Qt.Key_Up),
+ QtGui.QKeySequence(QtCore.Qt.Key_PageUp)],
+ 'shortcuts/printServiceItem': [QtGui.QKeySequence(QtGui.QKeySequence.Print)],
+ 'shortcuts/songExportItem': [],
+ 'shortcuts/songUsageStatus': [QtGui.QKeySequence(QtCore.Qt.Key_F4)],
+ 'shortcuts/searchShortcut': [QtGui.QKeySequence(QtGui.QKeySequence.Find)],
+ 'shortcuts/settingsShortcutsItem': [],
+ 'shortcuts/settingsImportItem': [],
+ 'shortcuts/settingsPluginListItem': [QtGui.QKeySequence(QtCore.Qt.ALT + QtCore.Qt.Key_F7)],
+ 'shortcuts/songUsageDelete': [],
+ 'shortcuts/settingsConfigureItem': [QtGui.QKeySequence(QtGui.QKeySequence.Preferences)],
+ 'shortcuts/shortcutAction_B': [QtGui.QKeySequence(QtCore.Qt.Key_B)],
+ 'shortcuts/shortcutAction_C': [QtGui.QKeySequence(QtCore.Qt.Key_C)],
+ 'shortcuts/shortcutAction_E': [QtGui.QKeySequence(QtCore.Qt.Key_E)],
+ 'shortcuts/shortcutAction_I': [QtGui.QKeySequence(QtCore.Qt.Key_I)],
+ 'shortcuts/shortcutAction_O': [QtGui.QKeySequence(QtCore.Qt.Key_O)],
+ 'shortcuts/shortcutAction_P': [QtGui.QKeySequence(QtCore.Qt.Key_P)],
+ 'shortcuts/shortcutAction_V': [QtGui.QKeySequence(QtCore.Qt.Key_V)],
+ 'shortcuts/shortcutAction_0': [QtGui.QKeySequence(QtCore.Qt.Key_0)],
+ 'shortcuts/shortcutAction_1': [QtGui.QKeySequence(QtCore.Qt.Key_1)],
+ 'shortcuts/shortcutAction_2': [QtGui.QKeySequence(QtCore.Qt.Key_2)],
+ 'shortcuts/shortcutAction_3': [QtGui.QKeySequence(QtCore.Qt.Key_3)],
+ 'shortcuts/shortcutAction_4': [QtGui.QKeySequence(QtCore.Qt.Key_4)],
+ 'shortcuts/shortcutAction_5': [QtGui.QKeySequence(QtCore.Qt.Key_5)],
+ 'shortcuts/shortcutAction_6': [QtGui.QKeySequence(QtCore.Qt.Key_6)],
+ 'shortcuts/shortcutAction_7': [QtGui.QKeySequence(QtCore.Qt.Key_7)],
+ 'shortcuts/shortcutAction_8': [QtGui.QKeySequence(QtCore.Qt.Key_8)],
+ 'shortcuts/shortcutAction_9': [QtGui.QKeySequence(QtCore.Qt.Key_9)],
+ 'shortcuts/settingsExportItem': [],
+ 'shortcuts/songUsageReport': [],
+ 'shortcuts/songImportItem': [],
+ 'shortcuts/themeScreen': [QtGui.QKeySequence(QtCore.Qt.Key_T)],
+ 'shortcuts/toolsReindexItem': [],
+ 'shortcuts/toolsFindDuplicates': [],
+ 'shortcuts/toolsAlertItem': [QtGui.QKeySequence(QtCore.Qt.Key_F7)],
+ 'shortcuts/toolsFirstTimeWizard': [],
+ 'shortcuts/toolsOpenDataFolder': [],
+ 'shortcuts/toolsAddToolItem': [],
+ 'shortcuts/updateThemeImages': [],
+ 'shortcuts/up': [QtGui.QKeySequence(QtCore.Qt.Key_Up)],
+ 'shortcuts/viewProjectorManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F6)],
+ 'shortcuts/viewThemeManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F10)],
+ 'shortcuts/viewMediaManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F8)],
+ 'shortcuts/viewPreviewPanel': [QtGui.QKeySequence(QtCore.Qt.Key_F11)],
+ 'shortcuts/viewLivePanel': [QtGui.QKeySequence(QtCore.Qt.Key_F12)],
+ 'shortcuts/viewServiceManagerItem': [QtGui.QKeySequence(QtCore.Qt.Key_F9)],
+ 'shortcuts/webSiteItem': []
+ })
+
+ def get_default_value(self, key):
+ """
+ Get the default value of the given key
+ """
+ if self.group():
+ key = self.group() + '/' + key
+ return Settings.__default_settings__[key]
+
+ def remove_obsolete_settings(self):
+ """
+ This method is only called to clean up the config. It removes old settings and it renames settings. See
+ ``__obsolete_settings__`` for more details.
+ """
+ for old_key, new_key, rules in Settings.__obsolete_settings__:
+ # Once removed we don't have to do this again.
+ if self.contains(old_key):
+ if new_key:
+ # Get the value of the old_key.
+ old_value = super(Settings, self).value(old_key)
+ # When we want to convert the value, we have to figure out the default value (because we cannot get
+ # the default value from the central settings dict.
+ if rules:
+ default_value = rules[0][1]
+ old_value = self._convert_value(old_value, default_value)
+ # Iterate over our rules and check what the old_value should be "converted" to.
+ for new, old in rules:
+ # If the value matches with the condition (rule), then use the provided value. This is used to
+ # convert values. E. g. an old value 1 results in True, and 0 in False.
+ if callable(new):
+ old_value = new(old_value)
+ elif old == old_value:
+ old_value = new
+ break
+ self.setValue(new_key, old_value)
+ self.remove(old_key)
+
+ def value(self, key):
+ """
+ Returns the value for the given ``key``. The returned ``value`` is of the same type as the default value in the
+ *Settings.__default_settings__* dict.
+
+ :param key: The key to return the value from.
+ """
+ # if group() is not empty the group has not been specified together with the key.
+ if self.group():
+ default_value = Settings.__default_settings__[self.group() + '/' + key]
+ else:
+ default_value = Settings.__default_settings__[key]
+ setting = super(Settings, self).value(key, default_value)
+ return self._convert_value(setting, default_value)
+
+ def _convert_value(self, setting, default_value):
+ """
+ This converts the given ``setting`` to the type of the given ``default_value``.
+
+ :param setting: The setting to convert. This could be ``true`` for example.Settings()
+ :param default_value: Indication the type the setting should be converted to. For example ``True``
+ (type is boolean), meaning that we convert the string ``true`` to a python boolean.
+
+ **Note**, this method only converts a few types and might need to be extended if a certain type is missing!
+ """
+ # Handle 'None' type (empty value) properly.
+ if setting is None:
+ # An empty string saved to the settings results in a None type being returned.
+ # Convert it to empty unicode string.
+ if isinstance(default_value, str):
+ return ''
+ # An empty list saved to the settings results in a None type being returned.
+ else:
+ return []
+ # Convert the setting to the correct type.
+ if isinstance(default_value, bool):
+ if isinstance(setting, bool):
+ return setting
+ # Sometimes setting is string instead of a boolean.
+ return setting == 'true'
+ if isinstance(default_value, int):
+ return int(setting)
+ return setting
+
+ def get_files_from_config(self, plugin):
+ """
+ This removes the settings needed for old way we saved files (e. g. the image paths for the image plugin). A list
+ of file paths are returned.
+
+ **Note**: Only a list of paths is returned; this does not convert anything!
+
+ :param plugin: The Plugin object.The caller has to convert/save the list himself; o
+ """
+ files_list = []
+ # We need QSettings instead of Settings here to bypass our central settings dict.
+ # Do NOT do this anywhere else!
+ settings = QtCore.QSettings(self.fileName(), Settings.IniFormat)
+ settings.beginGroup(plugin.settings_section)
+ if settings.contains('%s count' % plugin.name):
+ # Get the count.
+ list_count = int(settings.value('%s count' % plugin.name, 0))
+ if list_count:
+ for counter in range(list_count):
+ # The keys were named e. g.: "image 0"
+ item = settings.value('%s %d' % (plugin.name, counter), '')
+ if item:
+ files_list.append(item)
+ settings.remove('%s %d' % (plugin.name, counter))
+ settings.remove('%s count' % plugin.name)
+ settings.endGroup()
+ return files_list
=== added file 'openlp/core/common/uistrings.py'
--- openlp/core/common/uistrings.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/uistrings.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`uistrings` module provides standard strings for OpenLP.
+"""
+import logging
+
+from openlp.core.common import translate
+
+
+log = logging.getLogger(__name__)
+
+
+class UiStrings(object):
+ """
+ Provide standard strings for objects to use.
+ """
+ __instance__ = None
+
+ def __new__(cls):
+ """
+ Override the default object creation method to return a single instance.
+ """
+ if not cls.__instance__:
+ cls.__instance__ = object.__new__(cls)
+ return cls.__instance__
+
+ def __init__(self):
+ """
+ These strings should need a good reason to be retranslated elsewhere.
+ Should some/more/less of these have an & attached?
+ """
+ self.About = translate('OpenLP.Ui', 'About')
+ self.Add = translate('OpenLP.Ui', '&Add')
+ self.AddGroup = translate('OpenLP.Ui', 'Add group')
+ self.Advanced = translate('OpenLP.Ui', 'Advanced')
+ self.AllFiles = translate('OpenLP.Ui', 'All Files')
+ self.Automatic = translate('OpenLP.Ui', 'Automatic')
+ self.BackgroundColor = translate('OpenLP.Ui', 'Background Color')
+ self.Bottom = translate('OpenLP.Ui', 'Bottom')
+ self.Browse = translate('OpenLP.Ui', 'Browse...')
+ self.Cancel = translate('OpenLP.Ui', 'Cancel')
+ self.CCLINumberLabel = translate('OpenLP.Ui', 'CCLI number:')
+ self.CCLISongNumberLabel = translate('OpenLP.Ui', 'CCLI song number:')
+ self.CreateService = translate('OpenLP.Ui', 'Create a new service.')
+ self.ConfirmDelete = translate('OpenLP.Ui', 'Confirm Delete')
+ self.Continuous = translate('OpenLP.Ui', 'Continuous')
+ self.Default = translate('OpenLP.Ui', 'Default')
+ self.DefaultColor = translate('OpenLP.Ui', 'Default Color:')
+ self.DefaultServiceName = translate('OpenLP.Ui', 'Service %Y-%m-%d %H-%M',
+ 'This may not contain any of the following characters: /\\?*|<>\[\]":+\n'
+ 'See http://docs.python.org/library/datetime'
+ '.html#strftime-strptime-behavior for more information.')
+ self.Delete = translate('OpenLP.Ui', '&Delete')
+ self.DisplayStyle = translate('OpenLP.Ui', 'Display style:')
+ self.Duplicate = translate('OpenLP.Ui', 'Duplicate Error')
+ self.Edit = translate('OpenLP.Ui', '&Edit')
+ self.EmptyField = translate('OpenLP.Ui', 'Empty Field')
+ self.Error = translate('OpenLP.Ui', 'Error')
+ self.Export = translate('OpenLP.Ui', 'Export')
+ self.File = translate('OpenLP.Ui', 'File')
+ self.FileNotFound = translate('OpenLP.Ui', 'File Not Found')
+ self.FileNotFoundMessage = translate('OpenLP.Ui', 'File %s not found.\nPlease try selecting it individually.')
+ self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')
+ self.Help = translate('OpenLP.Ui', 'Help')
+ self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
+ self.IFdSs = translate('OpenLP.Ui', 'Invalid Folder Selected', 'Singular')
+ self.IFSs = translate('OpenLP.Ui', 'Invalid File Selected', 'Singular')
+ self.IFSp = translate('OpenLP.Ui', 'Invalid Files Selected', 'Plural')
+ self.Image = translate('OpenLP.Ui', 'Image')
+ self.Import = translate('OpenLP.Ui', 'Import')
+ self.LayoutStyle = translate('OpenLP.Ui', 'Layout style:')
+ self.Live = translate('OpenLP.Ui', 'Live')
+ self.LiveBGError = translate('OpenLP.Ui', 'Live Background Error')
+ self.LiveToolbar = translate('OpenLP.Ui', 'Live Toolbar')
+ self.Load = translate('OpenLP.Ui', 'Load')
+ self.Manufacturer = translate('OpenLP.Ui', 'Manufacturer', 'Singular')
+ self.Manufacturers = translate('OpenLP.Ui', 'Manufacturers', 'Plural')
+ self.Model = translate('OpenLP.Ui', 'Model', 'Singular')
+ self.Models = translate('OpenLP.Ui', 'Models', 'Plural')
+ self.Minutes = translate('OpenLP.Ui', 'm', 'The abbreviated unit for minutes')
+ self.Middle = translate('OpenLP.Ui', 'Middle')
+ self.New = translate('OpenLP.Ui', 'New')
+ self.NewService = translate('OpenLP.Ui', 'New Service')
+ self.NewTheme = translate('OpenLP.Ui', 'New Theme')
+ self.NextTrack = translate('OpenLP.Ui', 'Next Track')
+ self.NFdSs = translate('OpenLP.Ui', 'No Folder Selected', 'Singular')
+ self.NFSs = translate('OpenLP.Ui', 'No File Selected', 'Singular')
+ self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural')
+ self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular')
+ self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural')
+ self.OLP = translate('OpenLP.Ui', 'OpenLP')
+ self.OLPV2 = "%s %s" % (self.OLP, "2")
+ self.OLPV2x = "%s %s" % (self.OLP, "2.4")
+ self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?')
+ self.OpenService = translate('OpenLP.Ui', 'Open service.')
+ self.PlaySlidesInLoop = translate('OpenLP.Ui', 'Play Slides in Loop')
+ self.PlaySlidesToEnd = translate('OpenLP.Ui', 'Play Slides to End')
+ self.Preview = translate('OpenLP.Ui', 'Preview')
+ self.PreviewToolbar = translate('OpenLP.Ui', 'Preview Toolbar')
+ self.PrintService = translate('OpenLP.Ui', 'Print Service')
+ self.Projector = translate('OpenLP.Ui', 'Projector', 'Singular')
+ self.Projectors = translate('OpenLP.Ui', 'Projectors', 'Plural')
+ self.ReplaceBG = translate('OpenLP.Ui', 'Replace Background')
+ self.ReplaceLiveBG = translate('OpenLP.Ui', 'Replace live background.')
+ self.ReplaceLiveBGDisabled = translate('OpenLP.Ui', 'Replace live background is not available when the WebKit '
+ 'player is disabled.')
+ self.ResetBG = translate('OpenLP.Ui', 'Reset Background')
+ self.ResetLiveBG = translate('OpenLP.Ui', 'Reset live background.')
+ self.Seconds = translate('OpenLP.Ui', 's', 'The abbreviated unit for seconds')
+ self.SaveAndPreview = translate('OpenLP.Ui', 'Save && Preview')
+ self.Search = translate('OpenLP.Ui', 'Search')
+ self.SearchThemes = translate('OpenLP.Ui', 'Search Themes...', 'Search bar place holder text ')
+ self.SelectDelete = translate('OpenLP.Ui', 'You must select an item to delete.')
+ self.SelectEdit = translate('OpenLP.Ui', 'You must select an item to edit.')
+ self.Settings = translate('OpenLP.Ui', 'Settings')
+ self.SaveService = translate('OpenLP.Ui', 'Save Service')
+ self.Service = translate('OpenLP.Ui', 'Service')
+ self.Split = translate('OpenLP.Ui', 'Optional &Split')
+ self.SplitToolTip = translate('OpenLP.Ui',
+ 'Split a slide into two only if it does not fit on the screen as one slide.')
+ self.StartTimeCode = translate('OpenLP.Ui', 'Start %s')
+ self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
+ self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
+ self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')
+ self.Themes = translate('OpenLP.Ui', 'Themes', 'Plural')
+ self.Tools = translate('OpenLP.Ui', 'Tools')
+ self.Top = translate('OpenLP.Ui', 'Top')
+ self.UnsupportedFile = translate('OpenLP.Ui', 'Unsupported File')
+ self.VersePerSlide = translate('OpenLP.Ui', 'Verse Per Slide')
+ self.VersePerLine = translate('OpenLP.Ui', 'Verse Per Line')
+ self.Version = translate('OpenLP.Ui', 'Version')
+ self.View = translate('OpenLP.Ui', 'View')
+ self.ViewMode = translate('OpenLP.Ui', 'View Mode')
=== added file 'openlp/core/common/versionchecker.py'
--- openlp/core/common/versionchecker.py 1970-01-01 00:00:00 +0000
+++ openlp/core/common/versionchecker.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,170 @@
+import logging
+import os
+import platform
+import sys
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+from datetime import datetime
+from distutils.version import LooseVersion
+from subprocess import Popen, PIPE
+
+from openlp.core.common import AppLocation, Settings
+
+from PyQt5 import QtCore
+
+log = logging.getLogger(__name__)
+
+APPLICATION_VERSION = {}
+CONNECTION_TIMEOUT = 30
+CONNECTION_RETRIES = 2
+
+
+class VersionThread(QtCore.QThread):
+ """
+ A special Qt thread class to fetch the version of OpenLP from the website.
+ This is threaded so that it doesn't affect the loading time of OpenLP.
+ """
+ def __init__(self, main_window):
+ """
+ Constructor for the thread class.
+
+ :param main_window: The main window Object.
+ """
+ log.debug("VersionThread - Initialise")
+ super(VersionThread, self).__init__(None)
+ self.main_window = main_window
+
+ def run(self):
+ """
+ Run the thread.
+ """
+ self.sleep(1)
+ log.debug('Version thread - run')
+ app_version = get_application_version()
+ version = check_latest_version(app_version)
+ log.debug("Versions %s and %s " % (LooseVersion(str(version)), LooseVersion(str(app_version['full']))))
+ if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
+ self.main_window.openlp_version_check.emit('%s' % version)
+
+
+def get_application_version():
+ """
+ Returns the application version of the running instance of OpenLP::
+
+ {'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
+ """
+ global APPLICATION_VERSION
+ if APPLICATION_VERSION:
+ return APPLICATION_VERSION
+ if '--dev-version' in sys.argv or '-d' in sys.argv:
+ # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
+ # there.
+
+ # Get the revision of this tree.
+ bzr = Popen(('bzr', 'revno'), stdout=PIPE)
+ tree_revision, error = bzr.communicate()
+ tree_revision = tree_revision.decode()
+ code = bzr.wait()
+ if code != 0:
+ raise Exception('Error running bzr log')
+
+ # Get all tags.
+ bzr = Popen(('bzr', 'tags'), stdout=PIPE)
+ output, error = bzr.communicate()
+ code = bzr.wait()
+ if code != 0:
+ raise Exception('Error running bzr tags')
+ tags = list(map(bytes.decode, output.splitlines()))
+ if not tags:
+ tag_version = '0.0.0'
+ tag_revision = '0'
+ else:
+ # Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
+ # another series.
+ tags = [tag for tag in tags if tag.split()[-1].strip() != '?']
+ # Get the last tag and split it in a revision and tag name.
+ tag_version, tag_revision = tags[-1].split()
+ # If they are equal, then this tree is tarball with the source for the release. We do not want the revision
+ # number in the full version.
+ if tree_revision == tag_revision:
+ full_version = tag_version.strip()
+ else:
+ full_version = '%s-bzr%s' % (tag_version.strip(), tree_revision.strip())
+ else:
+ # We're not running the development version, let's use the file.
+ file_path = AppLocation.get_directory(AppLocation.VersionDir)
+ file_path = os.path.join(file_path, '.version')
+ version_file = None
+ try:
+ version_file = open(file_path, 'r')
+ full_version = str(version_file.read()).rstrip()
+ except IOError:
+ log.exception('Error in version file.')
+ full_version = '0.0.0-bzr000'
+ finally:
+ if version_file:
+ version_file.close()
+ bits = full_version.split('-')
+ APPLICATION_VERSION = {
+ 'full': full_version,
+ 'version': bits[0],
+ 'build': bits[1] if len(bits) > 1 else None
+ }
+ if APPLICATION_VERSION['build']:
+ log.info('Openlp version %s build %s', APPLICATION_VERSION['version'], APPLICATION_VERSION['build'])
+ else:
+ log.info('Openlp version %s' % APPLICATION_VERSION['version'])
+ return APPLICATION_VERSION
+
+
+def check_latest_version(current_version):
+ """
+ Check the latest version of OpenLP against the version file on the OpenLP
+ site.
+
+ **Rules around versions and version files:**
+
+ * If a version number has a build (i.e. -bzr1234), then it is a nightly.
+ * If a version number's minor version is an odd number, it is a development release.
+ * If a version number's minor version is an even number, it is a stable release.
+
+ :param current_version: The current version of OpenLP.
+ """
+ version_string = current_version['full']
+ # set to prod in the distribution config file.
+ settings = Settings()
+ settings.beginGroup('core')
+ last_test = settings.value('last version test')
+ this_test = str(datetime.now().date())
+ settings.setValue('last version test', this_test)
+ settings.endGroup()
+ if last_test != this_test:
+ if current_version['build']:
+ req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt')
+ else:
+ version_parts = current_version['version'].split('.')
+ if int(version_parts[1]) % 2 != 0:
+ req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt')
+ else:
+ req = urllib.request.Request('http://www.openlp.org/files/version.txt')
+ req.add_header('User-Agent', 'OpenLP/%s %s/%s; ' % (current_version['full'], platform.system(),
+ platform.release()))
+ remote_version = None
+ retries = 0
+ while True:
+ try:
+ remote_version = str(urllib.request.urlopen(req, None,
+ timeout=CONNECTION_TIMEOUT).read().decode()).strip()
+ except (urllib.error.URLError, ConnectionError):
+ if retries > CONNECTION_RETRIES:
+ log.exception('Failed to download the latest OpenLP version file')
+ else:
+ retries += 1
+ time.sleep(0.1)
+ continue
+ break
+ if remote_version:
+ version_string = remote_version
+ return version_string
=== added directory 'openlp/core/lib'
=== added file 'openlp/core/lib/__init__.py'
--- openlp/core/lib/__init__.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/__init__.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`lib` module contains most of the components and libraries that make
+OpenLP work.
+"""
+
+from distutils.version import LooseVersion
+import logging
+import os
+
+from PyQt5 import QtCore, QtGui, Qt, QtWidgets
+
+
+from openlp.core.common import translate
+
+log = logging.getLogger(__name__ + '.__init__')
+
+
+class ServiceItemContext(object):
+ """
+ The context in which a Service Item is being generated
+ """
+ Preview = 0
+ Live = 1
+ Service = 2
+
+
+class ImageSource(object):
+ """
+ This enumeration class represents different image sources. An image sources states where an image is used. This
+ enumeration class is need in the context of the :class:~openlp.core.lib.imagemanager`.
+
+ ``ImagePlugin``
+ This states that an image is being used by the image plugin.
+
+ ``Theme``
+ This says, that the image is used by a theme.
+ """
+ ImagePlugin = 1
+ Theme = 2
+
+
+class MediaType(object):
+ """
+ An enumeration class for types of media.
+ """
+ Audio = 1
+ Video = 2
+
+
+class ServiceItemAction(object):
+ """
+ Provides an enumeration for the required action moving between service items by left/right arrow keys
+ """
+ Previous = 1
+ PreviousLastSlide = 2
+ Next = 3
+
+
+def get_text_file_string(text_file):
+ """
+ Open a file and return its content as unicode string. If the supplied file name is not a file then the function
+ returns False. If there is an error loading the file or the content can't be decoded then the function will return
+ None.
+
+ :param text_file: The name of the file.
+ :return: The file as a single string
+ """
+ if not os.path.isfile(text_file):
+ return False
+ file_handle = None
+ content = None
+ try:
+ file_handle = open(text_file, 'r', encoding='utf-8')
+ if not file_handle.read(3) == '\xEF\xBB\xBF':
+ # no BOM was found
+ file_handle.seek(0)
+ content = file_handle.read()
+ except (IOError, UnicodeError):
+ log.exception('Failed to open text file %s' % text_file)
+ finally:
+ if file_handle:
+ file_handle.close()
+ return content
+
+
+def str_to_bool(string_value):
+ """
+ Convert a string version of a boolean into a real boolean.
+
+ :param string_value: The string value to examine and convert to a boolean type.
+ :return: The correct boolean value
+ """
+ if isinstance(string_value, bool):
+ return string_value
+ return str(string_value).strip().lower() in ('true', 'yes', 'y')
+
+
+def build_icon(icon):
+ """
+ Build a QIcon instance from an existing QIcon, a resource location, or a physical file location. If the icon is a
+ QIcon instance, that icon is simply returned. If not, it builds a QIcon instance from the resource or file name.
+
+ :param icon:
+ The icon to build. This can be a QIcon, a resource string in the form ``:/resource/file.png``, or a file
+ location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string.
+ :return: The build icon.
+ """
+ button_icon = QtGui.QIcon()
+ if isinstance(icon, QtGui.QIcon):
+ button_icon = icon
+ elif isinstance(icon, str):
+ if icon.startswith(':/'):
+ button_icon.addPixmap(QtGui.QPixmap(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+ else:
+ button_icon.addPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon)), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+ elif isinstance(icon, QtGui.QImage):
+ button_icon.addPixmap(QtGui.QPixmap.fromImage(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off)
+ return button_icon
+
+
+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()
+ # use buffer to store pixmap into byteArray
+ buffie = QtCore.QBuffer(byte_array)
+ 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')
+
+
+def create_thumb(image_path, thumb_path, return_icon=True, size=None):
+ """
+ Create a thumbnail from the given image path and depending on ``return_icon`` it returns an icon from this thumb.
+
+ :param image_path: The image file to create the icon from.
+ :param thumb_path: The filename to save the thumbnail to.
+ :param return_icon: States if an icon should be build and returned from the thumb. Defaults to ``True``.
+ :param size: Allows to state a own size (QtCore.QSize) to use. Defaults to ``None``, which means that a default
+ height of 88 is used.
+ :return: The final icon.
+ """
+ ext = os.path.splitext(thumb_path)[1].lower()
+ reader = QtGui.QImageReader(image_path)
+ if size is None:
+ ratio = reader.size().width() / reader.size().height()
+ reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
+ else:
+ reader.setScaledSize(size)
+ thumb = reader.read()
+ thumb.save(thumb_path, ext[1:])
+ if not return_icon:
+ return
+ if os.path.exists(thumb_path):
+ return build_icon(thumb_path)
+ # Fallback for files with animation support.
+ return build_icon(image_path)
+
+
+def validate_thumb(file_path, thumb_path):
+ """
+ Validates whether an file's thumb still exists and if is up to date. **Note**, you must **not** call this function,
+ before checking the existence of the file.
+
+ :param file_path: The path to the file. The file **must** exist!
+ :param thumb_path: The path to the thumb.
+ :return: True, False if the image has changed since the thumb was created.
+ """
+ if not os.path.exists(thumb_path):
+ return False
+ image_date = os.stat(file_path).st_mtime
+ thumb_date = os.stat(thumb_path).st_mtime
+ return image_date <= thumb_date
+
+
+def resize_image(image_path, width, height, background='#000000'):
+ """
+ Resize an image to fit on the current screen.
+
+ DO NOT REMOVE THE DEFAULT BACKGROUND VALUE!
+
+ :param image_path: The path to the image to resize.
+ :param width: The new image width.
+ :param height: The new image height.
+ :param background: The background colour. Defaults to black.
+ """
+ log.debug('resize_image - start')
+ reader = QtGui.QImageReader(image_path)
+ # The image's ratio.
+ image_ratio = reader.size().width() / reader.size().height()
+ resize_ratio = width / height
+ # Figure out the size we want to resize the image to (keep aspect ratio).
+ if image_ratio == resize_ratio:
+ size = QtCore.QSize(width, height)
+ elif image_ratio < resize_ratio:
+ # Use the image's height as reference for the new size.
+ size = QtCore.QSize(image_ratio * height, height)
+ else:
+ # Use the image's width as reference for the new size.
+ size = QtCore.QSize(width, 1 / (image_ratio / width))
+ reader.setScaledSize(size)
+ preview = reader.read()
+ if image_ratio == resize_ratio:
+ # We neither need to centre the image nor add "bars" to the image.
+ return preview
+ real_width = preview.width()
+ real_height = preview.height()
+ # and move it to the centre of the preview space
+ new_image = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32_Premultiplied)
+ painter = QtGui.QPainter(new_image)
+ painter.fillRect(new_image.rect(), QtGui.QColor(background))
+ painter.drawImage((width - real_width) // 2, (height - real_height) // 2, preview)
+ return new_image
+
+
+def check_item_selected(list_widget, message):
+ """
+ Check if a list item is selected so an action may be performed on it
+
+ :param list_widget: The list to check for selected items
+ :param message: The message to give the user if no item is selected
+ """
+ if not list_widget.selectedIndexes():
+ QtWidgets.QMessageBox.information(list_widget.parent(),
+ translate('OpenLP.MediaManagerItem', 'No Items Selected'), message)
+ return False
+ return True
+
+
+def clean_tags(text):
+ """
+ Remove Tags from text for display
+
+ :param text: Text to be cleaned
+ """
+ text = text.replace('<br>', '\n')
+ text = text.replace('{br}', '\n')
+ text = text.replace(' ', ' ')
+ for tag in FormattingTags.get_html_tags():
+ text = text.replace(tag['start tag'], '')
+ text = text.replace(tag['end tag'], '')
+ return text
+
+
+def expand_tags(text):
+ """
+ Expand tags HTML for display
+
+ :param text: The text to be expanded.
+ """
+ for tag in FormattingTags.get_html_tags():
+ text = text.replace(tag['start tag'], tag['start html'])
+ text = text.replace(tag['end tag'], tag['end html'])
+ return text
+
+
+def create_separated_list(string_list):
+ """
+ Returns a string that represents a join of a list of strings with a localized separator. This function corresponds
+
+ to QLocale::createSeparatedList which was introduced in Qt 4.8 and implements the algorithm from
+ http://www.unicode.org/reports/tr35/#ListPatterns
+
+ :param string_list: List of unicode strings
+ """
+ if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'):
+ return QtCore.QLocale().createSeparatedList(string_list)
+ if not string_list:
+ return ''
+ elif len(string_list) == 1:
+ return string_list[0]
+ elif len(string_list) == 2:
+ return translate('OpenLP.core.lib', '%s and %s',
+ 'Locale list separator: 2 items') % (string_list[0], string_list[1])
+ else:
+ merged = translate('OpenLP.core.lib', '%s, and %s',
+ 'Locale list separator: end') % (string_list[-2], string_list[-1])
+ for index in reversed(list(range(1, len(string_list) - 2))):
+ merged = translate('OpenLP.core.lib', '%s, %s',
+ 'Locale list separator: middle') % (string_list[index], merged)
+ return translate('OpenLP.core.lib', '%s, %s', 'Locale list separator: start') % (string_list[0], merged)
+
+
+from .colorbutton import ColorButton
+from .exceptions import ValidationError
+from .filedialog import FileDialog
+from .screen import ScreenList
+from .listwidgetwithdnd import ListWidgetWithDnD
+from .treewidgetwithdnd import TreeWidgetWithDnD
+from .formattingtags import FormattingTags
+from .spelltextedit import SpellTextEdit
+from .plugin import PluginStatus, StringContent, Plugin
+from .pluginmanager import PluginManager
+from .settingstab import SettingsTab
+from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
+from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
+from .toolbar import OpenLPToolbar
+from .dockwidget import OpenLPDockWidget
+from .imagemanager import ImageManager
+from .renderer import Renderer
+from .mediamanageritem import MediaManagerItem
+from .projector.db import ProjectorDB, Projector
+from .projector.pjlink1 import PJLink1
+from .projector.constants import PJLINK_PORT, ERROR_MSG, ERROR_STRING
=== added file 'openlp/core/lib/colorbutton.py'
--- openlp/core/lib/colorbutton.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/colorbutton.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+
+"""
+Provide a custom widget based on QPushButton for the selection of colors
+"""
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import translate
+
+
+class ColorButton(QtWidgets.QPushButton):
+ """
+ Subclasses QPushbutton to create a "Color Chooser" button
+ """
+
+ colorChanged = QtCore.pyqtSignal(str)
+
+ def __init__(self, parent=None):
+ """
+ Initialise the ColorButton
+ """
+ super(ColorButton, self).__init__()
+ self.parent = parent
+ self.change_color('#ffffff')
+ self.setToolTip(translate('OpenLP.ColorButton', 'Click to select a color.'))
+ self.clicked.connect(self.on_clicked)
+
+ def change_color(self, color):
+ """
+ Sets the _color variable and the background color.
+
+ :param color: String representation of a hexidecimal color
+ """
+ self._color = color
+ self.setStyleSheet('background-color: %s' % color)
+
+ @property
+ def color(self):
+ """
+ Property method to return the color variable
+
+ :return: String representation of a hexidecimal color
+ """
+ return self._color
+
+ @color.setter
+ def color(self, color):
+ """
+ Property setter to change the instance color
+
+ :param color: String representation of a hexidecimal color
+ """
+ self.change_color(color)
+
+ def on_clicked(self):
+ """
+ Handle the PushButton clicked signal, showing the ColorDialog and validating the input
+ """
+ new_color = QtWidgets.QColorDialog.getColor(QtGui.QColor(self._color), self.parent)
+ if new_color.isValid() and self._color != new_color.name():
+ self.change_color(new_color.name())
+ self.colorChanged.emit(new_color.name())
=== added file 'openlp/core/lib/db.py'
--- openlp/core/lib/db.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/db.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,476 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+
+"""
+The :mod:`db` module provides the core database functionality for OpenLP
+"""
+import logging
+import os
+from urllib.parse import quote_plus as urlquote
+
+from sqlalchemy import Table, MetaData, Column, types, create_engine
+from sqlalchemy.exc import SQLAlchemyError, InvalidRequestError, DBAPIError, OperationalError
+from sqlalchemy.orm import scoped_session, sessionmaker, mapper
+from sqlalchemy.pool import NullPool
+from alembic.migration import MigrationContext
+from alembic.operations import Operations
+
+from openlp.core.common import AppLocation, Settings, translate, delete_file
+from openlp.core.lib.ui import critical_error_message_box
+
+log = logging.getLogger(__name__)
+
+
+def init_db(url, auto_flush=True, auto_commit=False, base=None):
+ """
+ Initialise and return the session and metadata for a database
+
+ :param url: The database to initialise connection with
+ :param auto_flush: Sets the flushing behaviour of the session
+ :param auto_commit: Sets the commit behaviour of the session
+ :param base: If using declarative, the base class to bind with
+ """
+ engine = create_engine(url, poolclass=NullPool)
+ if base is None:
+ metadata = MetaData(bind=engine)
+ else:
+ base.metadata.bind = engine
+ metadata = base.metadata
+ session = scoped_session(sessionmaker(autoflush=auto_flush, autocommit=auto_commit, bind=engine))
+ return session, metadata
+
+
+def get_db_path(plugin_name, db_file_name=None):
+ """
+ Create a path to a database from the plugin name and database name
+
+ :param plugin_name: Name of plugin
+ :param db_file_name: File name of database
+ :return: The path to the database as type str
+ """
+ if db_file_name is None:
+ return 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
+ else:
+ return 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
+
+
+def handle_db_error(plugin_name, db_file_name):
+ """
+ Log and report to the user that a database cannot be loaded
+
+ :param plugin_name: Name of plugin
+ :param db_file_name: File name of database
+ :return: None
+ """
+ db_path = get_db_path(plugin_name, db_file_name)
+ log.exception('Error loading database: %s', db_path)
+ critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
+ translate('OpenLP.Manager', 'OpenLP cannot load your database.\n\nDatabase: %s')
+ % db_path)
+
+
+def init_url(plugin_name, db_file_name=None):
+ """
+ Return the database URL.
+
+ :param plugin_name: The name of the plugin for the database creation.
+ :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
+ """
+ settings = Settings()
+ settings.beginGroup(plugin_name)
+ db_type = settings.value('db type')
+ if db_type == 'sqlite':
+ db_url = get_db_path(plugin_name, db_file_name)
+ else:
+ db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')),
+ urlquote(settings.value('db password')),
+ urlquote(settings.value('db hostname')),
+ urlquote(settings.value('db database')))
+ settings.endGroup()
+ return db_url
+
+
+def get_upgrade_op(session):
+ """
+ Create a migration context and an operations object for performing upgrades.
+
+ :param session: The SQLAlchemy session object.
+ """
+ context = MigrationContext.configure(session.bind.connect())
+ return Operations(context)
+
+
+def upgrade_db(url, upgrade):
+ """
+ Upgrade a database.
+
+ :param url: The url of the database to upgrade.
+ :param upgrade: The python module that contains the upgrade instructions.
+ """
+ session, metadata = init_db(url)
+
+ class Metadata(BaseModel):
+ """
+ Provides a class for the metadata table.
+ """
+ pass
+
+ metadata_table = Table(
+ 'metadata', metadata,
+ Column('key', types.Unicode(64), primary_key=True),
+ Column('value', types.UnicodeText(), default=None)
+ )
+ metadata_table.create(checkfirst=True)
+ mapper(Metadata, metadata_table)
+ version_meta = session.query(Metadata).get('version')
+ if version_meta is None:
+ # Tables have just been created - fill the version field with the most recent version
+ if session.query(Metadata).get('dbversion'):
+ version = 0
+ else:
+ version = upgrade.__version__
+ version_meta = Metadata.populate(key='version', value=version)
+ session.add(version_meta)
+ session.commit()
+ else:
+ version = int(version_meta.value)
+ if version > upgrade.__version__:
+ return version, upgrade.__version__
+ version += 1
+ try:
+ while hasattr(upgrade, 'upgrade_%d' % version):
+ log.debug('Running upgrade_%d', version)
+ try:
+ upgrade_func = getattr(upgrade, 'upgrade_%d' % version)
+ upgrade_func(session, metadata)
+ session.commit()
+ # Update the version number AFTER a commit so that we are sure the previous transaction happened
+ version_meta.value = str(version)
+ session.commit()
+ version += 1
+ except (SQLAlchemyError, DBAPIError):
+ log.exception('Could not run database upgrade script "upgrade_%s", upgrade process has been halted.',
+ version)
+ break
+ except (SQLAlchemyError, DBAPIError):
+ version_meta = Metadata.populate(key='version', value=int(upgrade.__version__))
+ session.commit()
+ upgrade_version = upgrade.__version__
+ version_meta = int(version_meta.value)
+ session.close()
+ return version_meta, upgrade_version
+
+
+def delete_database(plugin_name, db_file_name=None):
+ """
+ Remove a database file from the system.
+
+ :param plugin_name: The name of the plugin to remove the database for
+ :param db_file_name: The database file name. Defaults to None resulting in the plugin_name being used.
+ """
+ if db_file_name:
+ db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), db_file_name)
+ else:
+ db_file_path = os.path.join(AppLocation.get_section_data_path(plugin_name), plugin_name)
+ return delete_file(db_file_path)
+
+
+class BaseModel(object):
+ """
+ BaseModel provides a base object with a set of generic functions
+ """
+ @classmethod
+ def populate(cls, **kwargs):
+ """
+ Creates an instance of a class and populates it, returning the instance
+ """
+ instance = cls()
+ for key, value in kwargs.items():
+ instance.__setattr__(key, value)
+ return instance
+
+
+class Manager(object):
+ """
+ Provide generic object persistence management
+ """
+ def __init__(self, plugin_name, init_schema, db_file_name=None, upgrade_mod=None, session=None):
+ """
+ Runs the initialisation process that includes creating the connection to the database and the tables if they do
+ not exist.
+
+ :param plugin_name: The name to setup paths and settings section names
+ :param init_schema: The init_schema function for this database
+ :param db_file_name: The upgrade_schema function for this database
+ :param upgrade_mod: The file name to use for this database. Defaults to None resulting in the plugin_name
+ being used.
+ """
+ self.is_dirty = False
+ self.session = None
+ self.db_url = None
+ if db_file_name:
+ log.debug('Manager: Creating new DB url')
+ self.db_url = init_url(plugin_name, db_file_name)
+ else:
+ self.db_url = init_url(plugin_name)
+ if upgrade_mod:
+ try:
+ db_ver, up_ver = upgrade_db(self.db_url, upgrade_mod)
+ except (SQLAlchemyError, DBAPIError):
+ handle_db_error(plugin_name, db_file_name)
+ return
+ if db_ver > up_ver:
+ critical_error_message_box(
+ translate('OpenLP.Manager', 'Database Error'),
+ translate('OpenLP.Manager', 'The database being loaded was created in a more recent version of '
+ 'OpenLP. The database is version %d, while OpenLP expects version %d. The database will '
+ 'not be loaded.\n\nDatabase: %s') % (db_ver, up_ver, self.db_url)
+ )
+ return
+ if not session:
+ try:
+ self.session = init_schema(self.db_url)
+ except (SQLAlchemyError, DBAPIError):
+ handle_db_error(plugin_name, db_file_name)
+ else:
+ self.session = session
+
+ def save_object(self, object_instance, commit=True):
+ """
+ Save an object to the database
+
+ :param object_instance: The object to save
+ :param commit: Commit the session with this object
+ """
+ for try_count in range(3):
+ try:
+ self.session.add(object_instance)
+ if commit:
+ self.session.commit()
+ self.is_dirty = True
+ return True
+ except OperationalError:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ log.exception('Probably a MySQL issue - "MySQL has gone away"')
+ self.session.rollback()
+ if try_count >= 2:
+ raise
+ except InvalidRequestError:
+ self.session.rollback()
+ log.exception('Object list save failed')
+ return False
+ except:
+ self.session.rollback()
+ raise
+
+ def save_objects(self, object_list, commit=True):
+ """
+ Save a list of objects to the database
+
+ :param object_list: The list of objects to save
+ :param commit: Commit the session with this object
+ """
+ for try_count in range(3):
+ try:
+ self.session.add_all(object_list)
+ if commit:
+ self.session.commit()
+ self.is_dirty = True
+ return True
+ except OperationalError:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ log.exception('Probably a MySQL issue, "MySQL has gone away"')
+ self.session.rollback()
+ if try_count >= 2:
+ raise
+ except InvalidRequestError:
+ self.session.rollback()
+ log.exception('Object list save failed')
+ return False
+ except:
+ self.session.rollback()
+ raise
+
+ def get_object(self, object_class, key=None):
+ """
+ Return the details of an object
+
+ :param object_class: The type of object to return
+ :param key: The unique reference or primary key for the instance to return
+ """
+ if not key:
+ return object_class()
+ else:
+ for try_count in range(3):
+ try:
+ return self.session.query(object_class).get(key)
+ except OperationalError:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ log.exception('Probably a MySQL issue, "MySQL has gone away"')
+ if try_count >= 2:
+ raise
+
+ def get_object_filtered(self, object_class, filter_clause):
+ """
+ Returns an object matching specified criteria
+
+ :param object_class: The type of object to return
+ :param filter_clause: The criteria to select the object by
+ """
+ for try_count in range(3):
+ try:
+ return self.session.query(object_class).filter(filter_clause).first()
+ except OperationalError as oe:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ if try_count >= 2 or 'MySQL has gone away' in str(oe):
+ raise
+ log.exception('Probably a MySQL issue, "MySQL has gone away"')
+
+ def get_all_objects(self, object_class, filter_clause=None, order_by_ref=None):
+ """
+ Returns all the objects from the database
+
+ :param object_class: The type of objects to return
+ :param filter_clause: The filter governing selection of objects to return. Defaults to None.
+ :param order_by_ref: Any parameters to order the returned objects by. Defaults to None.
+ """
+ query = self.session.query(object_class)
+ if filter_clause is not None:
+ query = query.filter(filter_clause)
+ if isinstance(order_by_ref, list):
+ query = query.order_by(*order_by_ref)
+ elif order_by_ref is not None:
+ query = query.order_by(order_by_ref)
+ for try_count in range(3):
+ try:
+ return query.all()
+ except OperationalError:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ log.exception('Probably a MySQL issue, "MySQL has gone away"')
+ if try_count >= 2:
+ raise
+
+ def get_object_count(self, object_class, filter_clause=None):
+ """
+ Returns a count of the number of objects in the database.
+
+ :param object_class: The type of objects to return.
+ :param filter_clause: The filter governing selection of objects to return. Defaults to None.
+ """
+ query = self.session.query(object_class)
+ if filter_clause is not None:
+ query = query.filter(filter_clause)
+ for try_count in range(3):
+ try:
+ return query.count()
+ except OperationalError:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ log.exception('Probably a MySQL issue, "MySQL has gone away"')
+ if try_count >= 2:
+ raise
+
+ def delete_object(self, object_class, key):
+ """
+ Delete an object from the database
+
+ :param object_class: The type of object to delete
+ :param key: The unique reference or primary key for the instance to be deleted
+ """
+ if key != 0:
+ object_instance = self.get_object(object_class, key)
+ for try_count in range(3):
+ try:
+ self.session.delete(object_instance)
+ self.session.commit()
+ self.is_dirty = True
+ return True
+ except OperationalError:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ log.exception('Probably a MySQL issue, "MySQL has gone away"')
+ self.session.rollback()
+ if try_count >= 2:
+ raise
+ except InvalidRequestError:
+ self.session.rollback()
+ log.exception('Failed to delete object')
+ return False
+ except:
+ self.session.rollback()
+ raise
+ else:
+ return True
+
+ def delete_all_objects(self, object_class, filter_clause=None):
+ """
+ Delete all object records. This method should only be used for simple tables and **not** ones with
+ relationships. The relationships are not deleted from the database and this will lead to database corruptions.
+
+ :param object_class: The type of object to delete
+ :param filter_clause: The filter governing selection of objects to return. Defaults to None.
+ """
+ for try_count in range(3):
+ try:
+ query = self.session.query(object_class)
+ if filter_clause is not None:
+ query = query.filter(filter_clause)
+ query.delete(synchronize_session=False)
+ self.session.commit()
+ self.is_dirty = True
+ return True
+ except OperationalError:
+ # This exception clause is for users running MySQL which likes to terminate connections on its own
+ # without telling anyone. See bug #927473. However, other dbms can raise it, usually in a
+ # non-recoverable way. So we only retry 3 times.
+ log.exception('Probably a MySQL issue, "MySQL has gone away"')
+ self.session.rollback()
+ if try_count >= 2:
+ raise
+ except InvalidRequestError:
+ self.session.rollback()
+ log.exception('Failed to delete %s records', object_class.__name__)
+ return False
+ except:
+ self.session.rollback()
+ raise
+
+ def finalise(self):
+ """
+ VACUUM the database on exit.
+ """
+ if self.is_dirty:
+ engine = create_engine(self.db_url)
+ if self.db_url.startswith('sqlite'):
+ engine.execute("vacuum")
=== added file 'openlp/core/lib/dockwidget.py'
--- openlp/core/lib/dockwidget.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/dockwidget.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+
+"""
+Provide additional functionality required by OpenLP from the inherited QDockWidget.
+"""
+
+import logging
+
+from PyQt5 import QtWidgets
+
+from openlp.core.lib import ScreenList, build_icon
+
+log = logging.getLogger(__name__)
+
+
+class OpenLPDockWidget(QtWidgets.QDockWidget):
+ """
+ Custom DockWidget class to handle events
+ """
+ def __init__(self, parent=None, name=None, icon=None):
+ """
+ Initialise the DockWidget
+ """
+ log.debug('Initialise the %s widget' % name)
+ super(OpenLPDockWidget, self).__init__(parent)
+ if name:
+ self.setObjectName(name)
+ if icon:
+ self.setWindowIcon(build_icon(icon))
+ # Sort out the minimum width.
+ screens = ScreenList()
+ main_window_docbars = screens.current['size'].width() // 5
+ if main_window_docbars > 300:
+ self.setMinimumWidth(300)
+ else:
+ self.setMinimumWidth(main_window_docbars)
=== added file 'openlp/core/lib/exceptions.py'
--- openlp/core/lib/exceptions.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/exceptions.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`~openlp.core.lib.exceptions` module contains custom exceptions
+"""
+
+
+class ValidationError(Exception):
+ """
+ The :class:`~openlp.core.lib.exceptions.ValidationError` exception provides a custom exception for validating
+ import files.
+ """
+ pass
=== added file 'openlp/core/lib/filedialog.py'
--- openlp/core/lib/filedialog.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/filedialog.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide a work around for a bug in QFileDialog <https://bugs.launchpad.net/openlp/+bug/1209515>
+"""
+import logging
+import os
+from urllib import parse
+
+from PyQt5 import QtWidgets
+
+from openlp.core.common import UiStrings
+
+log = logging.getLogger(__name__)
+
+
+class FileDialog(QtWidgets.QFileDialog):
+ """
+ Subclass QFileDialog to work round a bug
+ """
+ @staticmethod
+ def getOpenFileNames(parent, *args, **kwargs):
+ """
+ Reimplement getOpenFileNames to fix the way it returns some file names that url encoded when selecting multiple
+ files
+ """
+ files, filter_used = QtWidgets.QFileDialog.getOpenFileNames(parent, *args, **kwargs)
+ file_list = []
+ for file in files:
+ if not os.path.exists(file):
+ log.info('File not found. Attempting to unquote.')
+ file = parse.unquote(file)
+ if not os.path.exists(file):
+ log.error('File %s not found.' % file)
+ QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound,
+ UiStrings().FileNotFoundMessage % file)
+ continue
+ file_list.append(file)
+ return file_list
=== added file 'openlp/core/lib/formattingtags.py'
--- openlp/core/lib/formattingtags.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/formattingtags.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide HTML Tag management and Formatting Tag access class
+"""
+import json
+
+from openlp.core.common import Settings
+from openlp.core.lib import translate
+
+
+class FormattingTags(object):
+ """
+ Static Class for HTML Tags to be access around the code the list is managed by the Options Tab.
+ """
+ html_expands = []
+
+ @staticmethod
+ def get_html_tags():
+ """
+ Provide access to the html_expands list.
+ """
+ return FormattingTags.html_expands
+
+ @staticmethod
+ def save_html_tags(new_tags):
+ """
+ Saves all formatting tags except protected ones
+
+ `new_tags`
+ The tags to be saved..
+ """
+ # Formatting Tags were also known as display tags.
+ Settings().setValue('formattingTags/html_tags', json.dumps(new_tags) if new_tags else '')
+
+ @staticmethod
+ def load_tags():
+ """
+ Load the Tags from store so can be used in the system or used to update the display.
+ """
+ temporary_tags = [tag for tag in FormattingTags.html_expands if tag.get('temporary')]
+ FormattingTags.html_expands = []
+ base_tags = []
+ # Append the base tags.
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Red'),
+ 'start tag': '{r}',
+ 'start html': '<span style="-webkit-text-fill-color:red">',
+ 'end tag': '{/r}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Black'),
+ 'start tag': '{b}',
+ 'start html': '<span style="-webkit-text-fill-color:black">',
+ 'end tag': '{/b}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Blue'),
+ 'start tag': '{bl}',
+ 'start html': '<span style="-webkit-text-fill-color:blue">',
+ 'end tag': '{/bl}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Yellow'),
+ 'start tag': '{y}',
+ 'start html': '<span style="-webkit-text-fill-color:yellow">',
+ 'end tag': '{/y}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Green'),
+ 'start tag': '{g}',
+ 'start html': '<span style="-webkit-text-fill-color:green">',
+ 'end tag': '{/g}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Pink'),
+ 'start tag': '{pk}',
+ 'start html': '<span style="-webkit-text-fill-color:#FFC0CB">',
+ 'end tag': '{/pk}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Orange'),
+ 'start tag': '{o}',
+ 'start html': '<span style="-webkit-text-fill-color:#FFA500">',
+ 'end tag': '{/o}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Purple'),
+ 'start tag': '{pp}',
+ 'start html': '<span style="-webkit-text-fill-color:#800080">',
+ 'end tag': '{/pp}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'White'),
+ 'start tag': '{w}',
+ 'start html': '<span style="-webkit-text-fill-color:white">',
+ 'end tag': '{/w}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Superscript'),
+ 'start tag': '{su}', 'start html': '<sup>',
+ 'end tag': '{/su}', 'end html': '</sup>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Subscript'),
+ 'start tag': '{sb}', 'start html': '<sub>',
+ 'end tag': '{/sb}', 'end html': '</sub>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Paragraph'),
+ 'start tag': '{p}', 'start html': '<p>', 'end tag': '{/p}',
+ 'end html': '</p>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Bold'),
+ 'start tag': '{st}', 'start html': '<strong>',
+ 'end tag': '{/st}', 'end html': '</strong>',
+ 'protected': True, 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Italics'),
+ 'start tag': '{it}', 'start html': '<em>', 'end tag': '{/it}',
+ 'end html': '</em>', 'protected': True, 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Underline'),
+ 'start tag': '{u}',
+ 'start html': '<span style="text-decoration: underline;">',
+ 'end tag': '{/u}', 'end html': '</span>', 'protected': True,
+ 'temporary': False})
+ base_tags.append({
+ 'desc': translate('OpenLP.FormattingTags', 'Break'),
+ 'start tag': '{br}', 'start html': '<br>', 'end tag': '',
+ 'end html': '', 'protected': True,
+ 'temporary': False})
+ FormattingTags.add_html_tags(base_tags)
+ FormattingTags.add_html_tags(temporary_tags)
+ user_expands_string = str(Settings().value('formattingTags/html_tags'))
+ # If we have some user ones added them as well
+ if user_expands_string:
+ user_tags = json.loads(user_expands_string)
+ FormattingTags.add_html_tags(user_tags)
+
+ @staticmethod
+ def add_html_tags(tags):
+ """
+ Add a list of tags to the list.
+
+ :param tags: The list with tags to add.
+ Each **tag** has to be a ``dict`` and should have the following keys:
+
+ * desc
+ The formatting tag's description, e. g. **Red**
+
+ * start tag
+ The start tag, e. g. ``{r}``
+
+ * end tag
+ The end tag, e. g. ``{/r}``
+
+ * start html
+ The start html tag. For instance ``<span style="-webkit-text-fill-color:red">``
+
+ * end html
+ The end html tag. For example ``</span>``
+
+ * protected
+ A boolean stating whether this is a build-in tag or not. Should be ``True`` in most cases.
+
+ * temporary
+ A temporary tag will not be saved, but is also considered when displaying text containing the tag. It
+ has to be a ``boolean``.
+ """
+ FormattingTags.html_expands.extend(tags)
+
+ @staticmethod
+ def remove_html_tag(tag_id):
+ """
+ Removes an individual html_expands tag.
+ """
+ FormattingTags.html_expands.pop(tag_id)
=== added file 'openlp/core/lib/htmlbuilder.py'
--- openlp/core/lib/htmlbuilder.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/htmlbuilder.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,756 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 is responsible for generating the HTML for :class:`~openlp.core.ui.maindisplay`. The ``build_html`` function
+is the function which has to be called from outside. The generated and returned HTML will look similar to this::
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>OpenLP Display</title>
+ <style>
+ *{
+ margin: 0;
+ padding: 0;
+ border: 0;
+ overflow: hidden;
+ -webkit-user-select: none;
+ }
+ body {
+ background-color: #000000;
+ }
+ .size {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100%;
+ height: 100%;
+ }
+ #black {
+ z-index: 8;
+ background-color: black;
+ display: none;
+ }
+ #bgimage {
+ z-index: 1;
+ }
+ #image {
+ z-index: 2;
+ }
+
+ #videobackboard {
+ z-index:3;
+ background-color: #000000;
+ }
+ #video {
+ background-color: #000000;
+ z-index:4;
+ }
+
+ #flash {
+ z-index:5;
+ }
+
+ #alert {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ z-index: 10;
+ width: 100%;
+ vertical-align: bottom;
+ font-family: DejaVu Sans;
+ font-size: 40pt;
+ color: #ffffff;
+ background-color: #660000;
+ word-wrap: break-word;
+ }
+
+ #footer {
+ position: absolute;
+ z-index: 6;
+
+ left: 10px;
+ bottom: 0px;
+ width: 1580px;
+ font-family: Nimbus Sans L;
+ font-size: 12pt;
+ color: #FFFFFF;
+ text-align: left;
+ white-space: nowrap;
+
+ }
+ /* lyric css */
+
+ .lyricstable {
+ z-index: 5;
+ position: absolute;
+ display: table;
+ left: 10px; top: 0px;
+ }
+ .lyricscell {
+ display: table-cell;
+ word-wrap: break-word;
+ -webkit-transition: opacity 0.4s ease;
+ white-space:pre-wrap; word-wrap: break-word; text-align: left; vertical-align: top; font-family: Nimbus
+ Sans L; font-size: 40pt; color: #FFFFFF; line-height: 100%; margin: 0;padding: 0; padding-bottom: 0;
+ padding-left: 4px; width: 1580px; height: 810px;
+ }
+ .lyricsmain {
+ -webkit-text-stroke: 0.125em #000000; -webkit-text-fill-color: #FFFFFF; text-shadow: #000000 5px 5px;
+ }
+
+ sup {
+ font-size: 0.6em;
+ vertical-align: top;
+ position: relative;
+ top: -0.3em;
+ }
+ </style>
+ <script>
+ var timer = null;
+ var transition = false;
+
+ function show_video(state, path, volume, loop, variable_value){
+ // Sometimes video.currentTime stops slightly short of video.duration and video.ended is intermittent!
+
+ var video = document.getElementById('video');
+ if(volume != null){
+ video.volume = volume;
+ }
+ switch(state){
+ case 'load':
+ video.src = 'file:///' + path;
+ if(loop == true) {
+ video.loop = true;
+ }
+ video.load();
+ break;
+ case 'play':
+ video.play();
+ break;
+ case 'pause':
+ video.pause();
+ break;
+ case 'stop':
+ show_video('pause');
+ video.currentTime = 0;
+ break;
+ case 'close':
+ show_video('stop');
+ video.src = '';
+ break;
+ case 'length':
+ return video.duration;
+ case 'current_time':
+ return video.currentTime;
+ case 'seek':
+ video.currentTime = variable_value;
+ break;
+ case 'isEnded':
+ return video.ended;
+ case 'setVisible':
+ video.style.visibility = variable_value;
+ break;
+ case 'setBackBoard':
+ var back = document.getElementById('videobackboard');
+ back.style.visibility = variable_value;
+ break;
+ }
+ }
+
+ function getFlashMovieObject(movieName)
+ {
+ if (window.document[movieName]){
+ return window.document[movieName];
+ }
+ if (document.embeds && document.embeds[movieName]){
+ return document.embeds[movieName];
+ }
+ }
+
+ function show_flash(state, path, volume, variable_value){
+ var text = document.getElementById('flash');
+ var flashMovie = getFlashMovieObject("OpenLPFlashMovie");
+ var src = "src = 'file:///" + path + "'";
+ var view_parm = " wmode='opaque'" + " width='100%%'" + " height='100%%'";
+ var swf_parm = " name='OpenLPFlashMovie'" + " autostart='true' loop='false' play='true'" +
+ " hidden='false' swliveconnect='true' allowscriptaccess='always'" + " volume='" + volume + "'";
+
+ switch(state){
+ case 'load':
+ text.innerHTML = "<embed " + src + view_parm + swf_parm + "/>";
+ flashMovie = getFlashMovieObject("OpenLPFlashMovie");
+ flashMovie.Play();
+ break;
+ case 'play':
+ flashMovie.Play();
+ break;
+ case 'pause':
+ flashMovie.StopPlay();
+ break;
+ case 'stop':
+ flashMovie.StopPlay();
+ tempHtml = text.innerHTML;
+ text.innerHTML = '';
+ text.innerHTML = tempHtml;
+ break;
+ case 'close':
+ flashMovie.StopPlay();
+ text.innerHTML = '';
+ break;
+ case 'length':
+ return flashMovie.TotalFrames();
+ case 'current_time':
+ return flashMovie.CurrentFrame();
+ case 'seek':
+ // flashMovie.GotoFrame(variable_value);
+ break;
+ case 'isEnded':
+ //TODO check flash end
+ return false;
+ case 'setVisible':
+ text.style.visibility = variable_value;
+ break;
+ }
+ }
+
+ function show_alert(alerttext, position){
+ var text = document.getElementById('alert');
+ text.innerHTML = alerttext;
+ if(alerttext == '') {
+ text.style.visibility = 'hidden';
+ return 0;
+ }
+ if(position == ''){
+ position = getComputedStyle(text, '').verticalAlign;
+ }
+ switch(position)
+ {
+ case 'top':
+ text.style.top = '0px';
+ break;
+ case 'middle':
+ text.style.top = ((window.innerHeight - text.clientHeight) / 2)
+ + 'px';
+ break;
+ case 'bottom':
+ text.style.top = (window.innerHeight - text.clientHeight)
+ + 'px';
+ break;
+ }
+ text.style.visibility = 'visible';
+ return text.clientHeight;
+ }
+
+ function update_css(align, font, size, color, bgcolor){
+ var text = document.getElementById('alert');
+ text.style.fontSize = size + "pt";
+ text.style.fontFamily = font;
+ text.style.color = color;
+ text.style.backgroundColor = bgcolor;
+ switch(align)
+ {
+ case 'top':
+ text.style.top = '0px';
+ break;
+ case 'middle':
+ text.style.top = ((window.innerHeight - text.clientHeight) / 2)
+ + 'px';
+ break;
+ case 'bottom':
+ text.style.top = (window.innerHeight - text.clientHeight)
+ + 'px';
+ break;
+ }
+ }
+
+
+ function show_image(src){
+ var img = document.getElementById('image');
+ img.src = src;
+ if(src == '')
+ img.style.display = 'none';
+ else
+ img.style.display = 'block';
+ }
+
+ function show_blank(state){
+ var black = 'none';
+ var lyrics = '';
+ switch(state){
+ case 'theme':
+ lyrics = 'hidden';
+ break;
+ case 'black':
+ black = 'block';
+ break;
+ case 'desktop':
+ break;
+ }
+ document.getElementById('black').style.display = black;
+ document.getElementById('lyricsmain').style.visibility = lyrics;
+ document.getElementById('image').style.visibility = lyrics;
+ document.getElementById('footer').style.visibility = lyrics;
+ }
+
+ function show_footer(footertext){
+ document.getElementById('footer').innerHTML = footertext;
+ }
+
+ function show_text(new_text){
+ var match = /-webkit-text-fill-color:[^;"]+/gi;
+ if(timer != null)
+ clearTimeout(timer);
+ /*
+ QtWebkit bug with outlines and justify causing outline alignment
+ problems. (Bug 859950) Surround each word with a <span> to workaround,
+ but only in this scenario.
+ */
+ var txt = document.getElementById('lyricsmain');
+ if(window.getComputedStyle(txt).textAlign == 'justify'){
+ if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
+ new_text = new_text.replace(/(\s| )+(?![^<]*>)/g,
+ function(match) {
+ return '</span>' + match + '<span>';
+ });
+ new_text = '<span>' + new_text + '</span>';
+ }
+ }
+ text_fade('lyricsmain', new_text);
+ }
+
+ function text_fade(id, new_text){
+ /*
+ Show the text.
+ */
+ var text = document.getElementById(id);
+ if(text == null) return;
+ if(!transition){
+ text.innerHTML = new_text;
+ return;
+ }
+ // Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
+ text.style.opacity = '0.1';
+ // Fade new text in after the old text has finished fading out.
+ timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
+ }
+
+ function _show_text(text, new_text) {
+ /*
+ Helper function to show the new_text delayed.
+ */
+ text.innerHTML = new_text;
+ text.style.opacity = '1';
+ // Wait until the text is completely visible. We want to save the timer id, to be able to call
+ // clearTimeout(timer) when the text has changed before finishing fading.
+ timer = window.setTimeout(function(){timer = null;}, 400);
+ }
+
+ function show_text_completed(){
+ return (timer == null);
+ }
+ </script>
+ </head>
+ <body>
+ <img id="bgimage" class="size" style="display:none;" />
+ <img id="image" class="size" style="display:none;" />
+
+ <div id="videobackboard" class="size" style="visibility:hidden"></div>
+ <video id="video" class="size" style="visibility:hidden" autobuffer preload></video>
+
+ <div id="flash" class="size" style="visibility:hidden"></div>
+
+ <div id="alert" style="visibility:hidden"></div>
+
+ <div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
+ <div id="footer" class="footer"></div>
+ <div id="black" class="size"></div>
+ </body>
+ </html>
+"""
+import logging
+
+from PyQt5 import QtWebKit
+
+from openlp.core.common import Settings
+from openlp.core.lib.theme import BackgroundType, BackgroundGradientType, VerticalType, HorizontalType
+
+log = logging.getLogger(__name__)
+
+HTMLSRC = """
+<!DOCTYPE html>
+<html>
+<head>
+<title>OpenLP Display</title>
+<style>
+*{
+ margin: 0;
+ padding: 0;
+ border: 0;
+ overflow: hidden;
+ -webkit-user-select: none;
+}
+body {
+ %s;
+}
+.size {
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: 100%%;
+ height: 100%%;
+}
+#black {
+ z-index: 8;
+ background-color: black;
+ display: none;
+}
+#bgimage {
+ z-index: 1;
+}
+#image {
+ z-index: 2;
+}
+%s
+#footer {
+ position: absolute;
+ z-index: 6;
+ %s
+}
+/* lyric css */
+%s
+sup {
+ font-size: 0.6em;
+ vertical-align: top;
+ position: relative;
+ top: -0.3em;
+}
+</style>
+<script>
+ var timer = null;
+ var transition = %s;
+ %s
+
+ function show_image(src){
+ var img = document.getElementById('image');
+ img.src = src;
+ if(src == '')
+ img.style.display = 'none';
+ else
+ img.style.display = 'block';
+ }
+
+ function show_blank(state){
+ var black = 'none';
+ var lyrics = '';
+ switch(state){
+ case 'theme':
+ lyrics = 'hidden';
+ break;
+ case 'black':
+ black = 'block';
+ break;
+ case 'desktop':
+ break;
+ }
+ document.getElementById('black').style.display = black;
+ document.getElementById('lyricsmain').style.visibility = lyrics;
+ document.getElementById('image').style.visibility = lyrics;
+ document.getElementById('footer').style.visibility = lyrics;
+ }
+
+ function show_footer(footertext){
+ document.getElementById('footer').innerHTML = footertext;
+ }
+
+ function show_text(new_text){
+ var match = /-webkit-text-fill-color:[^;\"]+/gi;
+ if(timer != null)
+ clearTimeout(timer);
+ /*
+ QtWebkit bug with outlines and justify causing outline alignment
+ problems. (Bug 859950) Surround each word with a <span> to workaround,
+ but only in this scenario.
+ */
+ var txt = document.getElementById('lyricsmain');
+ if(window.getComputedStyle(txt).textAlign == 'justify'){
+ if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
+ new_text = new_text.replace(/(\s| )+(?![^<]*>)/g,
+ function(match) {
+ return '</span>' + match + '<span>';
+ });
+ new_text = '<span>' + new_text + '</span>';
+ }
+ }
+ text_fade('lyricsmain', new_text);
+ }
+
+ function text_fade(id, new_text){
+ /*
+ Show the text.
+ */
+ var text = document.getElementById(id);
+ if(text == null) return;
+ if(!transition){
+ text.innerHTML = new_text;
+ return;
+ }
+ // Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
+ text.style.opacity = '0.1';
+ // Fade new text in after the old text has finished fading out.
+ timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
+ }
+
+ function _show_text(text, new_text) {
+ /*
+ Helper function to show the new_text delayed.
+ */
+ text.innerHTML = new_text;
+ text.style.opacity = '1';
+ // Wait until the text is completely visible. We want to save the timer id, to be able to call
+ // clearTimeout(timer) when the text has changed before finishing fading.
+ timer = window.setTimeout(function(){timer = null;}, 400);
+ }
+
+ function show_text_completed(){
+ return (timer == null);
+ }
+</script>
+</head>
+<body>
+<img id="bgimage" class="size" %s />
+<img id="image" class="size" %s />
+%s
+<div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
+<div id="footer" class="footer"></div>
+<div id="black" class="size"></div>
+</body>
+</html>
+"""
+
+
+def build_html(item, screen, is_live, background, image=None, plugins=None):
+ """
+ Build the full web paged structure for display
+
+ :param item: Service Item to be displayed
+ :param screen: Current display information
+ :param is_live: Item is going live, rather than preview/theme building
+ :param background: Theme background image - bytes
+ :param image: Image media item - bytes
+ :param plugins: The List of available plugins
+ """
+ width = screen['size'].width()
+ height = screen['size'].height()
+ theme_data = item.theme_data
+ # Image generated and poked in
+ if background:
+ bgimage_src = 'src="data:image/png;base64,%s"' % background
+ elif item.bg_image_bytes:
+ bgimage_src = 'src="data:image/png;base64,%s"' % item.bg_image_bytes
+ else:
+ bgimage_src = 'style="display:none;"'
+ if image:
+ image_src = 'src="data:image/png;base64,%s"' % image
+ else:
+ image_src = 'style="display:none;"'
+ css_additions = ''
+ js_additions = ''
+ html_additions = ''
+ if plugins:
+ for plugin in plugins:
+ css_additions += plugin.get_display_css()
+ js_additions += plugin.get_display_javascript()
+ html_additions += plugin.get_display_html()
+ html = HTMLSRC % (
+ build_background_css(item, width),
+ css_additions,
+ build_footer_css(item, height),
+ build_lyrics_css(item),
+ 'true' if theme_data and theme_data.display_slide_transition and is_live else 'false',
+ js_additions,
+ bgimage_src,
+ image_src,
+ html_additions
+ )
+ return html
+
+
+def webkit_version():
+ """
+ Return the Webkit version in use. Note method added relatively recently, so return 0 if prior to this
+ """
+ try:
+ webkit_ver = float(QtWebKit.qWebKitVersion())
+ log.debug('Webkit version = %s' % webkit_ver)
+ except AttributeError:
+ webkit_ver = 0
+ return webkit_ver
+
+
+def build_background_css(item, width):
+ """
+ Build the background css
+
+ :param item: Service Item containing theme and location information
+ :param width:
+ """
+ width = int(width) // 2
+ theme = item.theme_data
+ background = 'background-color: black'
+ if theme:
+ if theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
+ background = ''
+ elif theme.background_type == BackgroundType.to_string(BackgroundType.Solid):
+ background = 'background-color: %s' % theme.background_color
+ else:
+ if theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Horizontal):
+ background = 'background: -webkit-gradient(linear, left top, left bottom, from(%s), to(%s)) fixed' \
+ % (theme.background_start_color, theme.background_end_color)
+ elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftTop):
+ background = 'background: -webkit-gradient(linear, left top, right bottom, from(%s), to(%s)) fixed' \
+ % (theme.background_start_color, theme.background_end_color)
+ elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftBottom):
+ background = 'background: -webkit-gradient(linear, left bottom, right top, from(%s), to(%s)) fixed' \
+ % (theme.background_start_color, theme.background_end_color)
+ elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Vertical):
+ background = 'background: -webkit-gradient(linear, left top, right top, from(%s), to(%s)) fixed' % \
+ (theme.background_start_color, theme.background_end_color)
+ else:
+ background = 'background: -webkit-gradient(radial, %s 50%%, 100, %s 50%%, %s, from(%s), to(%s)) fixed'\
+ % (width, width, width, theme.background_start_color, theme.background_end_color)
+ return background
+
+
+def build_lyrics_css(item):
+ """
+ Build the lyrics display css
+
+ :param item: Service Item containing theme and location information
+ """
+ style = """
+.lyricstable {
+ z-index: 5;
+ position: absolute;
+ display: table;
+ %s
+}
+.lyricscell {
+ display: table-cell;
+ word-wrap: break-word;
+ -webkit-transition: opacity 0.4s ease;
+ %s
+}
+.lyricsmain {
+ %s
+}
+"""
+ theme_data = item.theme_data
+ lyricstable = ''
+ lyrics = ''
+ lyricsmain = ''
+ if theme_data and item.main:
+ lyricstable = 'left: %spx; top: %spx;' % (item.main.x(), item.main.y())
+ lyrics = build_lyrics_format_css(theme_data, item.main.width(), item.main.height())
+ lyricsmain += build_lyrics_outline_css(theme_data)
+ if theme_data.font_main_shadow:
+ lyricsmain += ' text-shadow: %s %spx %spx;' % \
+ (theme_data.font_main_shadow_color, theme_data.font_main_shadow_size, theme_data.font_main_shadow_size)
+ lyrics_css = style % (lyricstable, lyrics, lyricsmain)
+ return lyrics_css
+
+
+def build_lyrics_outline_css(theme_data):
+ """
+ Build the css which controls the theme outline. Also used by renderer for splitting verses
+
+ :param theme_data: Object containing theme information
+ """
+ if theme_data.font_main_outline:
+ size = float(theme_data.font_main_outline_size) / 16
+ fill_color = theme_data.font_main_color
+ outline_color = theme_data.font_main_outline_color
+ return ' -webkit-text-stroke: %sem %s; -webkit-text-fill-color: %s; ' % (size, outline_color, fill_color)
+ return ''
+
+
+def build_lyrics_format_css(theme_data, width, height):
+ """
+ Build the css which controls the theme format. Also used by renderer for splitting verses
+
+ :param theme_data: Object containing theme information
+ :param width: Width of the lyrics block
+ :param height: Height of the lyrics block
+ """
+ align = HorizontalType.Names[theme_data.display_horizontal_align]
+ valign = VerticalType.Names[theme_data.display_vertical_align]
+ if theme_data.font_main_outline:
+ left_margin = int(theme_data.font_main_outline_size) * 2
+ else:
+ left_margin = 0
+ justify = 'white-space:pre-wrap;'
+ # fix tag incompatibilities
+ if theme_data.display_horizontal_align == HorizontalType.Justify:
+ justify = ''
+ if theme_data.display_vertical_align == VerticalType.Bottom:
+ padding_bottom = '0.5em'
+ else:
+ padding_bottom = '0'
+ lyrics = '%s word-wrap: break-word; ' \
+ 'text-align: %s; vertical-align: %s; font-family: %s; ' \
+ 'font-size: %spt; color: %s; line-height: %d%%; margin: 0;' \
+ 'padding: 0; padding-bottom: %s; padding-left: %spx; width: %spx; height: %spx; ' % \
+ (justify, align, valign, theme_data.font_main_name, theme_data.font_main_size,
+ theme_data.font_main_color, 100 + int(theme_data.font_main_line_adjustment), padding_bottom,
+ left_margin, width, height)
+ if theme_data.font_main_italics:
+ lyrics += 'font-style:italic; '
+ if theme_data.font_main_bold:
+ lyrics += 'font-weight:bold; '
+ return lyrics
+
+
+def build_footer_css(item, height):
+ """
+ Build the display of the item footer
+
+ :param item: Service Item to be processed.
+ :param height:
+ """
+ style = """
+ left: %spx;
+ bottom: %spx;
+ width: %spx;
+ font-family: %s;
+ font-size: %spt;
+ color: %s;
+ text-align: left;
+ white-space: %s;
+ """
+ theme = item.theme_data
+ if not theme or not item.footer:
+ return ''
+ bottom = height - int(item.footer.y()) - int(item.footer.height())
+ whitespace = 'normal' if Settings().value('themes/wrap footer') else 'nowrap'
+ lyrics_html = style % (item.footer.x(), bottom, item.footer.width(),
+ theme.font_footer_name, theme.font_footer_size, theme.font_footer_color, whitespace)
+ return lyrics_html
=== added file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/imagemanager.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,321 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provides the store and management for Images automatically caching them and resizing them when needed. Only one copy of
+each image is needed in the system. A Thread is used to convert the image to a byte array so the user does not need to
+wait for the conversion to happen.
+"""
+import logging
+import os
+import time
+import queue
+
+from PyQt5 import QtCore
+
+from openlp.core.common import Registry
+from openlp.core.lib import ScreenList, resize_image, image_to_byte
+
+log = logging.getLogger(__name__)
+
+
+class ImageThread(QtCore.QThread):
+ """
+ A special Qt thread class to speed up the display of images. This is threaded so it loads the frames and generates
+ byte stream in background.
+ """
+ def __init__(self, manager):
+ """
+ Constructor for the thread class.
+
+ ``manager``
+ The image manager.
+ """
+ super(ImageThread, self).__init__(None)
+ self.image_manager = manager
+
+ def run(self):
+ """
+ Run the thread.
+ """
+ self.image_manager._process()
+
+
+class Priority(object):
+ """
+ Enumeration class for different priorities.
+
+ ``Lowest``
+ Only the image's byte stream has to be generated. But neither the ``QImage`` nor the byte stream has been
+ requested yet.
+
+ ``Low``
+ Only the image's byte stream has to be generated. Because the image's ``QImage`` has been requested previously
+ it is reasonable to assume that the byte stream will be needed before the byte stream of other images whose
+ ``QImage`` were not generated due to a request.
+
+ ``Normal``
+ The image's byte stream as well as the image has to be generated. Neither the ``QImage`` nor the byte stream has
+ been requested yet.
+
+ ``High``
+ The image's byte stream as well as the image has to be generated. The ``QImage`` for this image has been
+ requested. **Note**, this priority is only set when the ``QImage`` has not been generated yet.
+
+ ``Urgent``
+ The image's byte stream as well as the image has to be generated. The byte stream for this image has been
+ requested. **Note**, this priority is only set when the byte stream has not been generated yet.
+ """
+ Lowest = 4
+ Low = 3
+ Normal = 2
+ High = 1
+ Urgent = 0
+
+
+class Image(object):
+ """
+ This class represents an image. To mark an image as *dirty* call the :class:`ImageManager`'s ``_reset_image`` method
+ with the Image instance as argument.
+ """
+ secondary_priority = 0
+
+ def __init__(self, path, source, background, width=-1, height=-1):
+ """
+ Create an image for the :class:`ImageManager`'s cache.
+
+ :param path: The image's file path. This should be an existing file path.
+ :param source: The source describes the image's origin. Possible values are described in the
+ :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
+ self.image_bytes = None
+ self.priority = Priority.Normal
+ 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
+ self.secondary_priority = Image.secondary_priority
+ Image.secondary_priority += 1
+
+
+class PriorityQueue(queue.PriorityQueue):
+ """
+ Customised ``Queue.PriorityQueue``.
+
+ Each item in the queue must be a tuple with three values. The first value is the :class:`Image`'s ``priority``
+ attribute, the second value the :class:`Image`'s ``secondary_priority`` attribute. The last value the :class:`Image`
+ instance itself::
+
+ (image.priority, image.secondary_priority, image)
+
+ Doing this, the :class:`Queue.PriorityQueue` will sort the images according to their priorities, but also according
+ to there number. However, the number only has an impact on the result if there are more images with the same
+ priority. In such case the image which has been added earlier is privileged.
+ """
+ def modify_priority(self, image, new_priority):
+ """
+ Modifies the priority of the given ``image``.
+
+ :param image: The image to remove. This should be an :class:`Image` instance.
+ :param new_priority: The image's new priority. See the :class:`Priority` class for priorities.
+ """
+ self.remove(image)
+ image.priority = new_priority
+ self.put((image.priority, image.secondary_priority, image))
+
+ def remove(self, image):
+ """
+ Removes the given ``image`` from the queue.
+
+ :param image: The image to remove. This should be an ``Image`` instance.
+ """
+ if (image.priority, image.secondary_priority, image) in self.queue:
+ self.queue.remove((image.priority, image.secondary_priority, image))
+
+
+class ImageManager(QtCore.QObject):
+ """
+ Image Manager handles the conversion and sizing of images.
+ """
+ log.info('Image Manager loaded')
+
+ def __init__(self):
+ """
+ Constructor for the image manager.
+ """
+ super(ImageManager, self).__init__()
+ Registry().register('image_manager', self)
+ current_screen = ScreenList().current
+ self.width = current_screen['size'].width()
+ self.height = current_screen['size'].height()
+ self._cache = {}
+ self.image_thread = ImageThread(self)
+ self._conversion_queue = PriorityQueue()
+ self.stop_manager = False
+ Registry().register_function('images_regenerate', self.process_updates)
+
+ def update_display(self):
+ """
+ Screen has changed size so rebuild the cache to new size.
+ """
+ log.debug('update_display')
+ current_screen = ScreenList().current
+ self.width = current_screen['size'].width()
+ self.height = current_screen['size'].height()
+ # Mark the images as dirty for a rebuild by setting the image and byte stream to None.
+ for image in list(self._cache.values()):
+ self._reset_image(image)
+
+ def update_images_border(self, source, background):
+ """
+ Border has changed so update all the images affected.
+ """
+ log.debug('update_images_border')
+ # Mark the images as dirty for a rebuild by setting the image and byte stream to None.
+ for image in list(self._cache.values()):
+ if image.source == source:
+ image.background = background
+ self._reset_image(image)
+
+ 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, width, height)]
+ if image.source == source:
+ image.background = background
+ self._reset_image(image)
+
+ def _reset_image(self, image):
+ """
+ Mark the given :class:`Image` instance as dirty by setting its ``image`` and ``image_bytes`` attributes to None.
+ """
+ image.image = None
+ image.image_bytes = None
+ self._conversion_queue.modify_priority(image, Priority.Normal)
+
+ def process_updates(self):
+ """
+ Flush the queue to updated any data to update
+ """
+ # We want only one thread.
+ if not self.image_thread.isRunning():
+ self.image_thread.start()
+
+ 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, 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
+ self.process_updates()
+ while image.image is None:
+ log.debug('getImage - waiting')
+ time.sleep(0.1)
+ elif image.image_bytes is None:
+ # Set the priority to Low, because the image was requested but the byte stream was not generated yet.
+ # However, we only need to do this, when the image was generated before it was requested (otherwise this is
+ # already taken care of).
+ self._conversion_queue.modify_priority(image, Priority.Low)
+ return image.image
+
+ 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, 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
+ self.process_updates()
+ while image.image_bytes is None:
+ log.debug('getImageBytes - waiting')
+ time.sleep(0.1)
+ return image.image_bytes
+
+ 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, 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()):
+ if os.path.exists(path):
+ if image.path == path and image.timestamp != os.stat(path).st_mtime:
+ image.timestamp = os.stat(path).st_mtime
+ self._reset_image(image)
+ # We want only one thread.
+ if not self.image_thread.isRunning():
+ self.image_thread.start()
+
+ def _process(self):
+ """
+ Controls the processing called from a ``QtCore.QThread``.
+ """
+ log.debug('_process - started')
+ while not self._conversion_queue.empty() and not self.stop_manager:
+ self._process_cache()
+ log.debug('_process - ended')
+
+ def _process_cache(self):
+ """
+ Actually does the work.
+ """
+ log.debug('_processCache')
+ image = self._conversion_queue.get()[2]
+ # Generate the QImage for the image.
+ if image.image is None:
+ # 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)
+ return
+ # For image with high priority we set the priority to Low, as the byte stream might be needed earlier the
+ # byte stream of image with Normal priority. We stop here as we need to process more important images first.
+ elif image.priority == Priority.High:
+ self._conversion_queue.modify_priority(image, Priority.Low)
+ return
+ # Generate the byte stream for the image.
+ if image.image_bytes is None:
+ image.image_bytes = image_to_byte(image.image)
=== added directory 'openlp/core/lib/json'
=== added file 'openlp/core/lib/json/theme.json'
--- openlp/core/lib/json/theme.json 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/json/theme.json 2016-04-05 20:22:40 +0000
@@ -0,0 +1,59 @@
+{
+ "background" : {
+ "border_color": "#000000",
+ "color": "#000000",
+ "direction": "vertical",
+ "end_color": "#000000",
+ "filename": "",
+ "start_color": "#000000",
+ "type": "solid"
+ },
+ "display" :{
+ "horizontal_align": 0,
+ "slide_transition": false,
+ "vertical_align": 0
+ },
+ "font": {
+ "footer": {
+ "bold": false,
+ "color": "#FFFFFF",
+ "height": 78,
+ "italics": false,
+ "line_adjustment": 0,
+ "location": "",
+ "name": "Arial",
+ "outline": false,
+ "outline_color": "#000000",
+ "outline_size": 2,
+ "override": false,
+ "shadow": true,
+ "shadow_color": "#000000",
+ "shadow_size": 5,
+ "size": 12,
+ "width": 1004,
+ "x": 10,
+ "y": 690
+ },
+ "main": {
+ "bold": false,
+ "color": "#FFFFFF",
+ "height": 690,
+ "italics": false,
+ "line_adjustment": 0,
+ "location": "",
+ "name": "Arial",
+ "outline": false,
+ "outline_color": "#000000",
+ "outline_size": 2,
+ "override": false,
+ "shadow": true,
+ "shadow_color": "#000000",
+ "shadow_size": 5,
+ "size": 40,
+ "width": 1004,
+ "x": 10,
+ "y": 10
+ }
+ },
+ "theme_name": ""
+}
=== added file 'openlp/core/lib/listwidgetwithdnd.py'
--- openlp/core/lib/listwidgetwithdnd.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/listwidgetwithdnd.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Extend QListWidget to handle drag and drop functionality
+"""
+import os
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry
+
+
+class ListWidgetWithDnD(QtWidgets.QListWidget):
+ """
+ Provide a list widget to store objects and handle drag and drop events
+ """
+ def __init__(self, parent=None, name=''):
+ """
+ Initialise the list widget
+ """
+ super(ListWidgetWithDnD, self).__init__(parent)
+ self.mime_data_text = name
+
+ def activateDnD(self):
+ """
+ Activate DnD of widget
+ """
+ self.setAcceptDrops(True)
+ self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
+ Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
+
+ def mouseMoveEvent(self, event):
+ """
+ Drag and drop event does not care what data is selected as the recipient will use events to request the data
+ move just tell it what plugin to call
+ """
+ if event.buttons() != QtCore.Qt.LeftButton:
+ event.ignore()
+ return
+ if not self.selectedItems():
+ event.ignore()
+ return
+ drag = QtGui.QDrag(self)
+ mime_data = QtCore.QMimeData()
+ drag.setMimeData(mime_data)
+ mime_data.setText(self.mime_data_text)
+ drag.exec(QtCore.Qt.CopyAction)
+
+ def dragEnterEvent(self, event):
+ """
+ When something is dragged into this object, check if you should be able to drop it in here.
+ """
+ if event.mimeData().hasUrls():
+ event.accept()
+ else:
+ event.ignore()
+
+ def dragMoveEvent(self, event):
+ """
+ Make an object droppable, and set it to copy the contents of the object, not move it.
+ """
+ if event.mimeData().hasUrls():
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ else:
+ event.ignore()
+
+ def dropEvent(self, event):
+ """
+ Receive drop event check if it is a file and process it if it is.
+
+ :param event: Handle of the event pint passed
+ """
+ if event.mimeData().hasUrls():
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ files = []
+ for url in event.mimeData().urls():
+ local_file = os.path.normpath(url.toLocalFile())
+ if os.path.isfile(local_file):
+ files.append(local_file)
+ elif os.path.isdir(local_file):
+ listing = os.listdir(local_file)
+ for file in listing:
+ files.append(os.path.join(local_file, file))
+ Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())})
+ else:
+ event.ignore()
=== added file 'openlp/core/lib/mediamanageritem.py'
--- openlp/core/lib/mediamanageritem.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/mediamanageritem.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,682 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provides the generic functions for interfacing plugins with the Media Manager.
+"""
+import logging
+import os
+import re
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
+from openlp.core.lib import FileDialog, OpenLPToolbar, ServiceItem, StringContent, ListWidgetWithDnD, \
+ ServiceItemContext
+from openlp.core.lib.searchedit import SearchEdit
+from openlp.core.lib.ui import create_widget_action, critical_error_message_box
+
+log = logging.getLogger(__name__)
+
+
+class MediaManagerItem(QtWidgets.QWidget, RegistryProperties):
+ """
+ MediaManagerItem is a helper widget for plugins.
+
+ None of the following *need* to be used, feel free to override them completely in your plugin's implementation.
+ Alternatively, call them from your plugin before or after you've done extra things that you need to.
+
+ **Constructor Parameters**
+
+ ``parent``
+ The parent widget. Usually this will be the *Media Manager* itself. This needs to be a class descended from
+ ``QWidget``.
+
+ ``plugin``
+ The plugin widget. Usually this will be the *Plugin* itself. This needs to be a class descended from ``Plugin``.
+
+ **Member Variables**
+
+ When creating a descendant class from this class for your plugin, the following member variables should be set.
+
+ ``self.on_new_prompt``
+
+ Defaults to *'Select Image(s)'*.
+
+ ``self.on_new_file_masks``
+ Defaults to *'Images (*.jpg *jpeg *.gif *.png *.bmp)'*. This assumes that the new action is to load a file. If
+ not, you need to override the ``OnNew`` method.
+
+ ``self.PreviewFunction``
+ This must be a method which returns a QImage to represent the item (usually a preview). No scaling is required,
+ that is performed automatically by OpenLP when necessary. If this method is not defined, a default will be used
+ (treat the filename as an image).
+ """
+ log.info('Media Item loaded')
+
+ def __init__(self, parent=None, plugin=None):
+ """
+ Constructor to create the media manager item.
+ """
+ super(MediaManagerItem, self).__init__(parent)
+ self.plugin = plugin
+ self._setup()
+ self.setup_item()
+
+ def _setup(self):
+ """
+ Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
+ """
+ self.hide()
+ self.whitespace = re.compile(r'[\W_]+', re.UNICODE)
+ visible_title = self.plugin.get_string(StringContent.VisibleName)
+ self.title = str(visible_title['title'])
+ Registry().register(self.plugin.name, self)
+ self.settings_section = self.plugin.name
+ self.toolbar = None
+ self.remote_triggered = None
+ self.single_service_item = True
+ self.quick_preview_allowed = False
+ self.has_search = False
+ self.page_layout = QtWidgets.QVBoxLayout(self)
+ self.page_layout.setSpacing(0)
+ self.page_layout.setContentsMargins(0, 0, 0, 0)
+ self.required_icons()
+ self.setupUi()
+ self.retranslateUi()
+ self.auto_select_id = -1
+
+ def setup_item(self):
+ """
+ Override this for additional Plugin setup
+ """
+ pass
+
+ def required_icons(self):
+ """
+ This method is called to define the icons for the plugin. It provides a default set and the plugin is able to
+ override the if required.
+ """
+ self.has_import_icon = False
+ self.has_new_icon = True
+ self.has_edit_icon = True
+ self.has_file_icon = False
+ self.has_delete_icon = True
+ self.add_to_service_item = False
+
+ def retranslateUi(self):
+ """
+ This method is called automatically to provide OpenLP with the opportunity to translate the ``MediaManagerItem``
+ to another language.
+ """
+ pass
+
+ def add_toolbar(self):
+ """
+ A method to help developers easily add a toolbar to the media manager item.
+ """
+ if self.toolbar is None:
+ self.toolbar = OpenLPToolbar(self)
+ self.page_layout.addWidget(self.toolbar)
+
+ def setupUi(self):
+ """
+ This method sets up the interface on the button. Plugin developers use this to add and create toolbars, and the
+ rest of the interface of the media manager item.
+ """
+ # Add a toolbar
+ self.add_toolbar()
+ # Allow the plugin to define buttons at start of bar
+ self.add_start_header_bar()
+ # Add the middle of the tool bar (pre defined)
+ self.add_middle_header_bar()
+ # Allow the plugin to define buttons at end of bar
+ self.add_end_header_bar()
+ # Add the list view
+ self.add_list_view_to_toolbar()
+
+ def add_middle_header_bar(self):
+ """
+ Create buttons for the media item toolbar
+ """
+ toolbar_actions = []
+ # Import Button
+ if self.has_import_icon:
+ toolbar_actions.append(['Import', StringContent.Import,
+ ':/general/general_import.png', self.on_import_click])
+ # Load Button
+ if self.has_file_icon:
+ toolbar_actions.append(['Load', StringContent.Load, ':/general/general_open.png', self.on_file_click])
+ # New Button
+ if self.has_new_icon:
+ toolbar_actions.append(['New', StringContent.New, ':/general/general_new.png', self.on_new_click])
+ # Edit Button
+ if self.has_edit_icon:
+ toolbar_actions.append(['Edit', StringContent.Edit, ':/general/general_edit.png', self.on_edit_click])
+ # Delete Button
+ if self.has_delete_icon:
+ toolbar_actions.append(['Delete', StringContent.Delete,
+ ':/general/general_delete.png', self.on_delete_click])
+ # Preview
+ toolbar_actions.append(['Preview', StringContent.Preview,
+ ':/general/general_preview.png', self.on_preview_click])
+ # Live Button
+ toolbar_actions.append(['Live', StringContent.Live, ':/general/general_live.png', self.on_live_click])
+ # Add to service Button
+ toolbar_actions.append(['Service', StringContent.Service, ':/general/general_add.png', self.on_add_click])
+ for action in toolbar_actions:
+ if action[0] == StringContent.Preview:
+ self.toolbar.addSeparator()
+ self.toolbar.add_toolbar_action('%s%sAction' % (self.plugin.name, action[0]),
+ text=self.plugin.get_string(action[1])['title'], icon=action[2],
+ tooltip=self.plugin.get_string(action[1])['tooltip'],
+ triggers=action[3])
+
+ def add_list_view_to_toolbar(self):
+ """
+ Creates the main widget for listing items the media item is tracking
+ """
+ # Add the List widget
+ self.list_view = ListWidgetWithDnD(self, self.plugin.name)
+ self.list_view.setSpacing(1)
+ self.list_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
+ self.list_view.setAlternatingRowColors(True)
+ self.list_view.setObjectName('%sListView' % self.plugin.name)
+ # Add to page_layout
+ self.page_layout.addWidget(self.list_view)
+ # define and add the context menu
+ self.list_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+ if self.has_edit_icon:
+ create_widget_action(self.list_view,
+ text=self.plugin.get_string(StringContent.Edit)['title'],
+ icon=':/general/general_edit.png',
+ triggers=self.on_edit_click)
+ create_widget_action(self.list_view, separator=True)
+ create_widget_action(self.list_view,
+ 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Preview.title()),
+ text=self.plugin.get_string(StringContent.Preview)['title'],
+ icon=':/general/general_preview.png',
+ can_shortcuts=True,
+ triggers=self.on_preview_click)
+ create_widget_action(self.list_view,
+ 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Live.title()),
+ text=self.plugin.get_string(StringContent.Live)['title'],
+ icon=':/general/general_live.png',
+ can_shortcuts=True,
+ triggers=self.on_live_click)
+ create_widget_action(self.list_view,
+ 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Service.title()),
+ can_shortcuts=True,
+ text=self.plugin.get_string(StringContent.Service)['title'],
+ icon=':/general/general_add.png',
+ triggers=self.on_add_click)
+ if self.has_delete_icon:
+ create_widget_action(self.list_view, separator=True)
+ create_widget_action(self.list_view,
+ 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Delete.title()),
+ text=self.plugin.get_string(StringContent.Delete)['title'],
+ icon=':/general/general_delete.png',
+ can_shortcuts=True, triggers=self.on_delete_click)
+ if self.add_to_service_item:
+ create_widget_action(self.list_view, separator=True)
+ create_widget_action(self.list_view,
+ text=translate('OpenLP.MediaManagerItem', '&Add to selected Service Item'),
+ icon=':/general/general_add.png',
+ triggers=self.on_add_edit_click)
+ self.add_custom_context_actions()
+ # Create the context menu and add all actions from the list_view.
+ self.menu = QtWidgets.QMenu()
+ self.menu.addActions(self.list_view.actions())
+ self.list_view.doubleClicked.connect(self.on_double_clicked)
+ self.list_view.itemSelectionChanged.connect(self.on_selection_change)
+ self.list_view.customContextMenuRequested.connect(self.context_menu)
+
+ def add_search_to_toolbar(self):
+ """
+ Creates a search field with button and related signal handling.
+ """
+ self.search_widget = QtWidgets.QWidget(self)
+ self.search_widget.setObjectName('search_widget')
+ self.search_layout = QtWidgets.QVBoxLayout(self.search_widget)
+ self.search_layout.setObjectName('search_layout')
+ self.search_text_layout = QtWidgets.QFormLayout()
+ self.search_text_layout.setObjectName('search_text_layout')
+ self.search_text_label = QtWidgets.QLabel(self.search_widget)
+ self.search_text_label.setObjectName('search_text_label')
+ self.search_text_edit = SearchEdit(self.search_widget)
+ self.search_text_edit.setObjectName('search_text_edit')
+ self.search_text_label.setBuddy(self.search_text_edit)
+ self.search_text_layout.addRow(self.search_text_label, self.search_text_edit)
+ self.search_layout.addLayout(self.search_text_layout)
+ self.search_button_layout = QtWidgets.QHBoxLayout()
+ self.search_button_layout.setObjectName('search_button_layout')
+ self.search_button_layout.addStretch()
+ self.search_text_button = QtWidgets.QPushButton(self.search_widget)
+ self.search_text_button.setObjectName('search_text_button')
+ self.search_button_layout.addWidget(self.search_text_button)
+ self.search_layout.addLayout(self.search_button_layout)
+ self.page_layout.addWidget(self.search_widget)
+ # Signals and slots
+ self.search_text_edit.returnPressed.connect(self.on_search_text_button_clicked)
+ self.search_text_button.clicked.connect(self.on_search_text_button_clicked)
+ self.search_text_edit.textChanged.connect(self.on_search_text_edit_changed)
+
+ def add_custom_context_actions(self):
+ """
+ Implement this method in your descendant media manager item to add any context menu items.
+ This method is called automatically.
+ """
+ pass
+
+ def initialise(self):
+ """
+ Implement this method in your descendant media manager item to do any UI or other initialisation.
+ This method is called automatically.
+ """
+ pass
+
+ def add_start_header_bar(self):
+ """
+ Slot at start of toolbar for plugin to add widgets
+ """
+ pass
+
+ def add_end_header_bar(self):
+ """
+ Slot at end of toolbar for plugin to add widgets
+ """
+ pass
+
+ def on_file_click(self):
+ """
+ Add a file to the list widget to make it available for showing
+ """
+ files = FileDialog.getOpenFileNames(self, self.on_new_prompt,
+ Settings().value(self.settings_section + '/last directory'),
+ self.on_new_file_masks)
+ log.info('New files(s) %s' % files)
+ if files:
+ self.application.set_busy_cursor()
+ self.validate_and_load(files)
+ self.application.set_normal_cursor()
+
+ def load_file(self, data):
+ """
+ Turn file from Drag and Drop into an array so the Validate code can run it.
+
+ :param data: A dictionary containing the list of files to be loaded and the target
+ """
+ new_files = []
+ error_shown = False
+ for file_name in data['files']:
+ file_type = file_name.split('.')[-1]
+ if file_type.lower() not in self.on_new_file_masks:
+ if not error_shown:
+ critical_error_message_box(translate('OpenLP.MediaManagerItem', 'Invalid File Type'),
+ translate('OpenLP.MediaManagerItem',
+ 'Invalid File %s.\nSuffix not supported') % file_name)
+ error_shown = True
+ else:
+ new_files.append(file_name)
+ if new_files:
+ self.validate_and_load(new_files, data['target'])
+
+ def dnd_move_internal(self, target):
+ """
+ Handle internal moving of media manager items
+
+ :param target: The target of the DnD action
+ """
+ pass
+
+ def validate_and_load(self, files, target_group=None):
+ """
+ Process a list for files either from the File Dialog or from Drag and
+ Drop
+
+ :param files: The files to be loaded.
+ :param target_group: The QTreeWidgetItem of the group that will be the parent of the added files
+ """
+ names = []
+ full_list = []
+ for count in range(self.list_view.count()):
+ names.append(self.list_view.item(count).text())
+ full_list.append(self.list_view.item(count).data(QtCore.Qt.UserRole))
+ duplicates_found = False
+ files_added = False
+ for file_path in files:
+ if file_path in full_list:
+ duplicates_found = True
+ else:
+ files_added = True
+ full_list.append(file_path)
+ if full_list and files_added:
+ if target_group is None:
+ self.list_view.clear()
+ self.load_list(full_list, target_group)
+ last_dir = os.path.split(files[0])[0]
+ Settings().setValue(self.settings_section + '/last directory', last_dir)
+ Settings().setValue('%s/%s files' % (self.settings_section, self.settings_section), self.get_file_list())
+ if duplicates_found:
+ critical_error_message_box(UiStrings().Duplicate,
+ translate('OpenLP.MediaManagerItem',
+ 'Duplicate files were found on import and were ignored.'))
+
+ def context_menu(self, point):
+ """
+ Display a context menu
+
+ :param point: The point the cursor was at
+ """
+ item = self.list_view.itemAt(point)
+ # Decide if we have to show the context menu or not.
+ if item is None:
+ return
+ if not item.flags() & QtCore.Qt.ItemIsSelectable:
+ return
+ self.menu.exec(self.list_view.mapToGlobal(point))
+
+ def get_file_list(self):
+ """
+ Return the current list of files
+ """
+ file_list = []
+ for index in range(self.list_view.count()):
+ list_item = self.list_view.item(index)
+ filename = list_item.data(QtCore.Qt.UserRole)
+ file_list.append(filename)
+ return file_list
+
+ def load_list(self, load_list, target_group):
+ """
+ Load a list. Needs to be implemented by the plugin.
+
+ :param load_list: List object to load
+ :param target_group: Group to load
+ """
+ raise NotImplementedError('MediaManagerItem.loadList needs to be defined by the plugin')
+
+ def on_new_click(self):
+ """
+ Hook for plugins to define behaviour for adding new items.
+ """
+ pass
+
+ def on_edit_click(self):
+ """
+ Hook for plugins to define behaviour for editing items.
+ """
+ pass
+
+ def on_delete_click(self):
+ """
+ Delete an item. Needs to be implemented by the plugin.
+ """
+ raise NotImplementedError('MediaManagerItem.on_delete_click needs to be defined by the plugin')
+
+ def on_focus(self):
+ """
+ Run when a tab in the media manager gains focus. This gives the media
+ item a chance to focus any elements it wants to.
+ """
+ pass
+
+ def generate_slide_data(self, service_item, item=None, xml_version=False, remote=False,
+ context=ServiceItemContext.Live):
+ """
+ Generate the slide data. Needs to be implemented by the plugin.
+ :param service_item: The service Item to be processed
+ :param item: The database item to be used to build the service item
+ :param xml_version:
+ :param remote: Was this remote triggered (False)
+ :param context: The service context
+ """
+ raise NotImplementedError('MediaManagerItem.generate_slide_data needs to be defined by the plugin')
+
+ def on_double_clicked(self):
+ """
+ Allows the list click action to be determined dynamically
+ """
+ if Settings().value('advanced/double click live'):
+ self.on_live_click()
+ elif not Settings().value('advanced/single click preview'):
+ # NOTE: The above check is necessary to prevent bug #1419300
+ self.on_preview_click()
+
+ def on_selection_change(self):
+ """
+ Allows the change of current item in the list to be actioned
+ """
+ if Settings().value('advanced/single click preview') and self.quick_preview_allowed \
+ and self.list_view.selectedIndexes() and self.auto_select_id == -1:
+ self.on_preview_click(True)
+
+ def on_preview_click(self, keep_focus=False):
+ """
+ Preview an item by building a service item then adding that service item to the preview slide controller.
+
+ :param keep_focus: Do we keep focus (False)
+ """
+ if not self.list_view.selectedIndexes() and not self.remote_triggered:
+ QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+ translate('OpenLP.MediaManagerItem',
+ 'You must select one or more items to preview.'))
+ else:
+ log.debug('%s Preview requested' % self.plugin.name)
+ service_item = self.build_service_item()
+ if service_item:
+ service_item.from_plugin = True
+ self.preview_controller.add_service_item(service_item)
+ if not keep_focus:
+ self.preview_controller.preview_widget.setFocus()
+
+ def on_live_click(self):
+ """
+ Send an item live by building a service item then adding that service item to the live slide controller.
+ """
+ if not self.list_view.selectedIndexes():
+ QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+ translate('OpenLP.MediaManagerItem',
+ 'You must select one or more items to send live.'))
+ else:
+ self.go_live()
+
+ def go_live_remote(self, message):
+ """
+ Remote Call wrapper
+
+ :param message: The passed data item_id:Remote.
+ """
+ self.go_live(message[0], remote=message[1])
+
+ def go_live(self, item_id=None, remote=False):
+ """
+ Make the currently selected item go live.
+
+ :param item_id: item to make live
+ :param remote: From Remote
+ """
+ log.debug('%s Live requested', self.plugin.name)
+ item = None
+ if item_id:
+ item = self.create_item_from_id(item_id)
+ service_item = self.build_service_item(item, remote=remote)
+ if service_item:
+ if not item_id:
+ service_item.from_plugin = True
+ if remote:
+ service_item.will_auto_start = True
+ self.live_controller.add_service_item(service_item)
+ self.live_controller.preview_widget.setFocus()
+
+ def create_item_from_id(self, item_id):
+ """
+ Create a media item from an item id.
+
+ :param item_id: Id to make live
+ """
+ item = QtWidgets.QListWidgetItem()
+ item.setData(QtCore.Qt.UserRole, item_id)
+ return item
+
+ def on_add_click(self):
+ """
+ Add a selected item to the current service
+ """
+ if not self.list_view.selectedIndexes():
+ QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+ translate('OpenLP.MediaManagerItem',
+ 'You must select one or more items to add.'))
+ else:
+ # Is it possible to process multiple list items to generate
+ # multiple service items?
+ if self.single_service_item:
+ log.debug('%s Add requested', self.plugin.name)
+ self.add_to_service(replace=self.remote_triggered)
+ else:
+ items = self.list_view.selectedIndexes()
+ drop_position = self.service_manager.get_drop_position()
+ for item in items:
+ self.add_to_service(item, position=drop_position)
+ if drop_position != -1:
+ drop_position += 1
+
+ def add_to_service_remote(self, message):
+ """
+ Remote Call wrapper
+
+ :param message: The passed data item:Remote.
+ """
+ self.add_to_service(message[0], remote=message[1])
+
+ def add_to_service(self, item=None, replace=None, remote=False, position=-1):
+ """
+ Add this item to the current service.
+
+ :param item: Item to be processed
+ :param replace: Replace the existing item
+ :param remote: Triggered from remote
+ :param position: Position to place item
+ """
+ service_item = self.build_service_item(item, True, remote=remote, context=ServiceItemContext.Service)
+ if service_item:
+ service_item.from_plugin = False
+ self.service_manager.add_service_item(service_item, replace=replace, position=position)
+
+ def on_add_edit_click(self):
+ """
+ Add a selected item to an existing item in the current service.
+ """
+ if not self.list_view.selectedIndexes() and not self.remote_triggered:
+ QtWidgets.QMessageBox.information(self, UiStrings().NISp,
+ translate('OpenLP.MediaManagerItem',
+ 'You must select one or more items.'))
+ else:
+ log.debug('%s Add requested', self.plugin.name)
+ service_item = self.service_manager.get_service_item()
+ if not service_item:
+ QtWidgets.QMessageBox.information(self, UiStrings().NISs,
+ translate('OpenLP.MediaManagerItem',
+ 'You must select an existing service item to add to.'))
+ elif self.plugin.name == service_item.name:
+ self.generate_slide_data(service_item)
+ self.service_manager.add_service_item(service_item, replace=True)
+ else:
+ # Turn off the remote edit update message indicator
+ QtWidgets.QMessageBox.information(self, translate('OpenLP.MediaManagerItem', 'Invalid Service Item'),
+ translate('OpenLP.MediaManagerItem',
+ 'You must select a %s service item.') % self.title)
+
+ def build_service_item(self, item=None, xml_version=False, remote=False, context=ServiceItemContext.Live):
+ """
+ Common method for generating a service item
+ :param item: Service Item to be built.
+ :param xml_version: version of XML (False)
+ :param remote: Remote triggered (False)
+ :param context: The context on which this is called
+ """
+ service_item = ServiceItem(self.plugin)
+ service_item.add_icon(self.plugin.icon_path)
+ if self.generate_slide_data(service_item, item, xml_version, remote, context):
+ return service_item
+ else:
+ return None
+
+ def service_load(self, item):
+ """
+ Method to add processing when a service has been loaded and individual service items need to be processed by the
+ plugins.
+
+ :param item: The item to be processed and returned.
+ """
+ return item
+
+ def check_search_result(self):
+ """
+ Checks if the list_view is empty and adds a "No Search Results" item.
+ """
+ if self.list_view.count():
+ return
+ message = translate('OpenLP.MediaManagerItem', 'No Search Results')
+ item = QtWidgets.QListWidgetItem(message)
+ item.setFlags(QtCore.Qt.NoItemFlags)
+ font = QtGui.QFont()
+ font.setItalic(True)
+ item.setFont(font)
+ self.list_view.addItem(item)
+
+ def _get_id_of_item_to_generate(self, item, remote_item):
+ """
+ Utility method to check items being submitted for slide generation.
+
+ :param item: The item to check.
+ :param remote_item: The id to assign if the slide generation was remotely triggered.
+ """
+ if item is None:
+ if self.remote_triggered is None:
+ item = self.list_view.currentItem()
+ if item is None:
+ return False
+ item_id = item.data(QtCore.Qt.UserRole)
+ else:
+ item_id = remote_item
+ else:
+ item_id = item.data(QtCore.Qt.UserRole)
+ return item_id
+
+ def save_auto_select_id(self):
+ """
+ Sorts out, what item to select after loading a list.
+ """
+ # The item to select has not been set.
+ if self.auto_select_id == -1:
+ item = self.list_view.currentItem()
+ if item:
+ self.auto_select_id = item.data(QtCore.Qt.UserRole)
+
+ def search(self, string, show_error=True):
+ """
+ Performs a plugin specific search for items containing ``string``
+
+ :param string: String to be displayed
+ :param show_error: Should the error be shown (True)
+ """
+ raise NotImplementedError('Plugin.search needs to be defined by the plugin')
=== added file 'openlp/core/lib/plugin.py'
--- openlp/core/lib/plugin.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/plugin.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,385 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide the generic plugin functionality for OpenLP plugins.
+"""
+import logging
+
+from PyQt5 import QtCore
+
+from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings
+from openlp.core.common.versionchecker import get_application_version
+
+log = logging.getLogger(__name__)
+
+
+class PluginStatus(object):
+ """
+ Defines the status of the plugin
+ """
+ Active = 1
+ Inactive = 0
+ Disabled = -1
+
+
+class StringContent(object):
+ """
+ Provide standard strings for objects to use.
+ """
+ Name = 'name'
+ Import = 'import'
+ Load = 'load'
+ New = 'new'
+ Edit = 'edit'
+ Delete = 'delete'
+ Preview = 'preview'
+ Live = 'live'
+ Service = 'service'
+ VisibleName = 'visible_name'
+
+
+class Plugin(QtCore.QObject, RegistryProperties):
+ """
+ Base class for openlp plugins to inherit from.
+
+ **Basic Attributes**
+
+ ``name``
+ The name that should appear in the plugins list.
+
+ ``version``
+ The version number of this iteration of the plugin.
+
+ ``settings_section``
+ The namespace to store settings for the plugin.
+
+ ``icon``
+ An instance of QIcon, which holds an icon for this plugin.
+
+ ``log``
+ A log object used to log debugging messages. This is pre-instantiated.
+
+ ``weight``
+ A numerical value used to order the plugins.
+
+ **Hook Functions**
+
+ ``check_pre_conditions()``
+ Provides the Plugin with a handle to check if it can be loaded.
+
+ ``create_media_manager_item()``
+ Creates a new instance of MediaManagerItem to be used in the Media
+ Manager.
+
+ ``add_import_menu_item(import_menu)``
+ Add an item to the Import menu.
+
+ ``add_export_menu_item(export_menu)``
+ Add an item to the Export menu.
+
+ ``create_settings_tab()``
+ Creates a new instance of SettingsTabItem to be used in the Settings
+ dialog.
+
+ ``add_to_menu(menubar)``
+ A method to add a menu item to anywhere in the menu, given the menu bar.
+
+ ``handle_event(event)``
+ A method use to handle events, given an Event object.
+
+ ``about()``
+ Used in the plugin manager, when a person clicks on the 'About' button.
+
+ """
+ log.info('loaded')
+
+ def __init__(self, name, default_settings, media_item_class=None, settings_tab_class=None, version=None):
+ """
+ This is the constructor for the plugin object. This provides an easy way for descendant plugins to populate
+ common data. This method *must*
+
+ be overridden, like so::
+
+ class MyPlugin(Plugin):
+ def __init__(self):
+ super(MyPlugin, self).__init__('MyPlugin', version='0.1')
+
+ :param name: Defaults to *None*. The name of the plugin.
+ :param default_settings: A dict containing the plugin's settings. The value to each key is the default value
+ to be used.
+ :param media_item_class: The class name of the plugin's media item.
+ :param settings_tab_class: The class name of the plugin's settings tab.
+ :param version: Defaults to *None*, which means that the same version number is used as OpenLP's version number.
+ """
+ log.debug('Plugin %s initialised' % name)
+ super(Plugin, self).__init__()
+ self.name = name
+ self.text_strings = {}
+ self.set_plugin_text_strings()
+ self.name_strings = self.text_strings[StringContent.Name]
+ if version:
+ self.version = version
+ else:
+ self.version = get_application_version()['version']
+ self.settings_section = self.name
+ self.icon = None
+ self.media_item_class = media_item_class
+ self.settings_tab_class = settings_tab_class
+ self.settings_tab = None
+ self.media_item = None
+ self.weight = 0
+ self.status = PluginStatus.Inactive
+ # Add the default status to the default settings.
+ default_settings[name + '/status'] = PluginStatus.Inactive
+ default_settings[name + '/last directory'] = ''
+ # Append a setting for files in the mediamanager (note not all plugins
+ # which have a mediamanager need this).
+ if media_item_class is not None:
+ default_settings['%s/%s files' % (name, name)] = []
+ # Add settings to the dict of all settings.
+ Settings.extend_default_settings(default_settings)
+ Registry().register_function('%s_add_service_item' % self.name, self.process_add_service_event)
+ Registry().register_function('%s_config_updated' % self.name, self.config_update)
+
+ def check_pre_conditions(self):
+ """
+ Provides the Plugin with a handle to check if it can be loaded.
+ Failing Preconditions does not stop a settings Tab being created
+
+ Returns ``True`` or ``False``.
+ """
+ return True
+
+ def set_status(self):
+ """
+ Sets the status of the plugin
+ """
+ self.status = Settings().value(self.settings_section + '/status')
+
+ def toggle_status(self, new_status):
+ """
+ Changes the status of the plugin and remembers it
+ """
+ self.status = new_status
+ Settings().setValue(self.settings_section + '/status', self.status)
+ if new_status == PluginStatus.Active:
+ self.initialise()
+ elif new_status == PluginStatus.Inactive:
+ self.finalise()
+
+ def is_active(self):
+ """
+ Indicates if the plugin is active
+
+ Returns True or False.
+ """
+ return self.status == PluginStatus.Active
+
+ def create_media_manager_item(self):
+ """
+ Construct a MediaManagerItem object with all the buttons and things
+ you need, and return it for integration into OpenLP.
+ """
+ if self.media_item_class:
+ self.media_item = self.media_item_class(self.main_window.media_dock_manager.media_dock, self)
+
+ def upgrade_settings(self, settings):
+ """
+ Upgrade the settings of this plugin.
+
+ :param settings: The Settings object containing the old settings.
+ """
+ pass
+
+ def add_import_menu_item(self, import_menu):
+ """
+ Create a menu item and add it to the "Import" menu.
+
+ :param import_menu: The Import menu.
+ """
+ pass
+
+ def add_export_menu_item(self, export_menu):
+ """
+ Create a menu item and add it to the "Export" menu.
+
+ :param export_menu: The Export menu
+ """
+ pass
+
+ def add_tools_menu_item(self, tools_menu):
+ """
+ Create a menu item and add it to the "Tools" menu.
+
+ :param tools_menu: The Tools menu
+ """
+ pass
+
+ def create_settings_tab(self, parent):
+ """
+ Create a tab for the settings window to display the configurable options
+ for this plugin to the user.
+ """
+ if self.settings_tab_class:
+ self.settings_tab = self.settings_tab_class(parent, self.name,
+ self.get_string(StringContent.VisibleName)['title'],
+ self.icon_path)
+
+ def add_to_menu(self, menubar):
+ """
+ Add menu items to the menu, given the menubar.
+
+ :param menubar: The application's menu bar.
+ """
+ pass
+
+ def process_add_service_event(self, replace=False):
+ """
+ Generic Drag and drop handler triggered from service_manager.
+ """
+ log.debug('process_add_service_event event called for plugin %s' % self.name)
+ if replace:
+ self.media_item.on_add_edit_click()
+ else:
+ self.media_item.on_add_click()
+
+ @staticmethod
+ def about():
+ """
+ Show a dialog when the user clicks on the 'About' button in the plugin manager.
+ """
+ raise NotImplementedError('Plugin.about needs to be defined by the plugin')
+
+ def initialise(self):
+ """
+ Called by the plugin Manager to initialise anything it needs.
+ """
+ if self.media_item:
+ self.media_item.initialise()
+ self.main_window.media_dock_manager.add_item_to_dock(self.media_item)
+
+ def finalise(self):
+ """
+ Called by the plugin Manager to cleanup things.
+ """
+ if self.media_item:
+ self.main_window.media_dock_manager.remove_dock(self.media_item)
+
+ def app_startup(self):
+ """
+ Perform tasks on application startup
+ """
+ pass
+
+ def uses_theme(self, theme):
+ """
+ Called to find out if a plugin is currently using a theme.
+
+ Returns True if the theme is being used, otherwise returns False.
+ """
+ return False
+
+ def rename_theme(self, old_theme, new_theme):
+ """
+ Renames a theme a plugin is using making the plugin use the new name.
+
+ :param old_theme: The name of the theme the plugin should stop using.
+ :param new_theme: The new name the plugin should now use
+ """
+ pass
+
+ def get_string(self, name):
+ """
+ Encapsulate access of plugins translated text strings
+ """
+ return self.text_strings[name]
+
+ def set_plugin_ui_text_strings(self, tooltips):
+ """
+ Called to define all translatable texts of the plugin
+
+ :param tooltips:
+ """
+ # Load Action
+ self.__set_name_text_string(StringContent.Load, UiStrings().Load, tooltips['load'])
+ # Import Action
+ self.__set_name_text_string(StringContent.Import, UiStrings().Import, tooltips['import'])
+ # New Action
+ self.__set_name_text_string(StringContent.New, UiStrings().Add, tooltips['new'])
+ # Edit Action
+ self.__set_name_text_string(StringContent.Edit, UiStrings().Edit, tooltips['edit'])
+ # Delete Action
+ self.__set_name_text_string(StringContent.Delete, UiStrings().Delete, tooltips['delete'])
+ # Preview Action
+ self.__set_name_text_string(StringContent.Preview, UiStrings().Preview, tooltips['preview'])
+ # Send Live Action
+ self.__set_name_text_string(StringContent.Live, UiStrings().Live, tooltips['live'])
+ # Add to Service Action
+ self.__set_name_text_string(StringContent.Service, UiStrings().Service, tooltips['service'])
+
+ def __set_name_text_string(self, name, title, tooltip):
+ """
+ Utility method for creating a plugin's text_strings. This method makes use of the singular name of the
+ plugin object so must only be called after this has been set.
+ """
+ self.text_strings[name] = {'title': title, 'tooltip': tooltip}
+
+ def get_display_css(self):
+ """
+ Add css style sheets to htmlbuilder.
+ """
+ return ''
+
+ def get_display_javascript(self):
+ """
+ Add javascript functions to htmlbuilder.
+ """
+ return ''
+
+ def refresh_css(self, frame):
+ """
+ Allow plugins to refresh javascript on displayed screen.
+
+ ``frame``
+ The Web frame holding the page.
+ """
+ return ''
+
+ def get_display_html(self):
+ """
+ Add html code to htmlbuilder.
+ """
+ return ''
+
+ def config_update(self):
+ """
+ Called when Config is changed to restart values dependent on configuration.
+ """
+ log.info('config update processed')
+ if self.media_item:
+ self.media_item.config_update()
+
+ def new_service_created(self):
+ """
+ The plugin's needs to handle a new song creation
+ """
+ pass
=== added file 'openlp/core/lib/pluginmanager.py'
--- openlp/core/lib/pluginmanager.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/pluginmanager.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,207 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide plugin management
+"""
+import os
+import sys
+import imp
+
+from openlp.core.lib import Plugin, PluginStatus
+from openlp.core.common import AppLocation, RegistryProperties, OpenLPMixin, RegistryMixin
+
+
+class PluginManager(RegistryMixin, OpenLPMixin, RegistryProperties):
+ """
+ This is the Plugin manager, which loads all the plugins,
+ and executes all the hooks, as and when necessary.
+ """
+ def __init__(self, parent=None):
+ """
+ The constructor for the plugin manager. Passes the controllers on to
+ the plugins for them to interact with via their ServiceItems.
+ """
+ super(PluginManager, self).__init__(parent)
+ self.log_info('Plugin manager Initialising')
+ self.base_path = os.path.abspath(AppLocation.get_directory(AppLocation.PluginsDir))
+ self.log_debug('Base path %s ' % self.base_path)
+ self.plugins = []
+ self.log_info('Plugin manager Initialised')
+
+ def bootstrap_initialise(self):
+ """
+ Bootstrap all the plugin manager functions
+ """
+ self.find_plugins()
+ # hook methods have to happen after find_plugins. Find plugins needs
+ # the controllers hence the hooks have moved from setupUI() to here
+ # Find and insert settings tabs
+ self.hook_settings_tabs()
+ # Find and insert media manager items
+ self.hook_media_manager()
+ # Call the hook method to pull in import menus.
+ self.hook_import_menu()
+ # Call the hook method to pull in export menus.
+ self.hook_export_menu()
+ # Call the hook method to pull in tools menus.
+ self.hook_tools_menu()
+ # Call the initialise method to setup plugins.
+ self.initialise_plugins()
+
+ def find_plugins(self):
+ """
+ Scan a directory for objects inheriting from the ``Plugin`` class.
+ """
+ start_depth = len(os.path.abspath(self.base_path).split(os.sep))
+ present_plugin_dir = os.path.join(self.base_path, 'presentations')
+ self.log_debug('finding plugins in %s at depth %d' % (self.base_path, start_depth))
+ for root, dirs, files in os.walk(self.base_path):
+ for name in files:
+ if name.endswith('.py') and not name.startswith('__'):
+ path = os.path.abspath(os.path.join(root, name))
+ this_depth = len(path.split(os.sep))
+ if this_depth - start_depth > 2:
+ # skip anything lower down
+ break
+ module_name = name[:-3]
+ # import the modules
+ self.log_debug('Importing %s from %s. Depth %d' % (module_name, root, this_depth))
+ try:
+ # Use the "imp" library to try to get around a problem with the PyUNO library which
+ # monkey-patches the __import__ function to do some magic. This causes issues with our tests.
+ # First, try to find the module we want to import, searching the directory in root
+ fp, path_name, description = imp.find_module(module_name, [root])
+ # Then load the module (do the actual import) using the details from find_module()
+ imp.load_module(module_name, fp, path_name, description)
+ except ImportError as e:
+ self.log_exception('Failed to import module %s on path %s: %s'
+ % (module_name, path, e.args[0]))
+ plugin_classes = Plugin.__subclasses__()
+ plugin_objects = []
+ for p in plugin_classes:
+ try:
+ plugin = p()
+ self.log_debug('Loaded plugin %s' % str(p))
+ plugin_objects.append(plugin)
+ except TypeError:
+ self.log_exception('Failed to load plugin %s' % str(p))
+ plugins_list = sorted(plugin_objects, key=lambda plugin: plugin.weight)
+ for plugin in plugins_list:
+ if plugin.check_pre_conditions():
+ self.log_debug('Plugin %s active' % str(plugin.name))
+ plugin.set_status()
+ else:
+ plugin.status = PluginStatus.Disabled
+ self.plugins.append(plugin)
+
+ def hook_media_manager(self):
+ """
+ Create the plugins' media manager items.
+ """
+ for plugin in self.plugins:
+ if plugin.status is not PluginStatus.Disabled:
+ plugin.create_media_manager_item()
+
+ def hook_settings_tabs(self):
+ """
+ Loop through all the plugins. If a plugin has a valid settings tab
+ item, add it to the settings tab.
+ Tabs are set for all plugins not just Active ones
+
+ """
+ for plugin in self.plugins:
+ if plugin.status is not PluginStatus.Disabled:
+ plugin.create_settings_tab(self.settings_form)
+
+ def hook_import_menu(self):
+ """
+ Loop through all the plugins and give them an opportunity to add an
+ item to the import menu.
+
+ """
+ for plugin in self.plugins:
+ if plugin.status is not PluginStatus.Disabled:
+ plugin.add_import_menu_item(self.main_window.file_import_menu)
+
+ def hook_export_menu(self):
+ """
+ Loop through all the plugins and give them an opportunity to add an
+ item to the export menu.
+ """
+ for plugin in self.plugins:
+ if plugin.status is not PluginStatus.Disabled:
+ plugin.add_export_menu_item(self.main_window.file_export_menu)
+
+ def hook_tools_menu(self):
+ """
+ Loop through all the plugins and give them an opportunity to add an
+ item to the tools menu.
+ """
+ for plugin in self.plugins:
+ if plugin.status is not PluginStatus.Disabled:
+ plugin.add_tools_menu_item(self.main_window.tools_menu)
+
+ def hook_upgrade_plugin_settings(self, settings):
+ """
+ Loop through all the plugins and give them an opportunity to upgrade their settings.
+
+ :param settings: The Settings object containing the old settings.
+ """
+ for plugin in self.plugins:
+ if plugin.status is not PluginStatus.Disabled:
+ plugin.upgrade_settings(settings)
+
+ def initialise_plugins(self):
+ """
+ Loop through all the plugins and give them an opportunity to initialise themselves.
+ """
+ for plugin in self.plugins:
+ self.log_info('initialising plugins %s in a %s state' % (plugin.name, plugin.is_active()))
+ if plugin.is_active():
+ plugin.initialise()
+ self.log_info('Initialisation Complete for %s ' % plugin.name)
+
+ def finalise_plugins(self):
+ """
+ Loop through all the plugins and give them an opportunity to clean themselves up
+ """
+ for plugin in self.plugins:
+ if plugin.is_active():
+ plugin.finalise()
+ self.log_info('Finalisation Complete for %s ' % plugin.name)
+
+ def get_plugin_by_name(self, name):
+ """
+ Return the plugin which has a name with value ``name``.
+ """
+ for plugin in self.plugins:
+ if plugin.name == name:
+ return plugin
+ return None
+
+ def new_service_created(self):
+ """
+ Loop through all the plugins and give them an opportunity to handle a new service
+ """
+ for plugin in self.plugins:
+ if plugin.is_active():
+ plugin.new_service_created()
=== added directory 'openlp/core/lib/projector'
=== added file 'openlp/core/lib/projector/__init__.py'
--- openlp/core/lib/projector/__init__.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/__init__.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+ :mod:`openlp.core.ui.projector`
+
+ Initialization for the openlp.core.ui.projector modules.
+"""
+
+
+class DialogSourceStyle(object):
+ """
+ An enumeration for projector dialog box type.
+ """
+ Tabbed = 0
+ Single = 1
=== added file 'openlp/core/lib/projector/constants.py'
--- openlp/core/lib/projector/constants.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/constants.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,353 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+ :mod:`openlp.core.lib.projector.constants` module
+
+ Provides the constants used for projector errors/status/defaults
+"""
+
+import logging
+log = logging.getLogger(__name__)
+log.debug('projector_constants loaded')
+
+from openlp.core.common import translate
+
+
+__all__ = ['S_OK', 'E_GENERAL', 'E_NOT_CONNECTED', 'E_FAN', 'E_LAMP', 'E_TEMP',
+ 'E_COVER', 'E_FILTER', 'E_AUTHENTICATION', 'E_NO_AUTHENTICATION',
+ 'E_UNDEFINED', 'E_PARAMETER', 'E_UNAVAILABLE', 'E_PROJECTOR',
+ 'E_INVALID_DATA', 'E_WARN', 'E_ERROR', 'E_CLASS', 'E_PREFIX',
+ 'E_CONNECTION_REFUSED', 'E_REMOTE_HOST_CLOSED_CONNECTION', 'E_HOST_NOT_FOUND',
+ 'E_SOCKET_ACCESS', 'E_SOCKET_RESOURCE', 'E_SOCKET_TIMEOUT', 'E_DATAGRAM_TOO_LARGE',
+ 'E_NETWORK', 'E_ADDRESS_IN_USE', 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
+ 'E_UNSUPPORTED_SOCKET_OPERATION', 'E_PROXY_AUTHENTICATION_REQUIRED',
+ 'E_SLS_HANDSHAKE_FAILED', 'E_UNFINISHED_SOCKET_OPERATION', 'E_PROXY_CONNECTION_REFUSED',
+ 'E_PROXY_CONNECTION_CLOSED', 'E_PROXY_CONNECTION_TIMEOUT', 'E_PROXY_NOT_FOUND',
+ 'E_PROXY_PROTOCOL', 'E_UNKNOWN_SOCKET_ERROR',
+ 'S_NOT_CONNECTED', 'S_CONNECTING', 'S_CONNECTED',
+ 'S_STATUS', 'S_OFF', 'S_INITIALIZE', 'S_STANDBY', 'S_WARMUP', 'S_ON', 'S_COOLDOWN',
+ 'S_INFO', 'S_NETWORK_SENDING', 'S_NETWORK_RECEIVED',
+ 'ERROR_STRING', 'CR', 'LF', 'PJLINK_ERST_STATUS', 'PJLINK_POWR_STATUS',
+ 'PJLINK_PORT', 'PJLINK_MAX_PACKET', 'TIMEOUT', 'ERROR_MSG', 'PJLINK_ERRORS',
+ 'STATUS_STRING', 'PJLINK_VALID_CMD', 'CONNECTION_ERRORS']
+
+# Set common constants.
+CR = chr(0x0D) # \r
+LF = chr(0x0A) # \n
+PJLINK_PORT = 4352
+TIMEOUT = 30.0
+PJLINK_MAX_PACKET = 136
+PJLINK_VALID_CMD = {'1': ['PJLINK', # Initial connection
+ 'POWR', # Power option
+ 'INPT', # Video sources option
+ 'AVMT', # Shutter option
+ 'ERST', # Error status option
+ 'LAMP', # Lamp(s) query (Includes fans)
+ 'INST', # Input sources available query
+ 'NAME', # Projector name query
+ 'INF1', # Manufacturer name query
+ 'INF2', # Product name query
+ 'INFO', # Other information query
+ 'CLSS' # PJLink class support query
+ ]}
+
+# Error and status codes
+S_OK = E_OK = 0 # E_OK included since I sometimes forget
+# Error codes. Start at 200 so we don't duplicate system error codes.
+E_GENERAL = 200 # Unknown error
+E_NOT_CONNECTED = 201
+E_FAN = 202
+E_LAMP = 203
+E_TEMP = 204
+E_COVER = 205
+E_FILTER = 206
+E_NO_AUTHENTICATION = 207 # PIN set and no authentication set on projector
+E_UNDEFINED = 208 # ERR1
+E_PARAMETER = 209 # ERR2
+E_UNAVAILABLE = 210 # ERR3
+E_PROJECTOR = 211 # ERR4
+E_INVALID_DATA = 212
+E_WARN = 213
+E_ERROR = 214
+E_AUTHENTICATION = 215 # ERRA
+E_CLASS = 216
+E_PREFIX = 217
+
+# Remap Qt socket error codes to projector error codes
+E_CONNECTION_REFUSED = 230
+E_REMOTE_HOST_CLOSED_CONNECTION = 231
+E_HOST_NOT_FOUND = 232
+E_SOCKET_ACCESS = 233
+E_SOCKET_RESOURCE = 234
+E_SOCKET_TIMEOUT = 235
+E_DATAGRAM_TOO_LARGE = 236
+E_NETWORK = 237
+E_ADDRESS_IN_USE = 238
+E_SOCKET_ADDRESS_NOT_AVAILABLE = 239
+E_UNSUPPORTED_SOCKET_OPERATION = 240
+E_PROXY_AUTHENTICATION_REQUIRED = 241
+E_SLS_HANDSHAKE_FAILED = 242
+E_UNFINISHED_SOCKET_OPERATION = 243
+E_PROXY_CONNECTION_REFUSED = 244
+E_PROXY_CONNECTION_CLOSED = 245
+E_PROXY_CONNECTION_TIMEOUT = 246
+E_PROXY_NOT_FOUND = 247
+E_PROXY_PROTOCOL = 248
+E_UNKNOWN_SOCKET_ERROR = -1
+
+# Status codes start at 300
+S_NOT_CONNECTED = 300
+S_CONNECTING = 301
+S_CONNECTED = 302
+S_INITIALIZE = 303
+S_STATUS = 304
+S_OFF = 305
+S_STANDBY = 306
+S_WARMUP = 307
+S_ON = 308
+S_COOLDOWN = 309
+S_INFO = 310
+
+# Information that does not affect status
+S_NETWORK_SENDING = 400
+S_NETWORK_RECEIVED = 401
+
+CONNECTION_ERRORS = {E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS,
+ E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION,
+ E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT,
+ E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE,
+ E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED,
+ E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED,
+ E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND,
+ E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR
+ }
+
+PJLINK_ERRORS = {'ERRA': E_AUTHENTICATION, # Authentication error
+ 'ERR1': E_UNDEFINED, # Undefined command error
+ 'ERR2': E_PARAMETER, # Invalid parameter error
+ 'ERR3': E_UNAVAILABLE, # Projector busy
+ 'ERR4': E_PROJECTOR, # Projector or display failure
+ E_AUTHENTICATION: 'ERRA',
+ E_UNDEFINED: 'ERR1',
+ E_PARAMETER: 'ERR2',
+ E_UNAVAILABLE: 'ERR3',
+ E_PROJECTOR: 'ERR4'}
+
+# Map error/status codes to string
+ERROR_STRING = {0: 'S_OK',
+ E_GENERAL: 'E_GENERAL',
+ E_NOT_CONNECTED: 'E_NOT_CONNECTED',
+ E_FAN: 'E_FAN',
+ E_LAMP: 'E_LAMP',
+ E_TEMP: 'E_TEMP',
+ E_COVER: 'E_COVER',
+ E_FILTER: 'E_FILTER',
+ E_AUTHENTICATION: 'E_AUTHENTICATION',
+ E_NO_AUTHENTICATION: 'E_NO_AUTHENTICATION',
+ E_UNDEFINED: 'E_UNDEFINED',
+ E_PARAMETER: 'E_PARAMETER',
+ E_UNAVAILABLE: 'E_UNAVAILABLE',
+ E_PROJECTOR: 'E_PROJECTOR',
+ E_INVALID_DATA: 'E_INVALID_DATA',
+ E_WARN: 'E_WARN',
+ E_ERROR: 'E_ERROR',
+ E_CLASS: 'E_CLASS',
+ E_PREFIX: 'E_PREFIX', # Last projector error
+ E_CONNECTION_REFUSED: 'E_CONNECTION_REFUSED', # First QtSocket error
+ E_REMOTE_HOST_CLOSED_CONNECTION: 'E_REMOTE_HOST_CLOSED_CONNECTION',
+ E_HOST_NOT_FOUND: 'E_HOST_NOT_FOUND',
+ E_SOCKET_ACCESS: 'E_SOCKET_ACCESS',
+ E_SOCKET_RESOURCE: 'E_SOCKET_RESOURCE',
+ E_SOCKET_TIMEOUT: 'E_SOCKET_TIMEOUT',
+ E_DATAGRAM_TOO_LARGE: 'E_DATAGRAM_TOO_LARGE',
+ E_NETWORK: 'E_NETWORK',
+ E_ADDRESS_IN_USE: 'E_ADDRESS_IN_USE',
+ E_SOCKET_ADDRESS_NOT_AVAILABLE: 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
+ E_UNSUPPORTED_SOCKET_OPERATION: 'E_UNSUPPORTED_SOCKET_OPERATION',
+ E_PROXY_AUTHENTICATION_REQUIRED: 'E_PROXY_AUTHENTICATION_REQUIRED',
+ E_SLS_HANDSHAKE_FAILED: 'E_SLS_HANDSHAKE_FAILED',
+ E_UNFINISHED_SOCKET_OPERATION: 'E_UNFINISHED_SOCKET_OPERATION',
+ E_PROXY_CONNECTION_REFUSED: 'E_PROXY_CONNECTION_REFUSED',
+ E_PROXY_CONNECTION_CLOSED: 'E_PROXY_CONNECTION_CLOSED',
+ E_PROXY_CONNECTION_TIMEOUT: 'E_PROXY_CONNECTION_TIMEOUT',
+ E_PROXY_NOT_FOUND: 'E_PROXY_NOT_FOUND',
+ E_PROXY_PROTOCOL: 'E_PROXY_PROTOCOL',
+ E_UNKNOWN_SOCKET_ERROR: 'E_UNKNOWN_SOCKET_ERROR'}
+
+STATUS_STRING = {S_NOT_CONNECTED: 'S_NOT_CONNECTED',
+ S_CONNECTING: 'S_CONNECTING',
+ S_CONNECTED: 'S_CONNECTED',
+ S_STATUS: 'S_STATUS',
+ S_OFF: 'S_OFF',
+ S_INITIALIZE: 'S_INITIALIZE',
+ S_STANDBY: 'S_STANDBY',
+ S_WARMUP: 'S_WARMUP',
+ S_ON: 'S_ON',
+ S_COOLDOWN: 'S_COOLDOWN',
+ S_INFO: 'S_INFO',
+ S_NETWORK_SENDING: 'S_NETWORK_SENDING',
+ S_NETWORK_RECEIVED: 'S_NETWORK_RECEIVED'}
+
+# Map error/status codes to message strings
+ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK
+ E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'),
+ E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'),
+ E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'),
+ E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'),
+ E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'),
+ E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'),
+ E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'),
+ E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'),
+ E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'),
+ E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'),
+ E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'),
+ E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'),
+ E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invalid packet received'),
+ E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'),
+ E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'),
+ E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'),
+ E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'),
+ E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
+ 'The connection was refused by the peer (or timed out)'),
+ E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
+ 'The remote host closed the connection'),
+ E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'),
+ E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants',
+ 'The socket operation failed because the application '
+ 'lacked the required privileges'),
+ E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants',
+ 'The local system ran out of resources (e.g., too many sockets)'),
+ E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants',
+ 'The socket operation timed out'),
+ E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants',
+ 'The datagram was larger than the operating system\'s limit'),
+ E_NETWORK: translate('OpenLP.ProjectorConstants',
+ 'An error occurred with the network (Possibly someone pulled the plug?)'),
+ E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants',
+ 'The address specified with socket.bind() '
+ 'is already in use and was set to be exclusive'),
+ E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
+ 'The address specified to socket.bind() '
+ 'does not belong to the host'),
+ E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
+ 'The requested socket operation is not supported by the local '
+ 'operating system (e.g., lack of IPv6 support)'),
+ E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
+ 'The socket is using a proxy, '
+ 'and the proxy requires authentication'),
+ E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants',
+ 'The SSL/TLS handshake failed'),
+ E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
+ 'The last operation attempted has not finished yet '
+ '(still in progress in the background)'),
+ E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
+ 'Could not contact the proxy server because the connection '
+ 'to that server was denied'),
+ E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants',
+ 'The connection to the proxy server was closed unexpectedly '
+ '(before the connection to the final peer was established)'),
+ E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants',
+ 'The connection to the proxy server timed out or the proxy '
+ 'server stopped responding in the authentication phase.'),
+ E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants',
+ 'The proxy address set with setProxy() was not found'),
+ E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants',
+ 'The connection negotiation with the proxy server failed because the '
+ 'response from the proxy server could not be understood'),
+ E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'),
+ S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'),
+ S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'),
+ S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'),
+ S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'),
+ S_OFF: translate('OpenLP.ProjectorConstants', 'Off'),
+ S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'),
+ S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'),
+ S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'),
+ S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'),
+ S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'),
+ S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'),
+ S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'),
+ S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')}
+
+# Map for ERST return codes to string
+PJLINK_ERST_STATUS = {'0': ERROR_STRING[E_OK],
+ '1': ERROR_STRING[E_WARN],
+ '2': ERROR_STRING[E_ERROR]}
+
+# Map for POWR return codes to status code
+PJLINK_POWR_STATUS = {'0': S_STANDBY,
+ '1': S_ON,
+ '2': S_COOLDOWN,
+ '3': S_WARMUP}
+
+PJLINK_DEFAULT_SOURCES = {'1': translate('OpenLP.DB', 'RGB'),
+ '2': translate('OpenLP.DB', 'Video'),
+ '3': translate('OpenLP.DB', 'Digital'),
+ '4': translate('OpenLP.DB', 'Storage'),
+ '5': translate('OpenLP.DB', 'Network')}
+
+PJLINK_DEFAULT_CODES = {'11': translate('OpenLP.DB', 'RGB 1'),
+ '12': translate('OpenLP.DB', 'RGB 2'),
+ '13': translate('OpenLP.DB', 'RGB 3'),
+ '14': translate('OpenLP.DB', 'RGB 4'),
+ '15': translate('OpenLP.DB', 'RGB 5'),
+ '16': translate('OpenLP.DB', 'RGB 6'),
+ '17': translate('OpenLP.DB', 'RGB 7'),
+ '18': translate('OpenLP.DB', 'RGB 8'),
+ '19': translate('OpenLP.DB', 'RGB 9'),
+ '21': translate('OpenLP.DB', 'Video 1'),
+ '22': translate('OpenLP.DB', 'Video 2'),
+ '23': translate('OpenLP.DB', 'Video 3'),
+ '24': translate('OpenLP.DB', 'Video 4'),
+ '25': translate('OpenLP.DB', 'Video 5'),
+ '26': translate('OpenLP.DB', 'Video 6'),
+ '27': translate('OpenLP.DB', 'Video 7'),
+ '28': translate('OpenLP.DB', 'Video 8'),
+ '29': translate('OpenLP.DB', 'Video 9'),
+ '31': translate('OpenLP.DB', 'Digital 1'),
+ '32': translate('OpenLP.DB', 'Digital 2'),
+ '33': translate('OpenLP.DB', 'Digital 3'),
+ '34': translate('OpenLP.DB', 'Digital 4'),
+ '35': translate('OpenLP.DB', 'Digital 5'),
+ '36': translate('OpenLP.DB', 'Digital 6'),
+ '37': translate('OpenLP.DB', 'Digital 7'),
+ '38': translate('OpenLP.DB', 'Digital 8'),
+ '39': translate('OpenLP.DB', 'Digital 9'),
+ '41': translate('OpenLP.DB', 'Storage 1'),
+ '42': translate('OpenLP.DB', 'Storage 2'),
+ '43': translate('OpenLP.DB', 'Storage 3'),
+ '44': translate('OpenLP.DB', 'Storage 4'),
+ '45': translate('OpenLP.DB', 'Storage 5'),
+ '46': translate('OpenLP.DB', 'Storage 6'),
+ '47': translate('OpenLP.DB', 'Storage 7'),
+ '48': translate('OpenLP.DB', 'Storage 8'),
+ '49': translate('OpenLP.DB', 'Storage 9'),
+ '51': translate('OpenLP.DB', 'Network 1'),
+ '52': translate('OpenLP.DB', 'Network 2'),
+ '53': translate('OpenLP.DB', 'Network 3'),
+ '54': translate('OpenLP.DB', 'Network 4'),
+ '55': translate('OpenLP.DB', 'Network 5'),
+ '56': translate('OpenLP.DB', 'Network 6'),
+ '57': translate('OpenLP.DB', 'Network 7'),
+ '58': translate('OpenLP.DB', 'Network 8'),
+ '59': translate('OpenLP.DB', 'Network 9')
+ }
=== added file 'openlp/core/lib/projector/db.py'
--- openlp/core/lib/projector/db.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/db.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,429 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+:mod:`openlp.core.lib.projector.db` module
+
+Provides the database functions for the Projector module.
+
+The Manufacturer, Model, Source tables keep track of the video source
+strings used for display of input sources. The Source table maps
+manufacturer-defined or user-defined strings from PJLink default strings
+to end-user readable strings; ex: PJLink code 11 would map "RGB 1"
+default string to "RGB PC (analog)" string.
+(Future feature).
+
+The Projector table keeps track of entries for controlled projectors.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+log.debug('projector.lib.db module loaded')
+
+from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_
+from sqlalchemy.ext.declarative import declarative_base, declared_attr
+from sqlalchemy.orm import backref, relationship
+
+from openlp.core.lib.db import Manager, init_db, init_url
+from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
+
+metadata = MetaData()
+Base = declarative_base(metadata)
+
+
+class CommonBase(object):
+ """
+ Base class to automate table name and ID column.
+ """
+ @declared_attr
+ def __tablename__(cls):
+ return cls.__name__.lower()
+
+ id = Column(Integer, primary_key=True)
+
+
+class Manufacturer(CommonBase, Base):
+ """
+ Projector manufacturer table.
+
+ Manufacturer:
+ name: Column(String(30))
+ models: Relationship(Model.id)
+
+ Model table is related.
+ """
+ def __repr__(self):
+ """
+ Returns a basic representation of a Manufacturer table entry.
+ """
+ return '<Manufacturer(name="%s")>' % self.name
+
+ name = Column(String(30))
+ models = relationship('Model',
+ order_by='Model.name',
+ backref='manufacturer',
+ cascade='all, delete-orphan',
+ primaryjoin='Manufacturer.id==Model.manufacturer_id',
+ lazy='joined')
+
+
+class Model(CommonBase, Base):
+ """
+ Projector model table.
+
+ Model:
+ name: Column(String(20))
+ sources: Relationship(Source.id)
+ manufacturer_id: Foreign_key(Manufacturer.id)
+
+ Manufacturer table links here.
+ Source table is related.
+ """
+ def __repr__(self):
+ """
+ Returns a basic representation of a Model table entry.
+ """
+ return '<Model(name=%s)>' % self.name
+
+ manufacturer_id = Column(Integer, ForeignKey('manufacturer.id'))
+ name = Column(String(20))
+ sources = relationship('Source',
+ order_by='Source.pjlink_name',
+ backref='model',
+ cascade='all, delete-orphan',
+ primaryjoin='Model.id==Source.model_id',
+ lazy='joined')
+
+
+class Source(CommonBase, Base):
+ """
+ Projector video source table.
+
+ Source:
+ pjlink_name: Column(String(15))
+ pjlink_code: Column(String(2))
+ text: Column(String(30))
+ model_id: Foreign_key(Model.id)
+
+ Model table links here.
+
+ These entries map PJLink input video source codes to text strings.
+ """
+ def __repr__(self):
+ """
+ Return basic representation of Source table entry.
+ """
+ return '<Source(pjlink_name="%s", pjlink_code="%s", text="%s")>' % \
+ (self.pjlink_name, self.pjlink_code, self.text)
+ model_id = Column(Integer, ForeignKey('model.id'))
+ pjlink_name = Column(String(15))
+ pjlink_code = Column(String(2))
+ text = Column(String(30))
+
+
+class Projector(CommonBase, Base):
+ """
+ Projector table.
+
+ Projector:
+ ip: Column(String(100)) # Allow for IPv6 or FQDN
+ port: Column(String(8))
+ pin: Column(String(20)) # Allow for test strings
+ name: Column(String(20))
+ location: Column(String(30))
+ notes: Column(String(200))
+ pjlink_name: Column(String(128)) # From projector (future)
+ manufacturer: Column(String(128)) # From projector (future)
+ model: Column(String(128)) # From projector (future)
+ other: Column(String(128)) # From projector (future)
+ sources: Column(String(128)) # From projector (future)
+
+ ProjectorSource relates
+ """
+ def __repr__(self):
+ """
+ Return basic representation of Source table entry.
+ """
+ return '< Projector(id="%s", ip="%s", port="%s", pin="%s", name="%s", location="%s",' \
+ 'notes="%s", pjlink_name="%s", manufacturer="%s", model="%s", other="%s",' \
+ 'sources="%s", source_list="%s") >' % (self.id, self.ip, self.port, self.pin, self.name, self.location,
+ self.notes, self.pjlink_name, self.manufacturer, self.model,
+ self.other, self.sources, self.source_list)
+ ip = Column(String(100))
+ port = Column(String(8))
+ pin = Column(String(20))
+ name = Column(String(20))
+ location = Column(String(30))
+ notes = Column(String(200))
+ pjlink_name = Column(String(128))
+ manufacturer = Column(String(128))
+ model = Column(String(128))
+ other = Column(String(128))
+ sources = Column(String(128))
+ source_list = relationship('ProjectorSource',
+ order_by='ProjectorSource.code',
+ backref='projector',
+ cascade='all, delete-orphan',
+ primaryjoin='Projector.id==ProjectorSource.projector_id',
+ lazy='joined')
+
+
+class ProjectorSource(CommonBase, Base):
+ """
+ Projector local source table
+ This table allows mapping specific projector source input to a local
+ connection; i.e., '11': 'DVD Player'
+
+ Projector Source:
+ projector_id: Foreign_key(Column(Projector.id))
+ code: Column(String(3)) # PJLink source code
+ text: Column(String(20)) # Text to display
+
+ Projector table links here
+ """
+ def __repr__(self):
+ """
+ Return basic representation of Source table entry.
+ """
+ return '<ProjectorSource(id="%s", code="%s", text="%s", projector_id="%s")>' % (self.id,
+ self.code,
+ self.text,
+ self.projector_id)
+ code = Column(String(3))
+ text = Column(String(20))
+ projector_id = Column(Integer, ForeignKey('projector.id'))
+
+
+class ProjectorDB(Manager):
+ """
+ Class to access the projector database.
+ """
+ def __init__(self, *args, **kwargs):
+ log.debug('ProjectorDB().__init__(args="%s", kwargs="%s")' % (args, kwargs))
+ super().__init__(plugin_name='projector', init_schema=self.init_schema)
+ log.debug('ProjectorDB() Initialized using db url %s' % self.db_url)
+ log.debug('Session: %s', self.session)
+
+ def init_schema(self, *args, **kwargs):
+ """
+ Setup the projector database and initialize the schema.
+
+ Declarative uses table classes to define schema.
+ """
+ self.db_url = init_url('projector')
+ session, metadata = init_db(self.db_url, base=Base)
+ metadata.create_all(checkfirst=True)
+ return session
+
+ def get_projector_by_id(self, dbid):
+ """
+ Locate a DB record by record ID.
+
+ :param dbid: DB record id
+ :returns: Projector() instance
+ """
+ log.debug('get_projector_by_id(id="%s")' % dbid)
+ projector = self.get_object_filtered(Projector, Projector.id == dbid)
+ if projector is None:
+ # Not found
+ log.warn('get_projector_by_id() did not find %s' % id)
+ return None
+ log.debug('get_projectorby_id() returning 1 entry for "%s" id="%s"' % (dbid, projector.id))
+ return projector
+
+ def get_projector_all(self):
+ """
+ Retrieve all projector entries.
+
+ :returns: List with Projector() instances used in Manager() QListWidget.
+ """
+ log.debug('get_all() called')
+ return_list = []
+ new_list = self.get_all_objects(Projector)
+ if new_list is None or new_list.count == 0:
+ return return_list
+ for new_projector in new_list:
+ return_list.append(new_projector)
+ log.debug('get_all() returning %s item(s)' % len(return_list))
+ return return_list
+
+ def get_projector_by_ip(self, ip):
+ """
+ Locate a projector by host IP/Name.
+
+ :param ip: Host IP/Name
+ :returns: Projector() instance
+ """
+ log.debug('get_projector_by_ip(ip="%s")' % ip)
+ projector = self.get_object_filtered(Projector, Projector.ip == ip)
+ if projector is None:
+ # Not found
+ log.warn('get_projector_by_ip() did not find %s' % ip)
+ return None
+ log.debug('get_projectorby_ip() returning 1 entry for "%s" id="%s"' % (ip, projector.id))
+ return projector
+
+ def get_projector_by_name(self, name):
+ """
+ Locate a projector by name field
+
+ :param name: Name of projector
+ :returns: Projector() instance
+ """
+ log.debug('get_projector_by_name(name="%s")' % name)
+ projector = self.get_object_filtered(Projector, Projector.name == name)
+ if projector is None:
+ # Not found
+ log.warn('get_projector_by_name() did not find "%s"' % name)
+ return None
+ log.debug('get_projector_by_name() returning one entry for "%s" id="%s"' % (name, projector.id))
+ return projector
+
+ def add_projector(self, projector):
+ """
+ Add a new projector entry
+
+ :param projector: Projector() instance to add
+ :returns: bool
+ True if entry added
+ False if entry already in DB or db error
+ """
+ old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip)
+ if old_projector is not None:
+ log.warn('add_new() skipping entry ip="%s" (Already saved)' % old_projector.ip)
+ return False
+ log.debug('add_new() saving new entry')
+ log.debug('ip="%s", name="%s", location="%s"' % (projector.ip,
+ projector.name,
+ projector.location))
+ log.debug('notes="%s"' % projector.notes)
+ return self.save_object(projector)
+
+ def update_projector(self, projector=None):
+ """
+ Update projector entry
+
+ :param projector: Projector() instance with new information
+ :returns: bool
+ True if DB record updated
+ False if entry not in DB or DB error
+ """
+ if projector is None:
+ log.error('No Projector() instance to update - cancelled')
+ return False
+ old_projector = self.get_object_filtered(Projector, Projector.id == projector.id)
+ if old_projector is None:
+ log.error('Edit called on projector instance not in database - cancelled')
+ return False
+ log.debug('(%s) Updating projector with dbid=%s' % (projector.ip, projector.id))
+ old_projector.ip = projector.ip
+ old_projector.name = projector.name
+ old_projector.location = projector.location
+ old_projector.pin = projector.pin
+ old_projector.port = projector.port
+ old_projector.pjlink_name = projector.pjlink_name
+ old_projector.manufacturer = projector.manufacturer
+ old_projector.model = projector.model
+ old_projector.other = projector.other
+ old_projector.sources = projector.sources
+ return self.save_object(old_projector)
+
+ def delete_projector(self, projector):
+ """
+ Delete an entry by record id
+
+ :param projector: Projector() instance to delete
+ :returns: bool
+ True if record deleted
+ False if DB error
+ """
+ deleted = self.delete_object(Projector, projector.id)
+ if deleted:
+ log.debug('delete_by_id() Removed entry id="%s"' % projector.id)
+ else:
+ log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id)
+ return deleted
+
+ def get_source_list(self, projector):
+ """
+ Retrieves the source inputs pjlink code-to-text if available based on
+ manufacturer and model.
+ If not available, then returns the PJLink code to default text.
+
+ :param projector: Projector instance
+ :returns: dict
+ key: (str) PJLink code for source
+ value: (str) From ProjectorSource, Sources tables or PJLink default code list
+ """
+ source_dict = {}
+ # Get default list first
+ for key in projector.source_available:
+ item = self.get_object_filtered(ProjectorSource,
+ and_(ProjectorSource.code == key,
+ ProjectorSource.projector_id == projector.dbid))
+ if item is None:
+ source_dict[key] = PJLINK_DEFAULT_CODES[key]
+ else:
+ source_dict[key] = item.text
+ return source_dict
+
+ def get_source_by_id(self, source):
+ """
+ Retrieves the ProjectorSource by ProjectorSource.id
+
+ :param source: ProjectorSource id
+ :returns: ProjetorSource instance or None
+ """
+ source_entry = self.get_object_filtered(ProjetorSource, ProjectorSource.id == source)
+ if source_entry is None:
+ # Not found
+ log.warn('get_source_by_id() did not find "%s"' % source)
+ return None
+ log.debug('get_source_by_id() returning one entry for "%s""' % (source))
+ return source_entry
+
+ def get_source_by_code(self, code, projector_id):
+ """
+ Retrieves the ProjectorSource by ProjectorSource.id
+
+ :param source: PJLink ID
+ :param projector_id: Projector.id
+ :returns: ProjetorSource instance or None
+ """
+ source_entry = self.get_object_filtered(ProjectorSource,
+ and_(ProjectorSource.code == code,
+ ProjectorSource.projector_id == projector_id))
+ if source_entry is None:
+ # Not found
+ log.warn('get_source_by_id() did not find code="%s" projector_id="%s"' % (code, projector_id))
+ return None
+ log.debug('get_source_by_id() returning one entry for code="%s" projector_id="%s"' % (code, projector_id))
+ return source_entry
+
+ def add_source(self, source):
+ """
+ Add a new ProjectorSource record
+
+ :param source: ProjectorSource() instance to add
+ """
+ log.debug('Saving ProjectorSource(projector_id="%s" code="%s" text="%s")' % (source.projector_id,
+ source.code, source.text))
+ return self.save_object(source)
=== added file 'openlp/core/lib/projector/pjlink1.py'
--- openlp/core/lib/projector/pjlink1.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/projector/pjlink1.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,912 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+ :mod:`openlp.core.lib.projector.pjlink1` module
+ Provides the necessary functions for connecting to a PJLink-capable projector.
+
+ See PJLink Class 1 Specifications for details.
+ http://pjlink.jbmia.or.jp/english/dl.html
+
+ Section 5-1 PJLink Specifications
+
+ Section 5-5 Guidelines for Input Terminals
+
+ NOTE:
+ Function names follow the following syntax:
+ def process_CCCC(...):
+ WHERE:
+ CCCC = PJLink command being processed.
+"""
+
+import logging
+log = logging.getLogger(__name__)
+
+log.debug('pjlink1 loaded')
+
+__all__ = ['PJLink1']
+
+from codecs import decode
+
+from PyQt5.QtCore import pyqtSignal, pyqtSlot
+from PyQt5.QtNetwork import QAbstractSocket, QTcpSocket
+
+from openlp.core.common import translate, qmd5_hash
+from openlp.core.lib.projector.constants import *
+
+# Shortcuts
+SocketError = QAbstractSocket.SocketError
+SocketSTate = QAbstractSocket.SocketState
+
+PJLINK_PREFIX = '%'
+PJLINK_CLASS = '1'
+PJLINK_HEADER = '%s%s' % (PJLINK_PREFIX, PJLINK_CLASS)
+PJLINK_SUFFIX = CR
+
+
+class PJLink1(QTcpSocket):
+ """
+ Socket service for connecting to a PJLink-capable projector.
+ """
+ # Signals sent by this module
+ changeStatus = pyqtSignal(str, int, str)
+ projectorNetwork = pyqtSignal(int) # Projector network activity
+ projectorStatus = pyqtSignal(int) # Status update
+ projectorAuthentication = pyqtSignal(str) # Authentication error
+ projectorNoAuthentication = pyqtSignal(str) # PIN set and no authentication needed
+ projectorReceivedData = pyqtSignal() # Notify when received data finished processing
+ projectorUpdateIcons = pyqtSignal() # Update the status icons on toolbar
+
+ def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
+ """
+ Setup for instance.
+
+ :param name: Display name
+ :param ip: IP address to connect to
+ :param port: Port to use. Default to PJLINK_PORT
+ :param pin: Access pin (if needed)
+
+ Optional parameters
+ :param dbid: Database ID number
+ :param location: Location where projector is physically located
+ :param notes: Extra notes about the projector
+ :param poll_time: Time (in seconds) to poll connected projector
+ :param socket_timeout: Time (in seconds) to abort the connection if no response
+ """
+ log.debug('PJlink(args="%s" kwargs="%s")' % (args, kwargs))
+ self.name = name
+ self.ip = ip
+ self.port = port
+ self.pin = pin
+ super(PJLink1, self).__init__()
+ self.dbid = None
+ self.location = None
+ self.notes = None
+ self.dbid = None if 'dbid' not in kwargs else kwargs['dbid']
+ self.location = None if 'location' not in kwargs else kwargs['location']
+ self.notes = None if 'notes' not in kwargs else kwargs['notes']
+ # Poll time 20 seconds unless called with something else
+ self.poll_time = 20000 if 'poll_time' not in kwargs else kwargs['poll_time'] * 1000
+ # Timeout 5 seconds unless called with something else
+ self.socket_timeout = 5000 if 'socket_timeout' not in kwargs else kwargs['socket_timeout'] * 1000
+ # In case we're called from somewhere that only wants information
+ self.no_poll = 'no_poll' in kwargs
+ self.i_am_running = False
+ self.status_connect = S_NOT_CONNECTED
+ self.last_command = ''
+ self.projector_status = S_NOT_CONNECTED
+ self.error_status = S_OK
+ # Socket information
+ # Add enough space to input buffer for extraneous \n \r
+ self.maxSize = PJLINK_MAX_PACKET + 2
+ self.setReadBufferSize(self.maxSize)
+ # PJLink information
+ self.pjlink_class = '1' # Default class
+ self.reset_information()
+ # Set from ProjectorManager.add_projector()
+ self.widget = None # QListBox entry
+ self.timer = None # Timer that calls the poll_loop
+ self.send_queue = []
+ self.send_busy = False
+ # Socket timer for some possible brain-dead projectors or network cable pulled
+ self.socket_timer = None
+ # Map command to function
+ self.PJLINK1_FUNC = {'AVMT': self.process_avmt,
+ 'CLSS': self.process_clss,
+ 'ERST': self.process_erst,
+ 'INFO': self.process_info,
+ 'INF1': self.process_inf1,
+ 'INF2': self.process_inf2,
+ 'INPT': self.process_inpt,
+ 'INST': self.process_inst,
+ 'LAMP': self.process_lamp,
+ 'NAME': self.process_name,
+ 'PJLINK': self.check_login,
+ 'POWR': self.process_powr
+ }
+
+ def reset_information(self):
+ """
+ Reset projector-specific information to default
+ """
+ log.debug('(%s) reset_information() connect status is %s' % (self.ip, self.state()))
+ self.power = S_OFF
+ self.pjlink_name = None
+ self.manufacturer = None
+ self.model = None
+ self.shutter = None
+ self.mute = None
+ self.lamp = None
+ self.fan = None
+ self.source_available = None
+ self.source = None
+ self.other_info = None
+ if hasattr(self, 'timer'):
+ self.timer.stop()
+ if hasattr(self, 'socket_timer'):
+ self.socket_timer.stop()
+ self.send_queue = []
+ self.send_busy = False
+
+ def thread_started(self):
+ """
+ Connects signals to methods when thread is started.
+ """
+ log.debug('(%s) Thread starting' % self.ip)
+ self.i_am_running = True
+ self.connected.connect(self.check_login)
+ self.disconnected.connect(self.disconnect_from_host)
+ self.error.connect(self.get_error)
+
+ def thread_stopped(self):
+ """
+ Cleanups when thread is stopped.
+ """
+ log.debug('(%s) Thread stopped' % self.ip)
+ try:
+ self.connected.disconnect(self.check_login)
+ except TypeError:
+ pass
+ try:
+ self.disconnected.disconnect(self.disconnect_from_host)
+ except TypeError:
+ pass
+ try:
+ self.error.disconnect(self.get_error)
+ except TypeError:
+ pass
+ try:
+ self.projectorReceivedData.disconnect(self._send_command)
+ except TypeError:
+ pass
+ self.disconnect_from_host()
+ self.deleteLater()
+ self.i_am_running = False
+
+ def socket_abort(self):
+ """
+ Aborts connection and closes socket in case of brain-dead projectors.
+ Should normally be called by socket_timer().
+ """
+ log.debug('(%s) socket_abort() - Killing connection' % self.ip)
+ self.disconnect_from_host(abort=True)
+
+ def poll_loop(self):
+ """
+ Retrieve information from projector that changes.
+ Normally called by timer().
+ """
+ if self.state() != self.ConnectedState:
+ return
+ log.debug('(%s) Updating projector status' % self.ip)
+ # Reset timer in case we were called from a set command
+ if self.timer.interval() < self.poll_time:
+ # Reset timer to 5 seconds
+ self.timer.setInterval(self.poll_time)
+ # Restart timer
+ self.timer.start()
+ # These commands may change during connetion
+ for command in ['POWR', 'ERST', 'LAMP', 'AVMT', 'INPT']:
+ self.send_command(command, queue=True)
+ # The following commands do not change, so only check them once
+ if self.power == S_ON and self.source_available is None:
+ self.send_command('INST', queue=True)
+ if self.other_info is None:
+ self.send_command('INFO', queue=True)
+ if self.manufacturer is None:
+ self.send_command('INF1', queue=True)
+ if self.model is None:
+ self.send_command('INF2', queue=True)
+ if self.pjlink_name is None:
+ self.send_command('NAME', queue=True)
+ if self.power == S_ON and self.source_available is None:
+ self.send_command('INST', queue=True)
+
+ def _get_status(self, status):
+ """
+ Helper to retrieve status/error codes and convert to strings.
+
+ :param status: Status/Error code
+ :returns: (Status/Error code, String)
+ """
+ if status in ERROR_STRING:
+ return ERROR_STRING[status], ERROR_MSG[status]
+ elif status in STATUS_STRING:
+ return STATUS_STRING[status], ERROR_MSG[status]
+ else:
+ return status, translate('OpenLP.PJLink1', 'Unknown status')
+
+ def change_status(self, status, msg=None):
+ """
+ Check connection/error status, set status for projector, then emit status change signal
+ for gui to allow changing the icons.
+
+ :param status: Status code
+ :param msg: Optional message
+ """
+ message = translate('OpenLP.PJLink1', 'No message') if msg is None else msg
+ (code, message) = self._get_status(status)
+ if msg is not None:
+ message = msg
+ if status in CONNECTION_ERRORS:
+ # Projector, connection state
+ self.projector_status = self.error_status = self.status_connect = E_NOT_CONNECTED
+ elif status >= S_NOT_CONNECTED and status < S_STATUS:
+ self.status_connect = status
+ self.projector_status = S_NOT_CONNECTED
+ elif status < S_NETWORK_SENDING:
+ self.status_connect = S_CONNECTED
+ self.projector_status = status
+ (status_code, status_message) = self._get_status(self.status_connect)
+ log.debug('(%s) status_connect: %s: %s' % (self.ip, status_code, status_message if msg is None else msg))
+ (status_code, status_message) = self._get_status(self.projector_status)
+ log.debug('(%s) projector_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg))
+ (status_code, status_message) = self._get_status(self.error_status)
+ log.debug('(%s) error_status: %s: %s' % (self.ip, status_code, status_message if msg is None else msg))
+ self.changeStatus.emit(self.ip, status, message)
+
+ @pyqtSlot()
+ def check_login(self, data=None):
+ """
+ Processes the initial connection and authentication (if needed).
+ Starts poll timer if connection is established.
+
+ :param data: Optional data if called from another routine
+ """
+ log.debug('(%s) check_login(data="%s")' % (self.ip, data))
+ if data is None:
+ # Reconnected setup?
+ if not self.waitForReadyRead(2000):
+ # Possible timeout issue
+ log.error('(%s) Socket timeout waiting for login' % self.ip)
+ self.change_status(E_SOCKET_TIMEOUT)
+ return
+ read = self.readLine(self.maxSize)
+ dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n
+ if read is None:
+ log.warn('(%s) read is None - socket error?' % self.ip)
+ return
+ elif len(read) < 8:
+ log.warn('(%s) Not enough data read)' % self.ip)
+ return
+ data = decode(read, 'ascii')
+ # Possibility of extraneous data on input when reading.
+ # Clean out extraneous characters in buffer.
+ dontcare = self.readLine(self.maxSize)
+ log.debug('(%s) check_login() read "%s"' % (self.ip, data.strip()))
+ # At this point, we should only have the initial login prompt with
+ # possible authentication
+ # PJLink initial login will be:
+ # 'PJLink 0' - Unauthenticated login - no extra steps required.
+ # 'PJLink 1 XXXXXX' Authenticated login - extra processing required.
+ if not data.upper().startswith('PJLINK'):
+ # Invalid response
+ return self.disconnect_from_host()
+ if '=' in data:
+ # Processing a login reply
+ data_check = data.strip().split('=')
+ else:
+ # Process initial connection
+ data_check = data.strip().split(' ')
+ log.debug('(%s) data_check="%s"' % (self.ip, data_check))
+ # Check for projector reporting an error
+ if data_check[1].upper() == 'ERRA':
+ # Authentication error
+ self.disconnect_from_host()
+ self.change_status(E_AUTHENTICATION)
+ log.debug('(%s) emitting projectorAuthentication() signal' % self.name)
+ return
+ elif data_check[1] == '0' and self.pin is not None:
+ # Pin set and no authentication needed
+ self.disconnect_from_host()
+ self.change_status(E_AUTHENTICATION)
+ log.debug('(%s) emitting projectorNoAuthentication() signal' % self.name)
+ self.projectorNoAuthentication.emit(self.name)
+ return
+ elif data_check[1] == '1':
+ # Authenticated login with salt
+ log.debug('(%s) Setting hash with salt="%s"' % (self.ip, data_check[2]))
+ log.debug('(%s) pin="%s"' % (self.ip, self.pin))
+ salt = qmd5_hash(salt=data_check[2].encode('ascii'), data=self.pin.encode('ascii'))
+ else:
+ salt = None
+ # We're connected at this point, so go ahead and do regular I/O
+ self.readyRead.connect(self.get_data)
+ self.projectorReceivedData.connect(self._send_command)
+ # Initial data we should know about
+ self.send_command(cmd='CLSS', salt=salt)
+ self.waitForReadyRead()
+ if (not self.no_poll) and (self.state() == self.ConnectedState):
+ log.debug('(%s) Starting timer' % self.ip)
+ self.timer.setInterval(2000) # Set 2 seconds for initial information
+ self.timer.start()
+
+ @pyqtSlot()
+ def get_data(self):
+ """
+ Socket interface to retrieve data.
+ """
+ log.debug('(%s) get_data(): Reading data' % self.ip)
+ if self.state() != self.ConnectedState:
+ log.debug('(%s) get_data(): Not connected - returning' % self.ip)
+ self.send_busy = False
+ return
+ read = self.readLine(self.maxSize)
+ if read == -1:
+ # No data available
+ log.debug('(%s) get_data(): No data available (-1)' % self.ip)
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+ self.socket_timer.stop()
+ self.projectorNetwork.emit(S_NETWORK_RECEIVED)
+ data_in = decode(read, 'ascii')
+ data = data_in.strip()
+ if len(data) < 7:
+ # Not enough data for a packet
+ log.debug('(%s) get_data(): Packet length < 7: "%s"' % (self.ip, data))
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+ log.debug('(%s) get_data(): Checking new data "%s"' % (self.ip, data))
+ if data.upper().startswith('PJLINK'):
+ # Reconnected from remote host disconnect ?
+ self.check_login(data)
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+ elif '=' not in data:
+ log.warn('(%s) get_data(): Invalid packet received' % self.ip)
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+ data_split = data.split('=')
+ try:
+ (prefix, class_, cmd, data) = (data_split[0][0], data_split[0][1], data_split[0][2:], data_split[1])
+ except ValueError as e:
+ log.warn('(%s) get_data(): Invalid packet - expected header + command + data' % self.ip)
+ log.warn('(%s) get_data(): Received data: "%s"' % (self.ip, read))
+ self.change_status(E_INVALID_DATA)
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+
+ if not (self.pjlink_class in PJLINK_VALID_CMD and cmd in PJLINK_VALID_CMD[self.pjlink_class]):
+ log.warn('(%s) get_data(): Invalid packet - unknown command "%s"' % (self.ip, cmd))
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+ return self.process_command(cmd, data)
+
+ @pyqtSlot(int)
+ def get_error(self, err):
+ """
+ Process error from SocketError signal.
+ Remaps system error codes to projector error codes.
+
+ :param err: Error code
+ """
+ log.debug('(%s) get_error(err=%s): %s' % (self.ip, err, self.errorString()))
+ if err <= 18:
+ # QSocket errors. Redefined in projector.constants so we don't mistake
+ # them for system errors
+ check = err + E_CONNECTION_REFUSED
+ self.timer.stop()
+ else:
+ check = err
+ if check < E_GENERAL:
+ # Some system error?
+ self.change_status(err, self.errorString())
+ else:
+ self.change_status(E_NETWORK, self.errorString())
+ self.projectorUpdateIcons.emit()
+ if self.status_connect == E_NOT_CONNECTED:
+ self.abort()
+ self.reset_information()
+ return
+
+ def send_command(self, cmd, opts='?', salt=None, queue=False):
+ """
+ Add command to output queue if not already in queue.
+
+ :param cmd: Command to send
+ :param opts: Command option (if any) - defaults to '?' (get information)
+ :param salt: Optional salt for md5 hash initial authentication
+ :param queue: Option to force add to queue rather than sending directly
+ """
+ if self.state() != self.ConnectedState:
+ log.warn('(%s) send_command(): Not connected - returning' % self.ip)
+ self.send_queue = []
+ return
+ self.projectorNetwork.emit(S_NETWORK_SENDING)
+ log.debug('(%s) send_command(): Building cmd="%s" opts="%s" %s' % (self.ip,
+ cmd,
+ opts,
+ '' if salt is None else 'with hash'))
+ if salt is None:
+ out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR)
+ else:
+ out = '%s%s%s %s%s' % (salt, PJLINK_HEADER, cmd, opts, CR)
+ if out in self.send_queue:
+ # Already there, so don't add
+ log.debug('(%s) send_command(out="%s") Already in queue - skipping' % (self.ip, out.strip()))
+ elif not queue and len(self.send_queue) == 0:
+ # Nothing waiting to send, so just send it
+ log.debug('(%s) send_command(out="%s") Sending data' % (self.ip, out.strip()))
+ return self._send_command(data=out)
+ else:
+ log.debug('(%s) send_command(out="%s") adding to queue' % (self.ip, out.strip()))
+ self.send_queue.append(out)
+ self.projectorReceivedData.emit()
+ log.debug('(%s) send_command(): send_busy is %s' % (self.ip, self.send_busy))
+ if not self.send_busy:
+ log.debug('(%s) send_command() calling _send_string()')
+ self._send_command()
+
+ @pyqtSlot()
+ def _send_command(self, data=None):
+ """
+ Socket interface to send data. If data=None, then check queue.
+
+ :param data: Immediate data to send
+ """
+ log.debug('(%s) _send_string()' % self.ip)
+ log.debug('(%s) _send_string(): Connection status: %s' % (self.ip, self.state()))
+ if self.state() != self.ConnectedState:
+ log.debug('(%s) _send_string() Not connected - abort' % self.ip)
+ self.send_queue = []
+ self.send_busy = False
+ return
+ if self.send_busy:
+ # Still waiting for response from last command sent
+ return
+ if data is not None:
+ out = data
+ log.debug('(%s) _send_string(data=%s)' % (self.ip, out.strip()))
+ elif len(self.send_queue) != 0:
+ out = self.send_queue.pop(0)
+ log.debug('(%s) _send_string(queued data=%s)' % (self.ip, out.strip()))
+ else:
+ # No data to send
+ log.debug('(%s) _send_string(): No data to send' % self.ip)
+ self.send_busy = False
+ return
+ self.send_busy = True
+ log.debug('(%s) _send_string(): Sending "%s"' % (self.ip, out.strip()))
+ log.debug('(%s) _send_string(): Queue = %s' % (self.ip, self.send_queue))
+ self.socket_timer.start()
+ self.projectorNetwork.emit(S_NETWORK_SENDING)
+ sent = self.write(out.encode('ascii'))
+ self.waitForBytesWritten(2000) # 2 seconds should be enough
+ if sent == -1:
+ # Network error?
+ self.change_status(E_NETWORK,
+ translate('OpenLP.PJLink1', 'Error while sending data to projector'))
+
+ def process_command(self, cmd, data):
+ """
+ Verifies any return error code. Calls the appropriate command handler.
+
+ :param cmd: Command to process
+ :param data: Data being processed
+ """
+ log.debug('(%s) Processing command "%s"' % (self.ip, cmd))
+ if data in PJLINK_ERRORS:
+ # Oops - projector error
+ if data.upper() == 'ERRA':
+ # Authentication error
+ self.disconnect_from_host()
+ self.change_status(E_AUTHENTICATION)
+ log.debug('(%s) emitting projectorAuthentication() signal' % self.ip)
+ self.projectorAuthentication.emit(self.name)
+ elif data.upper() == 'ERR1':
+ # Undefined command
+ self.change_status(E_UNDEFINED, '%s "%s"' %
+ (translate('OpenLP.PJLink1', 'Undefined command:'), cmd))
+ elif data.upper() == 'ERR2':
+ # Invalid parameter
+ self.change_status(E_PARAMETER)
+ elif data.upper() == 'ERR3':
+ # Projector busy
+ self.change_status(E_UNAVAILABLE)
+ elif data.upper() == 'ERR4':
+ # Projector/display error
+ self.change_status(E_PROJECTOR)
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+ # Command succeeded - no extra information
+ elif data.upper() == 'OK':
+ log.debug('(%s) Command returned OK' % self.ip)
+ # A command returned successfully, recheck data
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+ return
+
+ if cmd in self.PJLINK1_FUNC:
+ self.PJLINK1_FUNC[cmd](data)
+ else:
+ log.warn('(%s) Invalid command %s' % (self.ip, cmd))
+ self.send_busy = False
+ self.projectorReceivedData.emit()
+
+ def process_lamp(self, data):
+ """
+ Lamp(s) status. See PJLink Specifications for format.
+ Data may have more than 1 lamp to process.
+ Update self.lamp dictionary with lamp status.
+
+ :param data: Lamp(s) status.
+ """
+ lamps = []
+ data_dict = data.split()
+ while data_dict:
+ try:
+ fill = {'Hours': int(data_dict[0]), 'On': False if data_dict[1] == '0' else True}
+ except ValueError:
+ # In case of invalid entry
+ log.warn('(%s) process_lamp(): Invalid data "%s"' % (self.ip, data))
+ return
+ lamps.append(fill)
+ data_dict.pop(0) # Remove lamp hours
+ data_dict.pop(0) # Remove lamp on/off
+ self.lamp = lamps
+ return
+
+ def process_powr(self, data):
+ """
+ Power status. See PJLink specification for format.
+ Update self.power with status. Update icons if change from previous setting.
+
+ :param data: Power status
+ """
+ if data in PJLINK_POWR_STATUS:
+ power = PJLINK_POWR_STATUS[data]
+ update_icons = self.power != power
+ self.power = power
+ self.change_status(PJLINK_POWR_STATUS[data])
+ if update_icons:
+ self.projectorUpdateIcons.emit()
+ # Update the input sources available
+ if power == S_ON:
+ self.send_command('INST')
+ else:
+ # Log unknown status response
+ log.warn('Unknown power response: %s' % data)
+ return
+
+ def process_avmt(self, data):
+ """
+ Process shutter and speaker status. See PJLink specification for format.
+ Update self.mute (audio) and self.shutter (video shutter).
+
+ :param data: Shutter and audio status
+ """
+ shutter = self.shutter
+ mute = self.mute
+ if data == '11':
+ shutter = True
+ mute = False
+ elif data == '21':
+ shutter = False
+ mute = True
+ elif data == '30':
+ shutter = False
+ mute = False
+ elif data == '31':
+ shutter = True
+ mute = True
+ else:
+ log.warn('Unknown shutter response: %s' % data)
+ update_icons = shutter != self.shutter
+ update_icons = update_icons or mute != self.mute
+ self.shutter = shutter
+ self.mute = mute
+ if update_icons:
+ self.projectorUpdateIcons.emit()
+ return
+
+ def process_inpt(self, data):
+ """
+ Current source input selected. See PJLink specification for format.
+ Update self.source
+
+ :param data: Currently selected source
+ """
+ self.source = data
+ return
+
+ def process_clss(self, data):
+ """
+ PJLink class that this projector supports. See PJLink specification for format.
+ Updates self.class.
+
+ :param data: Class that projector supports.
+ """
+ # bug 1550891: Projector returns non-standard class response:
+ # : Expected: %1CLSS=1
+ # : Received: %1CLSS=Class 1
+ if len(data) > 1:
+ # Split non-standard information from response
+ clss = data.split()[-1]
+ else:
+ clss = data
+ self.pjlink_class = clss
+ log.debug('(%s) Setting pjlink_class for this projector to "%s"' % (self.ip, self.pjlink_class))
+ return
+
+ def process_name(self, data):
+ """
+ Projector name set in projector.
+ Updates self.pjlink_name
+
+ :param data: Projector name
+ """
+ self.pjlink_name = data
+ return
+
+ def process_inf1(self, data):
+ """
+ Manufacturer name set in projector.
+ Updates self.manufacturer
+
+ :param data: Projector manufacturer
+ """
+ self.manufacturer = data
+ return
+
+ def process_inf2(self, data):
+ """
+ Projector Model set in projector.
+ Updates self.model.
+
+ :param data: Model name
+ """
+ self.model = data
+ return
+
+ def process_info(self, data):
+ """
+ Any extra info set in projector.
+ Updates self.other_info.
+
+ :param data: Projector other info
+ """
+ self.other_info = data
+ return
+
+ def process_inst(self, data):
+ """
+ Available source inputs. See PJLink specification for format.
+ Updates self.source_available
+
+ :param data: Sources list
+ """
+ sources = []
+ check = data.split()
+ for source in check:
+ sources.append(source)
+ sources.sort()
+ self.source_available = sources
+ self.projectorUpdateIcons.emit()
+ return
+
+ def process_erst(self, data):
+ """
+ Error status. See PJLink Specifications for format.
+ Updates self.projector_errors
+
+ :param data: Error status
+ """
+ try:
+ datacheck = int(data)
+ except ValueError:
+ # Bad data - ignore
+ return
+ if datacheck == 0:
+ self.projector_errors = None
+ else:
+ self.projector_errors = {}
+ # Fan
+ if data[0] != '0':
+ self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Fan')] = \
+ PJLINK_ERST_STATUS[data[0]]
+ # Lamp
+ if data[1] != '0':
+ self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Lamp')] = \
+ PJLINK_ERST_STATUS[data[1]]
+ # Temp
+ if data[2] != '0':
+ self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Temperature')] = \
+ PJLINK_ERST_STATUS[data[2]]
+ # Cover
+ if data[3] != '0':
+ self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Cover')] = \
+ PJLINK_ERST_STATUS[data[3]]
+ # Filter
+ if data[4] != '0':
+ self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Filter')] = \
+ PJLINK_ERST_STATUS[data[4]]
+ # Other
+ if data[5] != '0':
+ self.projector_errors[translate('OpenLP.ProjectorPJLink', 'Other')] = \
+ PJLINK_ERST_STATUS[data[5]]
+ return
+
+ def connect_to_host(self):
+ """
+ Initiate connection to projector.
+ """
+ if self.state() == self.ConnectedState:
+ log.warn('(%s) connect_to_host(): Already connected - returning' % self.ip)
+ return
+ self.change_status(S_CONNECTING)
+ self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port))
+
+ @pyqtSlot()
+ def disconnect_from_host(self, abort=False):
+ """
+ Close socket and cleanup.
+ """
+ if abort or self.state() != self.ConnectedState:
+ if abort:
+ log.warn('(%s) disconnect_from_host(): Aborting connection' % self.ip)
+ else:
+ log.warn('(%s) disconnect_from_host(): Not connected - returning' % self.ip)
+ self.reset_information()
+ self.disconnectFromHost()
+ try:
+ self.readyRead.disconnect(self.get_data)
+ except TypeError:
+ pass
+ if abort:
+ self.change_status(E_NOT_CONNECTED)
+ else:
+ log.debug('(%s) disconnect_from_host() Current status %s' % (self.ip,
+ self._get_status(self.status_connect)[0]))
+ if self.status_connect != E_NOT_CONNECTED:
+ self.change_status(S_NOT_CONNECTED)
+ self.reset_information()
+ self.projectorUpdateIcons.emit()
+
+ def get_available_inputs(self):
+ """
+ Send command to retrieve available source inputs.
+ """
+ return self.send_command(cmd='INST')
+
+ def get_error_status(self):
+ """
+ Send command to retrieve currently known errors.
+ """
+ return self.send_command(cmd='ERST')
+
+ def get_input_source(self):
+ """
+ Send command to retrieve currently selected source input.
+ """
+ return self.send_command(cmd='INPT')
+
+ def get_lamp_status(self):
+ """
+ Send command to return the lap status.
+ """
+ return self.send_command(cmd='LAMP')
+
+ def get_manufacturer(self):
+ """
+ Send command to retrieve manufacturer name.
+ """
+ return self.send_command(cmd='INF1')
+
+ def get_model(self):
+ """
+ Send command to retrieve the model name.
+ """
+ return self.send_command(cmd='INF2')
+
+ def get_name(self):
+ """
+ Send command to retrieve name as set by end-user (if set).
+ """
+ return self.send_command(cmd='NAME')
+
+ def get_other_info(self):
+ """
+ Send command to retrieve extra info set by manufacturer.
+ """
+ return self.send_command(cmd='INFO')
+
+ def get_power_status(self):
+ """
+ Send command to retrieve power status.
+ """
+ return self.send_command(cmd='POWR')
+
+ def get_shutter_status(self):
+ """
+ Send command to retrieve shutter status.
+ """
+ return self.send_command(cmd='AVMT')
+
+ def set_input_source(self, src=None):
+ """
+ Verify input source available as listed in 'INST' command,
+ then send the command to select the input source.
+
+ :param src: Video source to select in projector
+ """
+ log.debug('(%s) set_input_source(src=%s)' % (self.ip, src))
+ if self.source_available is None:
+ return
+ elif src not in self.source_available:
+ return
+ log.debug('(%s) Setting input source to %s' % (self.ip, src))
+ self.send_command(cmd='INPT', opts=src)
+ self.poll_loop()
+
+ def set_power_on(self):
+ """
+ Send command to turn power to on.
+ """
+ self.send_command(cmd='POWR', opts='1')
+ self.poll_loop()
+
+ def set_power_off(self):
+ """
+ Send command to turn power to standby.
+ """
+ self.send_command(cmd='POWR', opts='0')
+ self.poll_loop()
+
+ def set_shutter_closed(self):
+ """
+ Send command to set shutter to closed position.
+ """
+ self.send_command(cmd='AVMT', opts='11')
+ self.poll_loop()
+
+ def set_shutter_open(self):
+ """
+ Send command to set shutter to open position.
+ """
+ self.send_command(cmd='AVMT', opts='10')
+ self.poll_loop()
=== added file 'openlp/core/lib/renderer.py'
--- openlp/core/lib/renderer.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/renderer.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,569 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 re
+
+from PyQt5 import QtGui, QtCore, QtWebKitWidgets
+
+from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
+from openlp.core.lib import FormattingTags, ImageSource, ItemCapabilities, ScreenList, ServiceItem, expand_tags, \
+ build_lyrics_format_css, build_lyrics_outline_css
+from openlp.core.common import ThemeLevel
+from openlp.core.ui import MainDisplay
+
+VERSE = 'The Lord said to {r}Noah{/r}: \n' \
+ 'There\'s gonna be a {su}floody{/su}, {sb}floody{/sb}\n' \
+ 'The Lord said to {g}Noah{/g}:\n' \
+ 'There\'s gonna be a {st}floody{/st}, {it}floody{/it}\n' \
+ 'Get those children out of the muddy, muddy \n' \
+ '{r}C{/r}{b}h{/b}{bl}i{/bl}{y}l{/y}{g}d{/g}{pk}' \
+ 'r{/pk}{o}e{/o}{pp}n{/pp} of the Lord\n'
+VERSE_FOR_LINE_COUNT = '\n'.join(map(str, range(100)))
+FOOTER = ['Arky Arky (Unknown)', 'Public Domain', 'CCLI 123456']
+
+
+class Renderer(OpenLPMixin, RegistryMixin, RegistryProperties):
+ """
+ Class to pull all Renderer interactions into one place. The plugins will call helper methods to do the rendering but
+ this class will provide display defense code.
+ """
+
+ def __init__(self):
+ """
+ Initialise the renderer.
+ """
+ super(Renderer, self).__init__(None)
+ # Need live behaviour if this is also working as a pseudo MainDisplay.
+ self.screens = ScreenList()
+ self.theme_level = ThemeLevel.Global
+ self.global_theme_name = ''
+ self.service_theme_name = ''
+ self.item_theme_name = ''
+ self.force_page = False
+ self._theme_dimensions = {}
+ self._calculate_default()
+ self.web = QtWebKitWidgets.QWebView()
+ self.web.setVisible(False)
+ self.web_frame = self.web.page().mainFrame()
+ Registry().register_function('theme_update_global', self.set_global_theme)
+
+ def bootstrap_initialise(self):
+ """
+ Initialise functions
+ """
+ self.display = MainDisplay(self)
+ self.display.setup()
+
+ def update_display(self):
+ """
+ Updates the renderer's information about the current screen.
+ """
+ self._calculate_default()
+ if self.display:
+ self.display.close()
+ self.display = MainDisplay(self)
+ self.display.setup()
+ self._theme_dimensions = {}
+
+ def update_theme(self, theme_name, old_theme_name=None, only_delete=False):
+ """
+ This method updates the theme in ``_theme_dimensions`` when a theme has been edited or renamed.
+
+ :param theme_name: The current theme name.
+ :param old_theme_name: The old theme name. Has only to be passed, when the theme has been renamed.
+ Defaults to *None*.
+ :param only_delete: Only remove the given ``theme_name`` from the ``_theme_dimensions`` list. This can be
+ used when a theme is permanently deleted.
+ """
+ if old_theme_name is not None and old_theme_name in self._theme_dimensions:
+ del self._theme_dimensions[old_theme_name]
+ if theme_name in self._theme_dimensions:
+ del self._theme_dimensions[theme_name]
+ if not only_delete and theme_name:
+ self._set_theme(theme_name)
+
+ def _set_theme(self, theme_name):
+ """
+ Helper method to save theme names and theme data.
+
+ :param theme_name: The theme name
+ """
+ self.log_debug("_set_theme with theme %s" % theme_name)
+ if theme_name not in self._theme_dimensions:
+ theme_data = self.theme_manager.get_theme_data(theme_name)
+ main_rect = self.get_main_rectangle(theme_data)
+ footer_rect = self.get_footer_rectangle(theme_data)
+ self._theme_dimensions[theme_name] = [theme_data, main_rect, footer_rect]
+ else:
+ theme_data, main_rect, footer_rect = self._theme_dimensions[theme_name]
+ # if No file do not update cache
+ if theme_data.background_filename:
+ self.image_manager.add_image(theme_data.background_filename,
+ ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
+
+ def pre_render(self, override_theme_data=None):
+ """
+ Set up the theme to be used before rendering an item.
+
+ :param override_theme_data: The theme data should be passed, when we want to use our own theme data, regardless
+ of the theme level. This should for example be used in the theme manager. **Note**, this is **not** to
+ be mixed up with the ``set_item_theme`` method.
+ """
+ # Just assume we use the global theme.
+ theme_to_use = self.global_theme_name
+ # The theme level is either set to Service or Item. Use the service theme if one is set. We also have to use the
+ # service theme, even when the theme level is set to Item, because the item does not necessarily have to have a
+ # theme.
+ if self.theme_level != ThemeLevel.Global:
+ # When the theme level is at Service and we actually have a service theme then use it.
+ if self.service_theme_name:
+ theme_to_use = self.service_theme_name
+ # If we have Item level and have an item theme then use it.
+ if self.theme_level == ThemeLevel.Song and self.item_theme_name:
+ theme_to_use = self.item_theme_name
+ if override_theme_data is None:
+ if theme_to_use not in self._theme_dimensions:
+ self._set_theme(theme_to_use)
+ theme_data, main_rect, footer_rect = self._theme_dimensions[theme_to_use]
+ else:
+ # Ignore everything and use own theme data.
+ theme_data = override_theme_data
+ main_rect = self.get_main_rectangle(override_theme_data)
+ footer_rect = self.get_footer_rectangle(override_theme_data)
+ self._set_text_rectangle(theme_data, main_rect, footer_rect)
+ return theme_data, self._rect, self._rect_footer
+
+ def set_theme_level(self, theme_level):
+ """
+ Sets the theme level.
+
+ :param theme_level: The theme level to be used.
+ """
+ self.theme_level = theme_level
+
+ def set_global_theme(self):
+ """
+ Set the global-level theme name.
+ """
+ global_theme_name = Settings().value('themes/global theme')
+ self._set_theme(global_theme_name)
+ self.global_theme_name = global_theme_name
+
+ def set_service_theme(self, service_theme_name):
+ """
+ Set the service-level theme.
+
+ :param service_theme_name: The service level theme's name.
+ """
+ self._set_theme(service_theme_name)
+ self.service_theme_name = service_theme_name
+
+ def set_item_theme(self, item_theme_name):
+ """
+ Set the item-level theme. **Note**, this has to be done for each item we are rendering.
+
+ :param item_theme_name: The item theme's name.
+ """
+ self.log_debug("set_item_theme with theme %s" % item_theme_name)
+ self._set_theme(item_theme_name)
+ self.item_theme_name = item_theme_name
+
+ def generate_preview(self, theme_data, force_page=False):
+ """
+ Generate a preview of a theme.
+
+ :param theme_data: The theme to generated a preview for.
+ :param force_page: Flag to tell message lines per page need to be generated.
+ """
+ # save value for use in format_slide
+ self.force_page = force_page
+ # build a service item to generate preview
+ service_item = ServiceItem()
+ if self.force_page:
+ # make big page for theme edit dialog to get line count
+ service_item.add_from_text(VERSE_FOR_LINE_COUNT)
+ else:
+ service_item.add_from_text(VERSE)
+ service_item.raw_footer = FOOTER
+ # if No file do not update cache
+ if theme_data.background_filename:
+ self.image_manager.add_image(
+ theme_data.background_filename, ImageSource.Theme, QtGui.QColor(theme_data.background_border_color))
+ theme_data, main, footer = self.pre_render(theme_data)
+ service_item.theme_data = theme_data
+ service_item.main = main
+ service_item.footer = footer
+ service_item.render(True)
+ if not self.force_page:
+ self.display.build_html(service_item)
+ raw_html = service_item.get_rendered_frame(0)
+ self.display.text(raw_html, False)
+ preview = self.display.preview()
+ return preview
+ self.force_page = False
+
+ def format_slide(self, text, item):
+ """
+ Calculate how much text can fit on a slide.
+
+ :param text: The words to go on the slides.
+ :param item: The :class:`~openlp.core.lib.serviceitem.ServiceItem` item object.
+
+ """
+ self.log_debug('format slide')
+ # Add line endings after each line of text used for bibles.
+ line_end = '<br>'
+ if item.is_capable(ItemCapabilities.NoLineBreaks):
+ line_end = ' '
+ # Bibles
+ if item.is_capable(ItemCapabilities.CanWordSplit):
+ pages = self._paginate_slide_words(text.split('\n'), line_end)
+ # Songs and Custom
+ elif item.is_capable(ItemCapabilities.CanSoftBreak):
+ pages = []
+ if '[---]' in text:
+ # Remove two or more option slide breaks next to each other (causing infinite loop).
+ while '\n[---]\n[---]\n' in text:
+ text = text.replace('\n[---]\n[---]\n', '\n[---]\n')
+ while ' [---]' in text:
+ text = text.replace(' [---]', '[---]')
+ while '[---] ' in text:
+ text = text.replace('[---] ', '[---]')
+ count = 0
+ # only loop 5 times as there will never be more than 5 incorrect logical splits on a single slide.
+ while True and count < 5:
+ slides = text.split('\n[---]\n', 2)
+ # If there are (at least) two occurrences of [---] we use the first two slides (and neglect the last
+ # for now).
+ if len(slides) == 3:
+ html_text = expand_tags('\n'.join(slides[:2]))
+ # We check both slides to determine if the optional split is needed (there is only one optional
+ # split).
+ else:
+ html_text = expand_tags('\n'.join(slides))
+ html_text = html_text.replace('\n', '<br>')
+ if self._text_fits_on_slide(html_text):
+ # The first two optional slides fit (as a whole) on one slide. Replace the first occurrence
+ # of [---].
+ text = text.replace('\n[---]', '', 1)
+ else:
+ # The first optional slide fits, which means we have to render the first optional slide.
+ text_contains_split = '[---]' in text
+ if text_contains_split:
+ try:
+ text_to_render, text = text.split('\n[---]\n', 1)
+ except ValueError:
+ text_to_render = text.split('\n[---]\n')[0]
+ text = ''
+ text_to_render, raw_tags, html_tags = get_start_tags(text_to_render)
+ if text:
+ text = raw_tags + text
+ else:
+ text_to_render = text
+ text = ''
+ lines = text_to_render.strip('\n').split('\n')
+ slides = self._paginate_slide(lines, line_end)
+ if len(slides) > 1 and text:
+ # Add all slides apart from the last one the list.
+ pages.extend(slides[:-1])
+ if text_contains_split:
+ text = slides[-1] + '\n[---]\n' + text
+ else:
+ text = slides[-1] + '\n' + text
+ text = text.replace('<br>', '\n')
+ else:
+ pages.extend(slides)
+ if '[---]' not in text:
+ lines = text.strip('\n').split('\n')
+ pages.extend(self._paginate_slide(lines, line_end))
+ break
+ count += 1
+ else:
+ # Clean up line endings.
+ pages = self._paginate_slide(text.split('\n'), line_end)
+ else:
+ pages = self._paginate_slide(text.split('\n'), line_end)
+ new_pages = []
+ for page in pages:
+ while page.endswith('<br>'):
+ page = page[:-4]
+ new_pages.append(page)
+ return new_pages
+
+ def _calculate_default(self):
+ """
+ Calculate the default dimensions of the screen.
+ """
+ screen_size = self.screens.current['size']
+ self.width = screen_size.width()
+ self.height = screen_size.height()
+ self.screen_ratio = self.height / self.width
+ self.log_debug('_calculate default %s, %f' % (screen_size, self.screen_ratio))
+ # 90% is start of footer
+ self.footer_start = int(self.height * 0.90)
+
+ def get_main_rectangle(self, theme_data):
+ """
+ Calculates the placement and size of the main rectangle.
+
+ :param theme_data: The theme information
+ """
+ if not theme_data.font_main_override:
+ return QtCore.QRect(10, 0, self.width - 20, self.footer_start)
+ else:
+ return QtCore.QRect(theme_data.font_main_x, theme_data.font_main_y,
+ theme_data.font_main_width - 1, theme_data.font_main_height - 1)
+
+ def get_footer_rectangle(self, theme_data):
+ """
+ Calculates the placement and size of the footer rectangle.
+
+ :param theme_data: The theme data.
+ """
+ if not theme_data.font_footer_override:
+ return QtCore.QRect(10, self.footer_start, self.width - 20, self.height - self.footer_start)
+ else:
+ return QtCore.QRect(theme_data.font_footer_x,
+ theme_data.font_footer_y, theme_data.font_footer_width - 1,
+ theme_data.font_footer_height - 1)
+
+ def _set_text_rectangle(self, theme_data, rect_main, rect_footer):
+ """
+ Sets the rectangle within which text should be rendered.
+
+ :param theme_data: The theme data.
+ :param rect_main: The main text block.
+ :param rect_footer: The footer text block.
+ """
+ self.log_debug('_set_text_rectangle %s , %s' % (rect_main, rect_footer))
+ self._rect = rect_main
+ self._rect_footer = rect_footer
+ self.page_width = self._rect.width()
+ self.page_height = self._rect.height()
+ if theme_data.font_main_shadow:
+ self.page_width -= int(theme_data.font_main_shadow_size)
+ self.page_height -= int(theme_data.font_main_shadow_size)
+ # For the life of my I don't know why we have to completely kill the QWebView in order for the display to work
+ # properly, but we do. See bug #1041366 for an example of what happens if we take this out.
+ self.web = None
+ self.web = QtWebKitWidgets.QWebView()
+ self.web.setVisible(False)
+ self.web.resize(self.page_width, self.page_height)
+ self.web_frame = self.web.page().mainFrame()
+ # Adjust width and height to account for shadow. outline done in css.
+ html = """<!DOCTYPE html><html><head><script>
+ function show_text(newtext) {
+ var main = document.getElementById('main');
+ main.innerHTML = newtext;
+ // We need to be sure that the page is loaded, that is why we
+ // return the element's height (even though we do not use the
+ // returned value).
+ return main.offsetHeight;
+ }
+ </script><style>*{margin: 0; padding: 0; border: 0;}
+ #main {position: absolute; top: 0px; %s %s}</style></head><body>
+ <div id="main"></div></body></html>""" % \
+ (build_lyrics_format_css(theme_data, self.page_width, self.page_height),
+ build_lyrics_outline_css(theme_data))
+ self.web.setHtml(html)
+ self.empty_height = self.web_frame.contentsSize().height()
+
+ def _paginate_slide(self, lines, line_end):
+ """
+ Figure out how much text can appear on a slide, using the current theme settings.
+
+ **Note:** The smallest possible "unit" of text for a slide is one line. If the line is too long it will be cut
+ off when displayed.
+
+ :param lines: The text to be fitted on the slide split into lines.
+ :param line_end: The text added after each line. Either ``' '`` or ``'<br>``.
+ """
+ formatted = []
+ previous_html = ''
+ previous_raw = ''
+ separator = '<br>'
+ html_lines = list(map(expand_tags, lines))
+ # Text too long so go to next page.
+ if not self._text_fits_on_slide(separator.join(html_lines)):
+ html_text, previous_raw = self._binary_chop(
+ formatted, previous_html, previous_raw, html_lines, lines, separator, '')
+ else:
+ previous_raw = separator.join(lines)
+ formatted.append(previous_raw)
+ return formatted
+
+ def _paginate_slide_words(self, lines, line_end):
+ """
+ Figure out how much text can appear on a slide, using the current theme settings.
+
+ **Note:** The smallest possible "unit" of text for a slide is one word. If one line is too long it will be
+ processed word by word. This is sometimes need for **bible** verses.
+
+ :param lines: The text to be fitted on the slide split into lines.
+ :param line_end: The text added after each line. Either ``' '`` or ``'<br>``. This is needed for **bibles**.
+ """
+ formatted = []
+ previous_html = ''
+ previous_raw = ''
+ for line in lines:
+ line = line.strip()
+ html_line = expand_tags(line)
+ # Text too long so go to next page.
+ if not self._text_fits_on_slide(previous_html + html_line):
+ # Check if there was a verse before the current one and append it, when it fits on the page.
+ if previous_html:
+ if self._text_fits_on_slide(previous_html):
+ formatted.append(previous_raw)
+ previous_html = ''
+ previous_raw = ''
+ # Now check if the current verse will fit, if it does not we have to start to process the verse
+ # word by word.
+ if self._text_fits_on_slide(html_line):
+ previous_html = html_line + line_end
+ previous_raw = line + line_end
+ continue
+ # Figure out how many words of the line will fit on screen as the line will not fit as a whole.
+ raw_words = words_split(line)
+ html_words = list(map(expand_tags, raw_words))
+ previous_html, previous_raw = \
+ self._binary_chop(formatted, previous_html, previous_raw, html_words, raw_words, ' ', line_end)
+ else:
+ previous_html += html_line + line_end
+ previous_raw += line + line_end
+ formatted.append(previous_raw)
+ return formatted
+
+ def _binary_chop(self, formatted, previous_html, previous_raw, html_list, raw_list, separator, line_end):
+ """
+ This implements the binary chop algorithm for faster rendering. This algorithm works line based (line by line)
+ and word based (word by word). It is assumed that this method is **only** called, when the lines/words to be
+ rendered do **not** fit as a whole.
+
+ :param formatted: The list to append any slides.
+ :param previous_html: The html text which is know to fit on a slide, but is not yet added to the list of
+ slides. (unicode string)
+ :param previous_raw: The raw text (with formatting tags) which is know to fit on a slide, but is not yet added
+ to the list of slides. (unicode string)
+ :param html_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
+ The text contains html.
+ :param raw_list: The elements which do not fit on a slide and needs to be processed using the binary chop.
+ The elements can contain formatting tags.
+ :param separator: The separator for the elements. For lines this is ``'<br>'`` and for words this is ``' '``.
+ :param line_end: The text added after each "element line". Either ``' '`` or ``'<br>``. This is needed for
+ bibles.
+ """
+ smallest_index = 0
+ highest_index = len(html_list) - 1
+ index = highest_index // 2
+ while True:
+ if not self._text_fits_on_slide(previous_html + separator.join(html_list[:index + 1]).strip()):
+ # We know that it does not fit, so change/calculate the new index and highest_index accordingly.
+ highest_index = index
+ index = index - (index - smallest_index) // 2
+ else:
+ smallest_index = index
+ index = index + (highest_index - index) // 2
+ # We found the number of words which will fit.
+ if smallest_index == index or highest_index == index:
+ index = smallest_index
+ text = previous_raw.rstrip('<br>') + separator.join(raw_list[:index + 1])
+ text, raw_tags, html_tags = get_start_tags(text)
+ formatted.append(text)
+ previous_html = ''
+ previous_raw = ''
+ # Stop here as the theme line count was requested.
+ if self.force_page:
+ Registry().execute('theme_line_count', index + 1)
+ break
+ else:
+ continue
+ # Check if the remaining elements fit on the slide.
+ if self._text_fits_on_slide(html_tags + separator.join(html_list[index + 1:]).strip()):
+ previous_html = html_tags + separator.join(html_list[index + 1:]).strip() + line_end
+ previous_raw = raw_tags + separator.join(raw_list[index + 1:]).strip() + line_end
+ break
+ else:
+ # The remaining elements do not fit, thus reset the indexes, create a new list and continue.
+ raw_list = raw_list[index + 1:]
+ raw_list[0] = raw_tags + raw_list[0]
+ html_list = html_list[index + 1:]
+ html_list[0] = html_tags + html_list[0]
+ smallest_index = 0
+ highest_index = len(html_list) - 1
+ index = highest_index // 2
+ return previous_html, previous_raw
+
+ def _text_fits_on_slide(self, text):
+ """
+ Checks if the given ``text`` fits on a slide. If it does ``True`` is returned, otherwise ``False``.
+
+ :param text: The text to check. It may contain HTML tags.
+ """
+ self.web_frame.evaluateJavaScript('show_text("%s")' % text.replace('\\', '\\\\').replace('\"', '\\\"'))
+ return self.web_frame.contentsSize().height() <= self.empty_height
+
+
+def words_split(line):
+ """
+ Split the slide up by word so can wrap better
+
+ :param line: Line to be split
+ """
+ # this parse we are to be wordy
+ return re.split('\s+', line)
+
+
+def get_start_tags(raw_text):
+ """
+ Tests the given text for not closed formatting tags and returns a tuple consisting of three unicode strings::
+
+ ('{st}{r}Text text text{/r}{/st}', '{st}{r}', '<strong><span style="-webkit-text-fill-color:red">')
+
+ The first unicode string is the text, with correct closing tags. The second unicode string are OpenLP's opening
+ formatting tags and the third unicode string the html opening formatting tags.
+
+ :param raw_text: The text to test. The text must **not** contain html tags, only OpenLP formatting tags
+ are allowed::
+ {st}{r}Text text text
+ """
+ raw_tags = []
+ html_tags = []
+ for tag in FormattingTags.get_html_tags():
+ if tag['start tag'] == '{br}':
+ continue
+ if raw_text.count(tag['start tag']) != raw_text.count(tag['end tag']):
+ raw_tags.append((raw_text.find(tag['start tag']), tag['start tag'], tag['end tag']))
+ html_tags.append((raw_text.find(tag['start tag']), tag['start html']))
+ # Sort the lists, so that the tags which were opened first on the first slide (the text we are checking) will be
+ # opened first on the next slide as well.
+ raw_tags.sort(key=lambda tag: tag[0])
+ html_tags.sort(key=lambda tag: tag[0])
+ # Create a list with closing tags for the raw_text.
+ end_tags = []
+ start_tags = []
+ for tag in raw_tags:
+ start_tags.append(tag[1])
+ end_tags.append(tag[2])
+ end_tags.reverse()
+ # Remove the indexes.
+ html_tags = [tag[1] for tag in html_tags]
+ return raw_text + ''.join(end_tags), ''.join(start_tags), ''.join(html_tags)
=== added file 'openlp/core/lib/screen.py'
--- openlp/core/lib/screen.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/screen.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,260 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`screen` module provides management functionality for a machines'
+displays.
+"""
+
+import logging
+import copy
+
+from PyQt5 import QtCore
+
+from openlp.core.common import Registry, Settings, translate
+
+log = logging.getLogger(__name__)
+
+
+class ScreenList(object):
+ """
+ Wrapper to handle the parameters of the display screen.
+
+ To get access to the screen list call ``ScreenList()``.
+ """
+ log.info('Screen loaded')
+ __instance__ = None
+
+ def __new__(cls):
+ """
+ Re-implement __new__ to create a true singleton.
+ """
+ if not cls.__instance__:
+ cls.__instance__ = object.__new__(cls)
+ return cls.__instance__
+
+ @classmethod
+ def create(cls, desktop):
+ """
+ Initialise the screen list.
+
+ :param desktop: A QDesktopWidget object.
+ """
+ screen_list = cls()
+ screen_list.desktop = desktop
+ screen_list.preview = None
+ screen_list.current = None
+ screen_list.override = None
+ screen_list.screen_list = []
+ screen_list.display_count = 0
+ screen_list.screen_count_changed()
+ screen_list.load_screen_settings()
+ desktop.resized.connect(screen_list.screen_resolution_changed)
+ desktop.screenCountChanged.connect(screen_list.screen_count_changed)
+ return screen_list
+
+ def screen_resolution_changed(self, number):
+ """
+ Called when the resolution of a screen has changed.
+
+ ``number``
+ The number of the screen, which size has changed.
+ """
+ log.info('screen_resolution_changed %d' % number)
+ for screen in self.screen_list:
+ if number == screen['number']:
+ new_screen = {
+ 'number': number,
+ 'size': self.desktop.screenGeometry(number),
+ 'primary': self.desktop.primaryScreen() == number
+ }
+ self.remove_screen(number)
+ self.add_screen(new_screen)
+ # The screen's default size is used, that is why we have to
+ # update the override screen.
+ if screen == self.override:
+ self.override = copy.deepcopy(new_screen)
+ self.set_override_display()
+ Registry().execute('config_screen_changed')
+ break
+
+ def screen_count_changed(self, changed_screen=-1):
+ """
+ Called when a screen has been added or removed.
+
+ ``changed_screen``
+ The screen's number which has been (un)plugged.
+ """
+ # Do not log at start up.
+ if changed_screen != -1:
+ log.info('screen_count_changed %d' % self.desktop.screenCount())
+ # Remove unplugged screens.
+ for screen in copy.deepcopy(self.screen_list):
+ if screen['number'] == self.desktop.screenCount():
+ self.remove_screen(screen['number'])
+ # Add new screens.
+ for number in range(self.desktop.screenCount()):
+ if not self.screen_exists(number):
+ self.add_screen({
+ 'number': number,
+ 'size': self.desktop.screenGeometry(number),
+ 'primary': (self.desktop.primaryScreen() == number)
+ })
+ # We do not want to send this message at start up.
+ if changed_screen != -1:
+ # Reload setting tabs to apply possible changes.
+ Registry().execute('config_screen_changed')
+
+ def get_screen_list(self):
+ """
+ Returns a list with the screens. This should only be used to display
+ available screens to the user::
+
+ ['Screen 1 (primary)', 'Screen 2']
+ """
+ screen_list = []
+ for screen in self.screen_list:
+ screen_name = '%s %d' % (translate('OpenLP.ScreenList', 'Screen'), screen['number'] + 1)
+ if screen['primary']:
+ screen_name = '%s (%s)' % (screen_name, translate('OpenLP.ScreenList', 'primary'))
+ screen_list.append(screen_name)
+ return screen_list
+
+ def add_screen(self, screen):
+ """
+ Add a screen to the list of known screens.
+
+ :param screen: A dict with the screen properties:
+
+ ::
+
+ {
+ 'primary': True,
+ 'number': 0,
+ 'size': PyQt5.QtCore.QRect(0, 0, 1024, 768)
+ }
+ """
+ log.info('Screen %d found with resolution %s' % (screen['number'], screen['size']))
+ if screen['primary']:
+ self.current = screen
+ self.override = copy.deepcopy(self.current)
+ self.screen_list.append(screen)
+ self.display_count += 1
+
+ def remove_screen(self, number):
+ """
+ Remove a screen from the list of known screens.
+
+ :param number: The screen number (int).
+ """
+ log.info('remove_screen %d' % number)
+ for screen in self.screen_list:
+ if screen['number'] == number:
+ self.screen_list.remove(screen)
+ self.display_count -= 1
+ break
+
+ def screen_exists(self, number):
+ """
+ Confirms a screen is known.
+
+ :param number: The screen number (int).
+ """
+ for screen in self.screen_list:
+ if screen['number'] == number:
+ return True
+ return False
+
+ def set_current_display(self, number):
+ """
+ Set up the current screen dimensions.
+
+ :param number: The screen number (int).
+ """
+ log.debug('set_current_display %s' % number)
+ if number + 1 > self.display_count:
+ self.current = self.screen_list[0]
+ else:
+ self.current = self.screen_list[number]
+ self.preview = copy.deepcopy(self.current)
+ self.override = copy.deepcopy(self.current)
+ if self.display_count == 1:
+ self.preview = self.screen_list[0]
+
+ def set_override_display(self):
+ """
+ Replace the current size with the override values, as the user wants to have their own screen attributes.
+ """
+ log.debug('set_override_display')
+ self.current = copy.deepcopy(self.override)
+ self.preview = copy.deepcopy(self.current)
+
+ def reset_current_display(self):
+ """
+ Replace the current values with the correct values, as the user wants to use the correct screen attributes.
+ """
+ log.debug('reset_current_display')
+ self.set_current_display(self.current['number'])
+
+ def which_screen(self, window):
+ """
+ Return the screen number that the centre of the passed window is in.
+
+ :param window: A QWidget we are finding the location of.
+ """
+ x = window.x() + (window.width() // 2)
+ y = window.y() + (window.height() // 2)
+ for screen in self.screen_list:
+ size = screen['size']
+ if x >= size.x() and x <= (size.x() + size.width()) and y >= size.y() and y <= (size.y() + size.height()):
+ return screen['number']
+
+ def load_screen_settings(self):
+ """
+ Loads the screen size and the monitor number from the settings.
+ """
+ # Add the screen settings to the settings dict. This has to be done here due to cyclic dependency.
+ # Do not do this anywhere else.
+ screen_settings = {
+ 'core/x position': self.current['size'].x(),
+ 'core/y position': self.current['size'].y(),
+ 'core/monitor': self.display_count - 1,
+ 'core/height': self.current['size'].height(),
+ 'core/width': self.current['size'].width()
+ }
+ Settings.extend_default_settings(screen_settings)
+ settings = Settings()
+ settings.beginGroup('core')
+ monitor = settings.value('monitor')
+ self.set_current_display(monitor)
+ self.display = settings.value('display on monitor')
+ override_display = settings.value('override position')
+ x = settings.value('x position')
+ y = settings.value('y position')
+ width = settings.value('width')
+ height = settings.value('height')
+ self.override['size'] = QtCore.QRect(x, y, width, height)
+ self.override['primary'] = False
+ settings.endGroup()
+ if override_display:
+ self.set_override_display()
+ else:
+ self.reset_current_display()
=== added file 'openlp/core/lib/searchedit.py'
--- openlp/core/lib/searchedit.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/searchedit.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,179 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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
+
+from PyQt5 import QtCore, QtWidgets
+
+from openlp.core.lib import build_icon
+from openlp.core.lib.ui import create_widget_action
+
+log = logging.getLogger(__name__)
+
+
+class SearchEdit(QtWidgets.QLineEdit):
+ """
+ This is a specialised QLineEdit with a "clear" button inside for searches.
+ """
+ searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant)
+ cleared = QtCore.pyqtSignal()
+
+ def __init__(self, parent):
+ """
+ Constructor.
+ """
+ super(SearchEdit, self).__init__(parent)
+ self._current_search_type = -1
+ self.clear_button = QtWidgets.QToolButton(self)
+ self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png'))
+ self.clear_button.setCursor(QtCore.Qt.ArrowCursor)
+ self.clear_button.setStyleSheet('QToolButton { border: none; padding: 0px; }')
+ self.clear_button.resize(18, 18)
+ self.clear_button.hide()
+ self.clear_button.clicked.connect(self._on_clear_button_clicked)
+ self.textChanged.connect(self._on_search_edit_text_changed)
+ self._update_style_sheet()
+ self.setAcceptDrops(False)
+
+ def _update_style_sheet(self):
+ """
+ Internal method to update the stylesheet depending on which widgets are available and visible.
+ """
+ frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
+ right_padding = self.clear_button.width() + frame_width
+ if hasattr(self, 'menu_button'):
+ left_padding = self.menu_button.width()
+ stylesheet = 'QLineEdit { padding-left: %spx; padding-right: %spx; } ' % (left_padding, right_padding)
+ else:
+ stylesheet = 'QLineEdit { padding-right: %spx; } ' % right_padding
+ self.setStyleSheet(stylesheet)
+ msz = self.minimumSizeHint()
+ self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2),
+ max(msz.height(), self.clear_button.height() + (frame_width * 2) + 2))
+
+ def resizeEvent(self, event):
+ """
+ Reimplemented method to react to resizing of the widget.
+
+ :param event: The event that happened.
+ """
+ size = self.clear_button.size()
+ frame_width = self.style().pixelMetric(QtWidgets.QStyle.PM_DefaultFrameWidth)
+ self.clear_button.move(self.rect().right() - frame_width - size.width(),
+ (self.rect().bottom() + 1 - size.height()) // 2)
+ if hasattr(self, 'menu_button'):
+ size = self.menu_button.size()
+ self.menu_button.move(self.rect().left() + frame_width + 2, (self.rect().bottom() + 1 - size.height()) // 2)
+
+ def current_search_type(self):
+ """
+ Readonly property to return the current search type.
+ """
+ return self._current_search_type
+
+ def set_current_search_type(self, identifier):
+ """
+ Set a new current search type.
+
+ :param identifier: The search type identifier (int).
+ """
+ menu = self.menu_button.menu()
+ for action in menu.actions():
+ if identifier == action.data():
+ # setPlaceholderText has been implemented in Qt 4.7 and in at least PyQt 4.9 (I am not sure, if it was
+ # implemented in PyQt 4.8).
+ try:
+ self.setPlaceholderText(action.placeholder_text)
+ except AttributeError:
+ pass
+ self.menu_button.setDefaultAction(action)
+ self._current_search_type = identifier
+ self.searchTypeChanged.emit(identifier)
+ return True
+
+ def set_search_types(self, items):
+ """
+ A list of tuples to be used in the search type menu. The first item in the list will be preselected as the
+ default.
+
+ :param items: The list of tuples to use. The tuples should contain an integer identifier, an icon (QIcon
+ instance or string) and a title for the item in the menu. In short, they should look like this::
+
+ (<identifier>, <icon>, <title>, <place holder text>)
+
+ For instance::
+
+ (1, <QIcon instance>, "Titles", "Search Song Titles...")
+
+ Or::
+
+ (2, ":/songs/authors.png", "Authors", "Search Authors...")
+ """
+ menu = QtWidgets.QMenu(self)
+ first = None
+ for identifier, icon, title, placeholder in items:
+ action = create_widget_action(
+ menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered)
+ action.placeholder_text = placeholder
+ if first is None:
+ first = action
+ self._current_search_type = identifier
+ if not hasattr(self, 'menu_button'):
+ self.menu_button = QtWidgets.QToolButton(self)
+ self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png'))
+ self.menu_button.setCursor(QtCore.Qt.ArrowCursor)
+ self.menu_button.setPopupMode(QtWidgets.QToolButton.InstantPopup)
+ self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }')
+ self.menu_button.resize(QtCore.QSize(28, 18))
+ self.menu_button.setMenu(menu)
+ self.menu_button.setDefaultAction(first)
+ self.menu_button.show()
+ self._update_style_sheet()
+
+ def _on_search_edit_text_changed(self, text):
+ """
+ Internally implemented slot to react to when the text in the line edit has changed so that we can show or hide
+ the clear button.
+
+ :param text: A :class:`~PyQt5.QtCore.QString` instance which represents the text in the line edit.
+ """
+ self.clear_button.setVisible(bool(text))
+
+ def _on_clear_button_clicked(self):
+ """
+ Internally implemented slot to react to the clear button being clicked to clear the line edit. Once it has
+ cleared the line edit, it emits the ``cleared()`` signal so that an application can react to the clearing of the
+ line edit.
+ """
+ self.clear()
+ self.cleared.emit()
+
+ def _on_menu_action_triggered(self):
+ """
+ Internally implemented slot to react to the select of one of the search types in the menu. Once it has set the
+ correct action on the button, and set the current search type (using the list of identifiers provided by the
+ developer), the ``searchTypeChanged(int)`` signal is emitted with the identifier.
+ """
+ for action in self.menu_button.menu().actions():
+ # Why is this needed?
+ action.setChecked(False)
+ self.set_current_search_type(self.sender().data())
=== added file 'openlp/core/lib/serviceitem.py'
--- openlp/core/lib/serviceitem.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/serviceitem.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,672 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`serviceitem` provides the service item functionality including the
+type and capability of an item.
+"""
+
+import datetime
+import html
+import logging
+import os
+import uuid
+import ntpath
+
+from PyQt5 import QtGui
+
+from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
+from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb
+
+log = logging.getLogger(__name__)
+
+
+class ServiceItemType(object):
+ """
+ Defines the type of service item
+ """
+ Text = 1
+ Image = 2
+ Command = 3
+
+
+class ItemCapabilities(object):
+ """
+ Provides an enumeration of a service item's capabilities
+
+ ``CanPreview``
+ The capability to allow the ServiceManager to add to the preview tab when making the previous item live.
+
+ ``CanEdit``
+ The capability to allow the ServiceManager to allow the item to be edited
+
+ ``CanMaintain``
+ The capability to allow the ServiceManager to allow the item to be reordered.
+
+ ``RequiresMedia``
+ Determines is the service_item needs a Media Player
+
+ ``CanLoop``
+ The capability to allow the SlideController to allow the loop processing.
+
+ ``CanAppend``
+ The capability to allow the ServiceManager to add leaves to the
+ item
+
+ ``NoLineBreaks``
+ The capability to remove lines breaks in the renderer
+
+ ``OnLoadUpdate``
+ The capability to update MediaManager when a service Item is loaded.
+
+ ``AddIfNewItem``
+ Not Used
+
+ ``ProvidesOwnDisplay``
+ The capability to tell the SlideController the service Item has a different display.
+
+ ``HasDetailedTitleDisplay``
+ Being Removed and decommissioned.
+
+ ``HasVariableStartTime``
+ The capability to tell the ServiceManager that a change to start time is possible.
+
+ ``CanSoftBreak``
+ The capability to tell the renderer that Soft Break is allowed
+
+ ``CanWordSplit``
+ The capability to tell the renderer that it can split words is
+ allowed
+
+ ``HasBackgroundAudio``
+ That a audio file is present with the text.
+
+ ``CanAutoStartForLive``
+ The capability to ignore the do not play if display blank flag.
+
+ ``CanEditTitle``
+ The capability to edit the title of the item
+
+ ``IsOptical``
+ 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
+ CanEdit = 2
+ CanMaintain = 3
+ RequiresMedia = 4
+ CanLoop = 5
+ CanAppend = 6
+ NoLineBreaks = 7
+ OnLoadUpdate = 8
+ AddIfNewItem = 9
+ ProvidesOwnDisplay = 10
+ # HasDetailedTitleDisplay = 11
+ HasVariableStartTime = 12
+ CanSoftBreak = 13
+ CanWordSplit = 14
+ HasBackgroundAudio = 15
+ CanAutoStartForLive = 16
+ CanEditTitle = 17
+ IsOptical = 18
+ HasDisplayTitle = 19
+ HasNotes = 20
+ HasThumbnails = 21
+
+
+class ServiceItem(RegistryProperties):
+ """
+ The service item is a base class for the plugins to use to interact with
+ the service manager, the slide controller, and the projection screen
+ compositor.
+ """
+ log.info('Service Item created')
+
+ def __init__(self, plugin=None):
+ """
+ Set up the service item.
+
+ :param plugin: The plugin that this service item belongs to.
+ """
+ if plugin:
+ self.name = plugin.name
+ self.title = ''
+ self.processor = None
+ self.audit = ''
+ self.items = []
+ self.iconic_representation = None
+ self.raw_footer = []
+ self.foot_text = ''
+ self.theme = None
+ self.service_item_type = None
+ self._raw_frames = []
+ self._display_frames = []
+ self.unique_identifier = 0
+ self.notes = ''
+ self.from_plugin = False
+ self.capabilities = []
+ self.is_valid = True
+ self.icon = None
+ self.theme_data = None
+ self.main = None
+ self.footer = None
+ self.bg_image_bytes = None
+ self.search_string = ''
+ self.data_string = ''
+ self.edit_id = None
+ self.xml_version = None
+ self.start_time = 0
+ self.end_time = 0
+ self.media_length = 0
+ self.from_service = False
+ self.image_border = '#000000'
+ self.background_audio = []
+ self.theme_overwritten = False
+ self.temporary_edit = False
+ self.auto_play_slides_once = False
+ self.auto_play_slides_loop = False
+ self.timed_slide_interval = 0
+ self.will_auto_start = False
+ self.has_original_files = True
+ self._new_item()
+
+ def _new_item(self):
+ """
+ Method to set the internal id of the item. This is used to compare service items to see if they are the same.
+ """
+ self.unique_identifier = str(uuid.uuid1())
+ self.validate_item()
+
+ def add_capability(self, capability):
+ """
+ Add an ItemCapability to a ServiceItem
+
+ :param capability: The capability to add
+ """
+ self.capabilities.append(capability)
+
+ def is_capable(self, capability):
+ """
+ Tell the caller if a ServiceItem has a capability
+
+ :param capability: The capability to test for
+ """
+ return capability in self.capabilities
+
+ def add_icon(self, icon):
+ """
+ Add an icon to the service item. This is used when displaying the service item in the service manager.
+
+ :param icon: A string to an icon in the resources or on disk.
+ """
+ self.icon = icon
+ self.iconic_representation = build_icon(icon)
+
+ def render(self, provides_own_theme_data=False):
+ """
+ The render method is what generates the frames for the screen and obtains the display information from the
+ renderer. At this point all slides are built for the given display size.
+
+ :param provides_own_theme_data: This switch disables the usage of the item's theme. However, this is
+ disabled by default. If this is used, it has to be taken care, that
+ the renderer knows the correct theme data. However, this is needed
+ for the theme manager.
+ """
+ log.debug('Render called')
+ self._display_frames = []
+ self.bg_image_bytes = None
+ if not provides_own_theme_data:
+ self.renderer.set_item_theme(self.theme)
+ self.theme_data, self.main, self.footer = self.renderer.pre_render()
+ if self.service_item_type == ServiceItemType.Text:
+ log.debug('Formatting slides: %s' % self.title)
+ # Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
+ # the dict instead of rendering them again.
+ previous_pages = {}
+ for slide in self._raw_frames:
+ verse_tag = slide['verseTag']
+ if verse_tag in previous_pages and previous_pages[verse_tag][0] == slide['raw_slide']:
+ pages = previous_pages[verse_tag][1]
+ else:
+ pages = self.renderer.format_slide(slide['raw_slide'], self)
+ previous_pages[verse_tag] = (slide['raw_slide'], pages)
+ for page in pages:
+ page = page.replace('<br>', '{br}')
+ html_data = expand_tags(html.escape(page.rstrip()))
+ self._display_frames.append({
+ 'title': clean_tags(page),
+ 'text': clean_tags(page.rstrip()),
+ 'html': html_data.replace('&nbsp;', ' '),
+ 'verseTag': verse_tag
+ })
+ elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
+ pass
+ else:
+ log.error('Invalid value renderer: %s' % self.service_item_type)
+ self.title = clean_tags(self.title)
+ # The footer should never be None, but to be compatible with a few
+ # nightly builds between 1.9.4 and 1.9.5, we have to correct this to
+ # avoid tracebacks.
+ if self.raw_footer is None:
+ self.raw_footer = []
+ self.foot_text = '<br>'.join([_f for _f in self.raw_footer if _f])
+
+ 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
+ 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()
+
+ def add_from_text(self, raw_slide, verse_tag=None):
+ """
+ Add a text slide to the service item.
+
+ :param raw_slide: The raw text of the slide.
+ :param verse_tag:
+ """
+ if verse_tag:
+ verse_tag = verse_tag.upper()
+ self.service_item_type = ServiceItemType.Text
+ title = raw_slide[:30].split('\n')[0]
+ 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, 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
+ # 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)
+ # Update image path to match servicemanager location if file was loaded from service
+ if image and not self.has_original_files and self.name == 'presentations':
+ file_location = os.path.join(path, file_name)
+ file_location_hash = md5_hash(file_location.encode('utf-8'))
+ image = os.path.join(AppLocation.get_section_data_path(self.name), 'thumbnails',
+ file_location_hash, ntpath.basename(image))
+ 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):
+ """
+ This method returns some text which can be saved into the service file to represent this item.
+ """
+ service_header = {
+ 'name': self.name,
+ 'plugin': self.name,
+ 'theme': self.theme,
+ 'title': self.title,
+ 'icon': self.icon,
+ 'footer': self.raw_footer,
+ 'type': self.service_item_type,
+ 'audit': self.audit,
+ 'notes': self.notes,
+ 'from_plugin': self.from_plugin,
+ 'capabilities': self.capabilities,
+ 'search': self.search_string,
+ 'data': self.data_string,
+ 'xml_version': self.xml_version,
+ 'auto_play_slides_once': self.auto_play_slides_once,
+ 'auto_play_slides_loop': self.auto_play_slides_loop,
+ 'timed_slide_interval': self.timed_slide_interval,
+ 'start_time': self.start_time,
+ 'end_time': self.end_time,
+ 'media_length': self.media_length,
+ 'background_audio': self.background_audio,
+ 'theme_overwritten': self.theme_overwritten,
+ 'will_auto_start': self.will_auto_start,
+ 'processor': self.processor
+ }
+ service_data = []
+ if self.service_item_type == ServiceItemType.Text:
+ service_data = [slide for slide in self._raw_frames]
+ elif self.service_item_type == ServiceItemType.Image:
+ if lite_save:
+ for slide in self._raw_frames:
+ service_data.append({'title': slide['title'], 'path': slide['path']})
+ else:
+ 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'],
+ 'display_title': slide['display_title'], 'notes': slide['notes']})
+ return {'header': service_header, 'data': service_data}
+
+ def set_from_service(self, service_item, path=None):
+ """
+ This method takes a service item from a saved service file (passed from the ServiceManager) and extracts the
+ data actually required.
+
+ :param service_item: The item to extract data from.
+ :param path: Defaults to *None*. This is the service manager path for things which have their files saved
+ with them or None when the saved service is lite and the original file paths need to be preserved.
+ """
+ log.debug('set_from_service called with path %s' % path)
+ header = service_item['serviceitem']['header']
+ self.title = header['title']
+ self.name = header['name']
+ self.service_item_type = header['type']
+ self.theme = header['theme']
+ self.add_icon(header['icon'])
+ self.raw_footer = header['footer']
+ self.audit = header['audit']
+ self.notes = header['notes']
+ self.from_plugin = header['from_plugin']
+ self.capabilities = header['capabilities']
+ # Added later so may not be present in older services.
+ self.search_string = header.get('search', '')
+ self.data_string = header.get('data', '')
+ self.xml_version = header.get('xml_version')
+ self.start_time = header.get('start_time', 0)
+ self.end_time = header.get('end_time', 0)
+ self.media_length = header.get('media_length', 0)
+ self.auto_play_slides_once = header.get('auto_play_slides_once', False)
+ self.auto_play_slides_loop = header.get('auto_play_slides_loop', False)
+ self.timed_slide_interval = header.get('timed_slide_interval', 0)
+ self.will_auto_start = header.get('will_auto_start', False)
+ self.processor = header.get('processor', None)
+ self.has_original_files = True
+ if 'background_audio' in header:
+ self.background_audio = []
+ for filename in header['background_audio']:
+ # Give them real file paths.
+ filepath = filename
+ if path:
+ # Windows can handle both forward and backward slashes, so we use ntpath to get the basename
+ filepath = os.path.join(path, ntpath.basename(filename))
+ self.background_audio.append(filepath)
+ self.theme_overwritten = header.get('theme_overwritten', False)
+ if self.service_item_type == ServiceItemType.Text:
+ for slide in service_item['serviceitem']['data']:
+ self._raw_frames.append(slide)
+ elif self.service_item_type == ServiceItemType.Image:
+ settings_section = service_item['serviceitem']['header']['name']
+ background = QtGui.QColor(Settings().value(settings_section + '/background color'))
+ if path:
+ self.has_original_files = False
+ for text_image in service_item['serviceitem']['data']:
+ filename = os.path.join(path, text_image)
+ self.add_from_image(filename, text_image, background)
+ else:
+ for text_image in service_item['serviceitem']['data']:
+ self.add_from_image(text_image['path'], text_image['title'], background)
+ elif self.service_item_type == ServiceItemType.Command:
+ for text_image in service_item['serviceitem']['data']:
+ if not self.title:
+ self.title = text_image['title']
+ if self.is_capable(ItemCapabilities.IsOptical):
+ self.has_original_files = False
+ self.add_from_command(text_image['path'], text_image['title'], text_image['image'])
+ elif path:
+ self.has_original_files = False
+ self.add_from_command(path, text_image['title'], text_image['image'],
+ 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()
+
+ def get_display_title(self):
+ """
+ Returns the title of the service item.
+ """
+ if self.is_text() or self.is_capable(ItemCapabilities.IsOptical) \
+ or self.is_capable(ItemCapabilities.CanEditTitle):
+ return self.title
+ else:
+ if len(self._raw_frames) > 1:
+ return self.title
+ else:
+ return self._raw_frames[0]['title']
+
+ def merge(self, other):
+ """
+ Updates the unique_identifier with the value from the original one
+ The unique_identifier is unique for a given service item but this allows one to replace an original version.
+
+ :param other: The service item to be merged with
+ """
+ self.unique_identifier = other.unique_identifier
+ self.notes = other.notes
+ self.temporary_edit = other.temporary_edit
+ # Copy theme over if present.
+ if other.theme is not None:
+ self.theme = other.theme
+ self._new_item()
+ self.render()
+ if self.is_capable(ItemCapabilities.HasBackgroundAudio):
+ log.debug(self.background_audio)
+
+ def __eq__(self, other):
+ """
+ Confirms the service items are for the same instance
+ """
+ if not other:
+ return False
+ return self.unique_identifier == other.unique_identifier
+
+ def __ne__(self, other):
+ """
+ Confirms the service items are not for the same instance
+ """
+ return self.unique_identifier != other.unique_identifier
+
+ def __hash__(self):
+ """
+ Return the hash for the service item.
+ """
+ return self.unique_identifier
+
+ def is_media(self):
+ """
+ Confirms if the ServiceItem is media
+ """
+ return ItemCapabilities.RequiresMedia in self.capabilities
+
+ def is_command(self):
+ """
+ Confirms if the ServiceItem is a command
+ """
+ return self.service_item_type == ServiceItemType.Command
+
+ def is_image(self):
+ """
+ Confirms if the ServiceItem is an image
+ """
+ return self.service_item_type == ServiceItemType.Image
+
+ def uses_file(self):
+ """
+ Confirms if the ServiceItem uses a file
+ """
+ return self.service_item_type == ServiceItemType.Image or \
+ (self.service_item_type == ServiceItemType.Command and not self.is_capable(ItemCapabilities.IsOptical))
+
+ def is_text(self):
+ """
+ Confirms if the ServiceItem is text
+ """
+ return self.service_item_type == ServiceItemType.Text
+
+ def set_media_length(self, length):
+ """
+ Stores the media length of the item
+
+ :param length: The length of the media item
+ """
+ self.media_length = length
+ if length > 0:
+ self.add_capability(ItemCapabilities.HasVariableStartTime)
+
+ def get_frames(self):
+ """
+ Returns the frames for the ServiceItem
+ """
+ if self.service_item_type == ServiceItemType.Text:
+ return self._display_frames
+ else:
+ return self._raw_frames
+
+ def get_rendered_frame(self, row):
+ """
+ Returns the correct frame for a given list and renders it if required.
+
+ :param row: The service item slide to be returned
+ """
+ if self.service_item_type == ServiceItemType.Text:
+ return self._display_frames[row]['html'].split('\n')[0]
+ elif self.service_item_type == ServiceItemType.Image:
+ return self._raw_frames[row]['path']
+ else:
+ return self._raw_frames[row]['image']
+
+ def get_frame_title(self, row=0):
+ """
+ Returns the title of the raw frame
+ """
+ try:
+ return self._raw_frames[row]['title']
+ except IndexError:
+ return ''
+
+ def get_frame_path(self, row=0, frame=None):
+ """
+ Returns the path of the raw frame
+ """
+ if not frame:
+ try:
+ frame = self._raw_frames[row]
+ except IndexError:
+ return ''
+ if self.is_image() or self.is_capable(ItemCapabilities.IsOptical):
+ path_from = frame['path']
+ else:
+ path_from = os.path.join(frame['path'], frame['title'])
+ return path_from
+
+ def remove_frame(self, frame):
+ """
+ Remove the specified frame from the item
+ """
+ if frame in self._raw_frames:
+ self._raw_frames.remove(frame)
+
+ def get_media_time(self):
+ """
+ Returns the start and finish time for a media item
+ """
+ start = None
+ end = None
+ if self.start_time != 0:
+ start = translate('OpenLP.ServiceItem', '<strong>Start</strong>: %s') % \
+ str(datetime.timedelta(seconds=self.start_time))
+ if self.media_length != 0:
+ end = translate('OpenLP.ServiceItem', '<strong>Length</strong>: %s') % \
+ str(datetime.timedelta(seconds=self.media_length))
+ if not start and not end:
+ return ''
+ elif start and not end:
+ return start
+ elif not start and end:
+ return end
+ else:
+ return '%s <br>%s' % (start, end)
+
+ def update_theme(self, theme):
+ """
+ updates the theme in the service item
+
+ :param theme: The new theme to be replaced in the service item
+ """
+ self.theme_overwritten = (theme is None)
+ self.theme = theme
+ self._new_item()
+ self.render()
+
+ def remove_invalid_frames(self, invalid_paths=None):
+ """
+ Remove invalid frames, such as ones where the file no longer exists.
+ """
+ if self.uses_file():
+ for frame in self.get_frames():
+ if self.get_frame_path(frame=frame) in invalid_paths:
+ self.remove_frame(frame)
+
+ def missing_frames(self):
+ """
+ Returns if there are any frames in the service item
+ """
+ return not bool(self._raw_frames)
+
+ def validate_item(self, suffix_list=None):
+ """
+ Validates a service item to make sure it is valid
+ """
+ self.is_valid = True
+ for frame in self._raw_frames:
+ if self.is_image() and not os.path.exists(frame['path']):
+ self.is_valid = False
+ break
+ elif self.is_command():
+ if self.is_capable(ItemCapabilities.IsOptical):
+ if not os.path.exists(frame['title']):
+ self.is_valid = False
+ break
+ else:
+ file_name = os.path.join(frame['path'], frame['title'])
+ if not os.path.exists(file_name):
+ self.is_valid = False
+ break
+ if suffix_list and not self.is_text():
+ file_suffix = frame['title'].split('.')[-1]
+ if file_suffix.lower() not in suffix_list:
+ self.is_valid = False
+ break
=== added file 'openlp/core/lib/settingstab.py'
--- openlp/core/lib/settingstab.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/settingstab.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,138 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`~openlp.core.lib.settingstab` module contains the base SettingsTab class which plugins use for adding their
+own tab to the settings dialog.
+"""
+
+
+from PyQt5 import QtWidgets
+
+
+from openlp.core.common import RegistryProperties
+
+
+class SettingsTab(QtWidgets.QWidget, RegistryProperties):
+ """
+ SettingsTab is a helper widget for plugins to define Tabs for the settings dialog.
+ """
+ def __init__(self, parent, title, visible_title=None, icon_path=None):
+ """
+ Constructor to create the Settings tab item.
+
+ :param parent:
+ :param title: The title of the tab, which is used internally for the tab handling.
+ :param visible_title: The title of the tab, which is usually displayed on the tab.
+ :param icon_path:
+ """
+ super(SettingsTab, self).__init__(parent)
+ self.tab_title = title
+ self.tab_title_visible = visible_title
+ self.settings_section = self.tab_title.lower()
+ self.tab_visited = False
+ if icon_path:
+ self.icon_path = icon_path
+ self._setup()
+
+ def _setup(self):
+ """
+ Run some initial setup. This method is separate from __init__ in order to mock it out in tests.
+ """
+ self.setupUi()
+ self.retranslateUi()
+ self.initialise()
+ self.load()
+
+ def setupUi(self):
+ """
+ Setup the tab's interface.
+ """
+ self.tab_layout = QtWidgets.QHBoxLayout(self)
+ self.tab_layout.setObjectName('tab_layout')
+ self.left_column = QtWidgets.QWidget(self)
+ self.left_column.setObjectName('left_column')
+ self.left_layout = QtWidgets.QVBoxLayout(self.left_column)
+ self.left_layout.setContentsMargins(0, 0, 0, 0)
+ self.left_layout.setObjectName('left_layout')
+ self.tab_layout.addWidget(self.left_column)
+ self.right_column = QtWidgets.QWidget(self)
+ self.right_column.setObjectName('right_column')
+ self.right_layout = QtWidgets.QVBoxLayout(self.right_column)
+ self.right_layout.setContentsMargins(0, 0, 0, 0)
+ self.right_layout.setObjectName('right_layout')
+ self.tab_layout.addWidget(self.right_column)
+
+ def resizeEvent(self, event=None):
+ """
+ Resize the sides in two equal halves if the layout allows this.
+ """
+ if event:
+ QtWidgets.QWidget.resizeEvent(self, event)
+ width = self.width() - self.tab_layout.spacing() - \
+ self.tab_layout.contentsMargins().left() - self.tab_layout.contentsMargins().right()
+ left_width = min(width - self.right_column.minimumSizeHint().width(), width // 2)
+ left_width = max(left_width, self.left_column.minimumSizeHint().width())
+ self.left_column.setFixedWidth(left_width)
+
+ def retranslateUi(self):
+ """
+ Setup the interface translation strings.
+ """
+ pass
+
+ def initialise(self):
+ """
+ Do any extra initialisation here.
+ """
+ pass
+
+ def load(self):
+ """
+ Load settings from disk.
+ """
+ pass
+
+ def save(self):
+ """
+ Save settings to disk.
+ """
+ pass
+
+ def cancel(self):
+ """
+ Reset any settings if cancel triggered
+ """
+ self.load()
+
+ def post_set_up(self, post_update=False):
+ """
+ Changes which need to be made after setup of application
+
+ :param post_update: Indicates if called before or after updates.
+ """
+ pass
+
+ def tab_visible(self):
+ """
+ Tab has just been made visible to the user
+ """
+ self.tab_visited = True
=== added file 'openlp/core/lib/spelltextedit.py'
--- openlp/core/lib/spelltextedit.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/spelltextedit.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`~openlp.core.lib.spelltextedit` module contains a classes to add spell checking to an edit widget.
+"""
+
+import logging
+import re
+
+try:
+ import enchant
+ from enchant import DictNotFoundError
+ from enchant.errors import Error
+ ENCHANT_AVAILABLE = True
+except ImportError:
+ ENCHANT_AVAILABLE = False
+
+# based on code from http://john.nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.lib import translate, FormattingTags
+from openlp.core.lib.ui import create_action
+
+log = logging.getLogger(__name__)
+
+
+class SpellTextEdit(QtWidgets.QPlainTextEdit):
+ """
+ Spell checking widget based on QPlanTextEdit.
+ """
+ def __init__(self, parent=None, formatting_tags_allowed=True):
+ """
+ Constructor.
+ """
+ global ENCHANT_AVAILABLE
+ super(SpellTextEdit, self).__init__(parent)
+ self.formatting_tags_allowed = formatting_tags_allowed
+ # Default dictionary based on the current locale.
+ if ENCHANT_AVAILABLE:
+ try:
+ self.dictionary = enchant.Dict()
+ self.highlighter = Highlighter(self.document())
+ self.highlighter.spelling_dictionary = self.dictionary
+ except (Error, DictNotFoundError):
+ ENCHANT_AVAILABLE = False
+ log.debug('Could not load default dictionary')
+
+ def mousePressEvent(self, event):
+ """
+ Handle mouse clicks within the text edit region.
+ """
+ if event.button() == QtCore.Qt.RightButton:
+ # Rewrite the mouse event to a left button event so the cursor is moved to the location of the pointer.
+ event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress,
+ event.pos(), QtCore.Qt.LeftButton, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier)
+ QtWidgets.QPlainTextEdit.mousePressEvent(self, event)
+
+ def contextMenuEvent(self, event):
+ """
+ Provide the context menu for the text edit region.
+ """
+ popup_menu = self.createStandardContextMenu()
+ # Select the word under the cursor.
+ cursor = self.textCursor()
+ # only select text if not already selected
+ if not cursor.hasSelection():
+ cursor.select(QtGui.QTextCursor.WordUnderCursor)
+ self.setTextCursor(cursor)
+ # Add menu with available languages.
+ if ENCHANT_AVAILABLE:
+ lang_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Language:'))
+ for lang in enchant.list_languages():
+ action = create_action(lang_menu, lang, text=lang, checked=lang == self.dictionary.tag)
+ lang_menu.addAction(action)
+ popup_menu.insertSeparator(popup_menu.actions()[0])
+ popup_menu.insertMenu(popup_menu.actions()[0], lang_menu)
+ lang_menu.triggered.connect(self.set_language)
+ # Check if the selected word is misspelled and offer spelling suggestions if it is.
+ if ENCHANT_AVAILABLE and self.textCursor().hasSelection():
+ text = self.textCursor().selectedText()
+ if not self.dictionary.check(text):
+ spell_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Spelling Suggestions'))
+ for word in self.dictionary.suggest(text):
+ action = SpellAction(word, spell_menu)
+ action.correct.connect(self.correct_word)
+ spell_menu.addAction(action)
+ # Only add the spelling suggests to the menu if there are suggestions.
+ if spell_menu.actions():
+ popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
+ tag_menu = QtWidgets.QMenu(translate('OpenLP.SpellTextEdit', 'Formatting Tags'))
+ if self.formatting_tags_allowed:
+ for html in FormattingTags.get_html_tags():
+ action = SpellAction(html['desc'], tag_menu)
+ action.correct.connect(self.html_tag)
+ tag_menu.addAction(action)
+ popup_menu.insertSeparator(popup_menu.actions()[0])
+ popup_menu.insertMenu(popup_menu.actions()[0], tag_menu)
+ popup_menu.exec(event.globalPos())
+
+ def set_language(self, action):
+ """
+ Changes the language for this spelltextedit.
+
+ :param action: The action.
+ """
+ self.dictionary = enchant.Dict(action.text())
+ self.highlighter.spelling_dictionary = self.dictionary
+ self.highlighter.highlightBlock(self.toPlainText())
+ self.highlighter.rehighlight()
+
+ def correct_word(self, word):
+ """
+ Replaces the selected text with word.
+ """
+ cursor = self.textCursor()
+ cursor.beginEditBlock()
+ cursor.removeSelectedText()
+ cursor.insertText(word)
+ cursor.endEditBlock()
+
+ def html_tag(self, tag):
+ """
+ Replaces the selected text with word.
+ """
+ for html in FormattingTags.get_html_tags():
+ if tag == html['desc']:
+ cursor = self.textCursor()
+ if self.textCursor().hasSelection():
+ text = cursor.selectedText()
+ cursor.beginEditBlock()
+ cursor.removeSelectedText()
+ cursor.insertText(html['start tag'])
+ cursor.insertText(text)
+ cursor.insertText(html['end tag'])
+ cursor.endEditBlock()
+ else:
+ cursor = self.textCursor()
+ cursor.insertText(html['start tag'])
+ cursor.insertText(html['end tag'])
+
+
+class Highlighter(QtGui.QSyntaxHighlighter):
+ """
+ Provides a text highlighter for pointing out spelling errors in text.
+ """
+ WORDS = '(?iu)[\w\']+'
+
+ def __init__(self, *args):
+ """
+ Constructor
+ """
+ super(Highlighter, self).__init__(*args)
+ self.spelling_dictionary = None
+
+ def highlightBlock(self, text):
+ """
+ Highlight mis spelt words in a block of text.
+
+ Note, this is a Qt hook.
+ """
+ if not self.spelling_dictionary:
+ return
+ text = str(text)
+ char_format = QtGui.QTextCharFormat()
+ char_format.setUnderlineColor(QtCore.Qt.red)
+ char_format.setUnderlineStyle(QtGui.QTextCharFormat.SpellCheckUnderline)
+ for word_object in re.finditer(self.WORDS, text):
+ if not self.spelling_dictionary.check(word_object.group()):
+ self.setFormat(word_object.start(), word_object.end() - word_object.start(), char_format)
+
+
+class SpellAction(QtWidgets.QAction):
+ """
+ A special QAction that returns the text in a signal.
+ """
+ correct = QtCore.pyqtSignal(str)
+
+ def __init__(self, *args):
+ """
+ Constructor
+ """
+ super(SpellAction, self).__init__(*args)
+ self.triggered.connect(lambda x: self.correct.emit(self.text()))
=== added file 'openlp/core/lib/theme.py'
--- openlp/core/lib/theme.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/theme.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,559 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide the theme XML and handling functions for OpenLP v2 themes.
+"""
+import os
+import re
+import logging
+import json
+
+from xml.dom.minidom import Document
+from lxml import etree, objectify
+from openlp.core.common import AppLocation, de_hump
+
+from openlp.core.lib import str_to_bool, ScreenList, get_text_file_string
+
+log = logging.getLogger(__name__)
+
+
+class BackgroundType(object):
+ """
+ Type enumeration for backgrounds.
+ """
+ Solid = 0
+ Gradient = 1
+ Image = 2
+ Transparent = 3
+
+ @staticmethod
+ def to_string(background_type):
+ """
+ Return a string representation of a background type.
+ """
+ if background_type == BackgroundType.Solid:
+ return 'solid'
+ elif background_type == BackgroundType.Gradient:
+ return 'gradient'
+ elif background_type == BackgroundType.Image:
+ return 'image'
+ elif background_type == BackgroundType.Transparent:
+ return 'transparent'
+
+ @staticmethod
+ def from_string(type_string):
+ """
+ Return a background type for the given string.
+ """
+ if type_string == 'solid':
+ return BackgroundType.Solid
+ elif type_string == 'gradient':
+ return BackgroundType.Gradient
+ elif type_string == 'image':
+ return BackgroundType.Image
+ elif type_string == 'transparent':
+ return BackgroundType.Transparent
+
+
+class BackgroundGradientType(object):
+ """
+ Type enumeration for background gradients.
+ """
+ Horizontal = 0
+ Vertical = 1
+ Circular = 2
+ LeftTop = 3
+ LeftBottom = 4
+
+ @staticmethod
+ def to_string(gradient_type):
+ """
+ Return a string representation of a background gradient type.
+ """
+ if gradient_type == BackgroundGradientType.Horizontal:
+ return 'horizontal'
+ elif gradient_type == BackgroundGradientType.Vertical:
+ return 'vertical'
+ elif gradient_type == BackgroundGradientType.Circular:
+ return 'circular'
+ elif gradient_type == BackgroundGradientType.LeftTop:
+ return 'leftTop'
+ elif gradient_type == BackgroundGradientType.LeftBottom:
+ return 'leftBottom'
+
+ @staticmethod
+ def from_string(type_string):
+ """
+ Return a background gradient type for the given string.
+ """
+ if type_string == 'horizontal':
+ return BackgroundGradientType.Horizontal
+ elif type_string == 'vertical':
+ return BackgroundGradientType.Vertical
+ elif type_string == 'circular':
+ return BackgroundGradientType.Circular
+ elif type_string == 'leftTop':
+ return BackgroundGradientType.LeftTop
+ elif type_string == 'leftBottom':
+ return BackgroundGradientType.LeftBottom
+
+
+class HorizontalType(object):
+ """
+ Type enumeration for horizontal alignment.
+ """
+ Left = 0
+ Right = 1
+ Center = 2
+ Justify = 3
+
+ Names = ['left', 'right', 'center', 'justify']
+
+
+class VerticalType(object):
+ """
+ Type enumeration for vertical alignment.
+ """
+ Top = 0
+ Middle = 1
+ Bottom = 2
+
+ Names = ['top', 'middle', 'bottom']
+
+
+BOOLEAN_LIST = ['bold', 'italics', 'override', 'outline', 'shadow', 'slide_transition']
+
+INTEGER_LIST = ['size', 'line_adjustment', 'x', 'height', 'y', 'width', 'shadow_size', 'outline_size',
+ 'horizontal_align', 'vertical_align', 'wrap_style']
+
+
+class ThemeXML(object):
+ """
+ A class to encapsulate the Theme XML.
+ """
+ def __init__(self):
+ """
+ Initialise the theme object.
+ """
+ # basic theme object with defaults
+ json_dir = os.path.join(AppLocation.get_directory(AppLocation.AppDir), 'core', 'lib', 'json')
+ json_file = os.path.join(json_dir, 'theme.json')
+ jsn = get_text_file_string(json_file)
+ jsn = json.loads(jsn)
+ self.expand_json(jsn)
+
+ def expand_json(self, var, prev=None):
+ """
+ Expand the json objects and make into variables.
+
+ :param var: The array list to be processed.
+ :param prev: The preceding string to add to the key to make the variable.
+ """
+ for key, value in var.items():
+ if prev:
+ key = prev + "_" + key
+ else:
+ key = key
+ if isinstance(value, dict):
+ self.expand_json(value, key)
+ else:
+ setattr(self, key, value)
+
+ def extend_image_filename(self, path):
+ """
+ Add the path name to the image name so the background can be rendered.
+
+ :param path: The path name to be added.
+ """
+ if self.background_type == 'image':
+ if self.background_filename and path:
+ self.theme_name = self.theme_name.strip()
+ self.background_filename = self.background_filename.strip()
+ self.background_filename = os.path.join(path, self.theme_name, self.background_filename)
+
+ def _new_document(self, name):
+ """
+ Create a new theme XML document.
+ """
+ self.theme_xml = Document()
+ self.theme = self.theme_xml.createElement('theme')
+ self.theme_xml.appendChild(self.theme)
+ self.theme.setAttribute('version', '2.0')
+ self.name = self.theme_xml.createElement('name')
+ text_node = self.theme_xml.createTextNode(name)
+ self.name.appendChild(text_node)
+ self.theme.appendChild(self.name)
+
+ def add_background_transparent(self):
+ """
+ Add a transparent background.
+ """
+ background = self.theme_xml.createElement('background')
+ background.setAttribute('type', 'transparent')
+ self.theme.appendChild(background)
+
+ def add_background_solid(self, bkcolor):
+ """
+ Add a Solid background.
+
+ :param bkcolor: The color of the background.
+ """
+ background = self.theme_xml.createElement('background')
+ background.setAttribute('type', 'solid')
+ self.theme.appendChild(background)
+ self.child_element(background, 'color', str(bkcolor))
+
+ def add_background_gradient(self, startcolor, endcolor, direction):
+ """
+ Add a gradient background.
+
+ :param startcolor: The gradient's starting colour.
+ :param endcolor: The gradient's ending colour.
+ :param direction: The direction of the gradient.
+ """
+ background = self.theme_xml.createElement('background')
+ background.setAttribute('type', 'gradient')
+ self.theme.appendChild(background)
+ # Create startColor element
+ self.child_element(background, 'startColor', str(startcolor))
+ # Create endColor element
+ self.child_element(background, 'endColor', str(endcolor))
+ # Create direction element
+ self.child_element(background, 'direction', str(direction))
+
+ def add_background_image(self, filename, border_color):
+ """
+ Add a image background.
+
+ :param filename: The file name of the image.
+ :param border_color:
+ """
+ background = self.theme_xml.createElement('background')
+ background.setAttribute('type', 'image')
+ self.theme.appendChild(background)
+ # Create Filename element
+ self.child_element(background, 'filename', filename)
+ # Create endColor element
+ self.child_element(background, 'borderColor', str(border_color))
+
+ def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
+ line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
+ outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
+ """
+ Add a Font.
+
+ :param name: The name of the font.
+ :param color: The colour of the font.
+ :param size: The size of the font.
+ :param override: Whether or not to override the default positioning of the theme.
+ :param fonttype: The type of font, ``main`` or ``footer``. Defaults to ``main``.
+ :param bold:
+ :param italics: The weight of then font Defaults to 50 Normal
+ :param line_adjustment: Does the font render to italics Defaults to 0 Normal
+ :param xpos: The X position of the text block.
+ :param ypos: The Y position of the text block.
+ :param width: The width of the text block.
+ :param height: The height of the text block.
+ :param outline: Whether or not to show an outline.
+ :param outline_color: The colour of the outline.
+ :param outline_pixel: How big the Shadow is
+ :param shadow: Whether or not to show a shadow.
+ :param shadow_color: The colour of the shadow.
+ :param shadow_pixel: How big the Shadow is
+ """
+ background = self.theme_xml.createElement('font')
+ background.setAttribute('type', fonttype)
+ self.theme.appendChild(background)
+ # Create Font name element
+ self.child_element(background, 'name', name)
+ # Create Font color element
+ self.child_element(background, 'color', str(color))
+ # Create Proportion name element
+ self.child_element(background, 'size', str(size))
+ # Create weight name element
+ self.child_element(background, 'bold', str(bold))
+ # Create italics name element
+ self.child_element(background, 'italics', str(italics))
+ # Create indentation name element
+ self.child_element(background, 'line_adjustment', str(line_adjustment))
+ # Create Location element
+ element = self.theme_xml.createElement('location')
+ element.setAttribute('override', str(override))
+ element.setAttribute('x', str(xpos))
+ element.setAttribute('y', str(ypos))
+ element.setAttribute('width', str(width))
+ element.setAttribute('height', str(height))
+ background.appendChild(element)
+ # Shadow
+ element = self.theme_xml.createElement('shadow')
+ element.setAttribute('shadowColor', str(shadow_color))
+ element.setAttribute('shadowSize', str(shadow_pixel))
+ value = self.theme_xml.createTextNode(str(shadow))
+ element.appendChild(value)
+ background.appendChild(element)
+ # Outline
+ element = self.theme_xml.createElement('outline')
+ element.setAttribute('outlineColor', str(outline_color))
+ element.setAttribute('outlineSize', str(outline_pixel))
+ value = self.theme_xml.createTextNode(str(outline))
+ element.appendChild(value)
+ background.appendChild(element)
+
+ def add_display(self, horizontal, vertical, transition):
+ """
+ Add a Display options.
+
+ :param horizontal: The horizontal alignment of the text.
+ :param vertical: The vertical alignment of the text.
+ :param transition: Whether the slide transition is active.
+ """
+ background = self.theme_xml.createElement('display')
+ self.theme.appendChild(background)
+ # Horizontal alignment
+ element = self.theme_xml.createElement('horizontalAlign')
+ value = self.theme_xml.createTextNode(str(horizontal))
+ element.appendChild(value)
+ background.appendChild(element)
+ # Vertical alignment
+ element = self.theme_xml.createElement('verticalAlign')
+ value = self.theme_xml.createTextNode(str(vertical))
+ element.appendChild(value)
+ background.appendChild(element)
+ # Slide Transition
+ element = self.theme_xml.createElement('slideTransition')
+ value = self.theme_xml.createTextNode(str(transition))
+ element.appendChild(value)
+ background.appendChild(element)
+
+ def child_element(self, element, tag, value):
+ """
+ Generic child element creator.
+ """
+ child = self.theme_xml.createElement(tag)
+ child.appendChild(self.theme_xml.createTextNode(value))
+ element.appendChild(child)
+ return child
+
+ def set_default_header_footer(self):
+ """
+ Set the header and footer size into the current primary screen.
+ 10 px on each side is removed to allow for a border.
+ """
+ current_screen = ScreenList().current
+ self.font_main_y = 0
+ self.font_main_width = current_screen['size'].width() - 20
+ self.font_main_height = current_screen['size'].height() * 9 / 10
+ self.font_footer_width = current_screen['size'].width() - 20
+ self.font_footer_y = current_screen['size'].height() * 9 / 10
+ self.font_footer_height = current_screen['size'].height() / 10
+
+ def dump_xml(self):
+ """
+ Dump the XML to file used for debugging
+ """
+ return self.theme_xml.toprettyxml(indent=' ')
+
+ def extract_xml(self):
+ """
+ Print out the XML string.
+ """
+ self._build_xml_from_attrs()
+ return self.theme_xml.toxml('utf-8').decode('utf-8')
+
+ def extract_formatted_xml(self):
+ """
+ Pull out the XML string formatted for human consumption
+ """
+ self._build_xml_from_attrs()
+ return self.theme_xml.toprettyxml(indent=' ', newl='\n', encoding='utf-8')
+
+ def parse(self, xml):
+ """
+ Read in an XML string and parse it.
+
+ :param xml: The XML string to parse.
+ """
+ self.parse_xml(str(xml))
+
+ def parse_xml(self, xml):
+ """
+ Parse an XML string.
+
+ :param xml: The XML string to parse.
+ """
+ # remove encoding string
+ line = xml.find('?>')
+ if line:
+ xml = xml[line + 2:]
+ try:
+ theme_xml = objectify.fromstring(xml)
+ except etree.XMLSyntaxError:
+ log.exception('Invalid xml %s', xml)
+ return
+ xml_iter = theme_xml.getiterator()
+ for element in xml_iter:
+ master = ''
+ if element.tag == 'background':
+ if element.attrib:
+ for attr in element.attrib:
+ self._create_attr(element.tag, attr, element.attrib[attr])
+ parent = element.getparent()
+ if parent is not None:
+ if parent.tag == 'font':
+ master = parent.tag + '_' + parent.attrib['type']
+ # set up Outline and Shadow Tags and move to font_main
+ if parent.tag == 'display':
+ if element.tag.startswith('shadow') or element.tag.startswith('outline'):
+ self._create_attr('font_main', element.tag, element.text)
+ master = parent.tag
+ if parent.tag == 'background':
+ master = parent.tag
+ if master:
+ self._create_attr(master, element.tag, element.text)
+ if element.attrib:
+ for attr in element.attrib:
+ base_element = attr
+ # correction for the shadow and outline tags
+ if element.tag == 'shadow' or element.tag == 'outline':
+ if not attr.startswith(element.tag):
+ base_element = element.tag + '_' + attr
+ self._create_attr(master, base_element, element.attrib[attr])
+ else:
+ if element.tag == 'name':
+ self._create_attr('theme', element.tag, element.text)
+
+ def _translate_tags(self, master, element, value):
+ """
+ Clean up XML removing and redefining tags
+ """
+ master = master.strip().lstrip()
+ element = element.strip().lstrip()
+ value = str(value).strip().lstrip()
+ if master == 'display':
+ if element == 'wrapStyle':
+ return True, None, None, None
+ if element.startswith('shadow') or element.startswith('outline'):
+ master = 'font_main'
+ # fix bold font
+ if element == 'weight':
+ element = 'bold'
+ if value == 'Normal':
+ value = False
+ else:
+ value = True
+ if element == 'proportion':
+ element = 'size'
+ return False, master, element, value
+
+ def _create_attr(self, master, element, value):
+ """
+ Create the attributes with the correct data types and name format
+ """
+ reject, master, element, value = self._translate_tags(master, element, value)
+ if reject:
+ return
+ field = de_hump(element)
+ tag = master + '_' + field
+ if field in BOOLEAN_LIST:
+ setattr(self, tag, str_to_bool(value))
+ elif field in INTEGER_LIST:
+ setattr(self, tag, int(value))
+ else:
+ # make string value unicode
+ if not isinstance(value, str):
+ value = str(str(value), 'utf-8')
+ # None means an empty string so lets have one.
+ if value == 'None':
+ value = ''
+ setattr(self, tag, str(value).strip().lstrip())
+
+ def __str__(self):
+ """
+ Return a string representation of this object.
+ """
+ theme_strings = []
+ for key in dir(self):
+ if key[0:1] != '_':
+ theme_strings.append('%30s: %s' % (key, getattr(self, key)))
+ return '\n'.join(theme_strings)
+
+ def _build_xml_from_attrs(self):
+ """
+ Build the XML from the varables in the object
+ """
+ self._new_document(self.theme_name)
+ if self.background_type == BackgroundType.to_string(BackgroundType.Solid):
+ self.add_background_solid(self.background_color)
+ elif self.background_type == BackgroundType.to_string(BackgroundType.Gradient):
+ self.add_background_gradient(
+ self.background_start_color,
+ self.background_end_color,
+ self.background_direction
+ )
+ elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
+ filename = os.path.split(self.background_filename)[1]
+ self.add_background_image(filename, self.background_border_color)
+ elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
+ self.add_background_transparent()
+ self.add_font(
+ self.font_main_name,
+ self.font_main_color,
+ self.font_main_size,
+ self.font_main_override, 'main',
+ self.font_main_bold,
+ self.font_main_italics,
+ self.font_main_line_adjustment,
+ self.font_main_x,
+ self.font_main_y,
+ self.font_main_width,
+ self.font_main_height,
+ self.font_main_outline,
+ self.font_main_outline_color,
+ self.font_main_outline_size,
+ self.font_main_shadow,
+ self.font_main_shadow_color,
+ self.font_main_shadow_size
+ )
+ self.add_font(
+ self.font_footer_name,
+ self.font_footer_color,
+ self.font_footer_size,
+ self.font_footer_override, 'footer',
+ self.font_footer_bold,
+ self.font_footer_italics,
+ 0, # line adjustment
+ self.font_footer_x,
+ self.font_footer_y,
+ self.font_footer_width,
+ self.font_footer_height,
+ self.font_footer_outline,
+ self.font_footer_outline_color,
+ self.font_footer_outline_size,
+ self.font_footer_shadow,
+ self.font_footer_shadow_color,
+ self.font_footer_shadow_size
+ )
+ self.add_display(
+ self.display_horizontal_align,
+ self.display_vertical_align,
+ self.display_slide_transition
+ )
=== added file 'openlp/core/lib/toolbar.py'
--- openlp/core/lib/toolbar.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/toolbar.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Provide common toolbar handling for OpenLP
+"""
+import logging
+
+from PyQt5 import QtCore, QtWidgets
+
+from openlp.core.lib.ui import create_widget_action
+
+log = logging.getLogger(__name__)
+
+
+class OpenLPToolbar(QtWidgets.QToolBar):
+ """
+ Lots of toolbars around the place, so it makes sense to have a common way to manage them. This is the base toolbar
+ class.
+ """
+ def __init__(self, parent):
+ """
+ Initialise the toolbar.
+ """
+ super(OpenLPToolbar, self).__init__(parent)
+ # useful to be able to reuse button icons...
+ self.setIconSize(QtCore.QSize(20, 20))
+ self.actions = {}
+ log.debug('Init done for %s' % parent.__class__.__name__)
+
+ def add_toolbar_action(self, name, **kwargs):
+ """
+ A method to help developers easily add a button to the toolbar. A new QAction is created by calling
+ ``create_action()``. The action is added to the toolbar and the toolbar is set as parent. For more details
+ please look at openlp.core.lib.ui.create_action()
+ """
+ action = create_widget_action(self, name, **kwargs)
+ self.actions[name] = action
+ return action
+
+ def add_toolbar_widget(self, widget):
+ """
+ Add a widget and store it's handle under the widgets object name.
+ """
+ action = self.addWidget(widget)
+ self.actions[widget.objectName()] = action
+
+ def set_widget_visible(self, widgets, visible=True):
+ """
+ Set the visibility for a widget or a list of widgets.
+
+ :param widgets: A list of string with widget object names.
+ :param visible: The new state as bool.
+ """
+ for handle in widgets:
+ if handle in self.actions:
+ self.actions[handle].setVisible(visible)
+ else:
+ log.warning('No handle "%s" in actions list.', str(handle))
+
+ def set_widget_enabled(self, widgets, enabled=True):
+ """
+ Set the enabled state for a widget or a list of widgets.
+
+ :param widgets: A list of string with widget object names.
+ :param enabled: The new state as bool.
+ """
+ for handle in widgets:
+ if handle in self.actions:
+ self.actions[handle].setEnabled(enabled)
+ else:
+ log.warning('No handle "%s" in actions list.', str(handle))
=== added file 'openlp/core/lib/treewidgetwithdnd.py'
--- openlp/core/lib/treewidgetwithdnd.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/treewidgetwithdnd.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+Extend QTreeWidget to handle drag and drop functionality
+"""
+import os
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry
+
+
+class TreeWidgetWithDnD(QtWidgets.QTreeWidget):
+ """
+ Provide a tree widget to store objects and handle drag and drop events
+ """
+ def __init__(self, parent=None, name=''):
+ """
+ Initialise the tree widget
+ """
+ super(TreeWidgetWithDnD, self).__init__(parent)
+ self.mime_data_text = name
+ self.allow_internal_dnd = False
+ self.header().close()
+ self.default_indentation = self.indentation()
+ self.setIndentation(0)
+ self.setAnimated(True)
+
+ def activateDnD(self):
+ """
+ Activate DnD of widget
+ """
+ self.setAcceptDrops(True)
+ self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop)
+ Registry().register_function(('%s_dnd' % self.mime_data_text), self.parent().load_file)
+ Registry().register_function(('%s_dnd_internal' % self.mime_data_text), self.parent().dnd_move_internal)
+
+ def mouseMoveEvent(self, event):
+ """
+ Drag and drop event does not care what data is selected as the recipient will use events to request the data
+ move just tell it what plugin to call
+
+ :param event: The event that occurred
+ """
+ if event.buttons() != QtCore.Qt.LeftButton:
+ event.ignore()
+ return
+ if not self.selectedItems():
+ event.ignore()
+ return
+ drag = QtGui.QDrag(self)
+ mime_data = QtCore.QMimeData()
+ drag.setMimeData(mime_data)
+ mime_data.setText(self.mime_data_text)
+ drag.exec(QtCore.Qt.CopyAction)
+
+ def dragEnterEvent(self, event):
+ """
+ Receive drag enter event, check if it is a file or internal object and allow it if it is.
+
+ :param event: The event that occurred
+ """
+ if event.mimeData().hasUrls():
+ event.accept()
+ elif self.allow_internal_dnd:
+ event.accept()
+ else:
+ event.ignore()
+
+ def dragMoveEvent(self, event):
+ """
+ Receive drag move event, check if it is a file or internal object and allow it if it is.
+
+ :param event: The event that occurred
+ """
+ QtWidgets.QTreeWidget.dragMoveEvent(self, event)
+ if event.mimeData().hasUrls():
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ elif self.allow_internal_dnd:
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ else:
+ event.ignore()
+
+ def dropEvent(self, event):
+ """
+ Receive drop event, check if it is a file or internal object and process it if it is.
+
+ :param event: Handle of the event pint passed
+ """
+ if event.mimeData().hasUrls():
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ files = []
+ for url in event.mimeData().urls():
+ local_file = url.toLocalFile()
+ if os.path.isfile(local_file):
+ files.append(local_file)
+ elif os.path.isdir(local_file):
+ listing = os.listdir(local_file)
+ for file_name in listing:
+ files.append(os.path.join(local_file, file_name))
+ Registry().execute('%s_dnd' % self.mime_data_text, {'files': files, 'target': self.itemAt(event.pos())})
+ elif self.allow_internal_dnd:
+ event.setDropAction(QtCore.Qt.CopyAction)
+ event.accept()
+ Registry().execute('%s_dnd_internal' % self.mime_data_text, self.itemAt(event.pos()))
+ else:
+ event.ignore()
+
+ # Convenience methods for emulating a QListWidget. This helps keeping MediaManagerItem simple.
+ def addItem(self, item):
+ self.addTopLevelItem(item)
+
+ def count(self):
+ return self.topLevelItemCount()
+
+ def item(self, index):
+ return self.topLevelItem(index)
=== added file 'openlp/core/lib/ui.py'
--- openlp/core/lib/ui.py 1970-01-01 00:00:00 +0000
+++ openlp/core/lib/ui.py 2016-04-05 20:22:40 +0000
@@ -0,0 +1,326 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2016 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 #
+###############################################################################
+"""
+The :mod:`ui` module provides standard UI components for OpenLP.
+"""
+import logging
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+from openlp.core.common import Registry, UiStrings, translate, is_macosx
+from openlp.core.common.actions import ActionList
+from openlp.core.lib import build_icon
+
+
+log = logging.getLogger(__name__)
+
+
+def add_welcome_page(parent, image):
+ """
+ Generate an opening welcome page for a wizard using a provided image.
+
+ :param parent: A ``QWizard`` object to add the welcome page to.
+ :param image: A splash image for the wizard.
+ """
+ parent.welcome_page = QtWidgets.QWizardPage()
+ parent.welcome_page.setPixmap(QtWidgets.QWizard.WatermarkPixmap, QtGui.QPixmap(image))
+ parent.welcome_page.setObjectName('welcome_page')
+ parent.welcome_layout = QtWidgets.QVBoxLayout(parent.welcome_page)
+ parent.welcome_layout.setObjectName('WelcomeLayout')
+ parent.title_label = QtWidgets.QLabel(parent.welcome_page)
+ parent.title_label.setObjectName('title_label')
+ parent.welcome_layout.addWidget(parent.title_label)
+ parent.welcome_layout.addSpacing(40)
+ parent.information_label = QtWidgets.QLabel(parent.welcome_page)
+ parent.information_label.setWordWrap(True)
+ parent.information_label.setObjectName('information_label')
+ parent.welcome_layout.addWidget(parent.information_label)
+ parent.welcome_layout.addStretch()
+ parent.addPage(parent.welcome_page)
+
+
+def create_button_box(dialog, name, standard_buttons, custom_buttons=None):
+ """
+ Creates a QDialogButtonBox with the given buttons. The ``accepted()`` and ``rejected()`` signals of the button box
+ are connected with the dialogs ``accept()`` and ``reject()`` slots.
+
+ :param dialog: The parent object. This has to be a ``QDialog`` descendant.
+ :param name: A string which is set as object name.
+ :param standard_buttons: A list of strings for the used buttons. It might contain: ``ok``, ``save``, ``cancel``,
+ ``close``, and ``defaults``.
+ :param custom_buttons: A list of additional buttons. If an item is an instance of QtWidgets.QAbstractButton it is
+ added with QDialogButtonBox.ActionRole. Otherwise the item has to be a tuple of a Button and a ButtonRole.
+ """
+ if custom_buttons is None:
+ custom_buttons = []
+ if standard_buttons is None:
+ standard_buttons = []
+ buttons = QtWidgets.QDialogButtonBox.NoButton
+ if 'ok' in standard_buttons:
+ buttons |= QtWidgets.QDialogButtonBox.Ok
+ if 'save' in standard_buttons:
+ buttons |= QtWidgets.QDialogButtonBox.Save
+ if 'cancel' in standard_buttons:
+ buttons |= QtWidgets.QDialogButtonBox.Cancel
+ if 'close' in standard_buttons:
+ buttons |= QtWidgets.QDialogButtonBox.Close
+ if 'defaults' in standard_buttons:
+ buttons |= QtWidgets.QDialogButtonBox.RestoreDefaults
+ button_box = QtWidgets.QDialogButtonBox(dialog)
+ button_box.setObjectName(name)
+ button_box.setStandardButtons(buttons)
+ for button in custom_buttons:
+ if isinstance(button, QtWidgets.QAbstractButton):
+ button_box.addButton(button, QtWidgets.QDialogButtonBox.ActionRole)
+ else:
+ button_box.addButton(*button)
+ button_box.accepted.connect(dialog.accept)
+ button_box.rejected.connect(dialog.reject)
+ return button_box
+
+
+def critical_error_message_box(title=None, message=None, parent=None, question=False):
+ """
+ Provides a standard critical message box for errors that OpenLP displays to users.
+
+ :param title: The title for the message box.
+ :param message: The message to display to the user.
+ :param parent: The parent UI element to attach the dialog to.
+ :param question: Should this message box question the user.
+ """
+ if question:
+ return QtWidgets.QMessageBox.critical(parent, UiStrings().Error, message,
+ QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
+ QtWidgets.QMessageBox.No))
+ return Registry().get('main_window').error_message(title if title else UiStrings().Error, message)
+
+
+def create_horizontal_adjusting_combo_box(parent, name):
+ """
+ Creates a QComboBox with adapting width for media items.
+
+ :param parent: The parent widget.
+ :param name: A string set as object name for the combo box.
+ """
+ combo = QtWidgets.QComboBox(parent)
+ combo.setObjectName(name)
+ combo.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToMinimumContentsLength)
+ combo.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
+ return combo
+
+
+def create_button(parent, name, **kwargs):
+ """
+ Return an button with the object name set and the given parameters.
+
+ :param parent: A QtCore.QWidget for the buttons parent (required).
+ :param name: A string which is set as object name (required).
+ :param kwargs:
+
+ ``role``
+ A string which can have one value out of ``delete``, ``up``, and ``down``. This decides about default values
+ for properties like text, icon, or tooltip.
+
+ ``text``
+ A string for the action text.
+
+ ``icon``
+ Either a QIcon, a resource string, or a file location string for the action icon.
+
+ ``tooltip``
+ A string for the action tool tip.
+
+ ``enabled``
+ False in case the button should be disabled.
+
+ """
+ if 'role' in kwargs:
+ role = kwargs.pop('role')
+ if role == 'delete':
+ kwargs.setdefault('text', UiStrings().Delete)
+ kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Delete the selected item.'))
+ elif role == 'up':
+ kwargs.setdefault('icon', ':/services/service_up.png')
+ kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection up one position.'))
+ elif role == 'down':
+ kwargs.setdefault('icon', ':/services/service_down.png')
+ kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.'))
+ else:
+ log.warning('The role "%s" is not defined in create_push_button().', role)
+ if kwargs.pop('btn_class', '') == 'toolbutton':
+ button = QtWidgets.QToolButton(parent)
+ else:
+ button = QtWidgets.QPushButton(parent)
+ button.setObjectName(name)
+ if kwargs.get('text'):
+ button.setText(kwargs.pop('text'))
+ if kwargs.get('icon'):
+ button.setIcon(build_icon(kwargs.pop('icon')))
+ if kwargs.get('tooltip'):
+ button.setToolTip(kwargs.pop('tooltip'))
+ if not kwargs.pop('enabled', True):
+ button.setEnabled(False)
+ if kwargs.get('click'):
+ button.clicked.connect(kwargs.pop('click'))
+ for key in list(kwargs.keys()):
+ if key not in ['text', 'icon', 'tooltip', 'click']:
+ log.warning('Parameter %s was not consumed in create_button().', key)
+ return button
+
+
+def create_action(parent, name, **kwargs):
+ """
+ Return an action with the object name set and the given parameters.
+
+ :param parent: A QtCore.QObject for the actions parent (required).
+ :param name: A string which is set as object name (required).
+ :param kwargs:
+
+ ``text``
+ A string for the action text.
+
+ ``icon``
+ Either a QIcon, a resource string, or a file location string for the
+ action icon.
+
+ ``tooltip``
+ A string for the action tool tip.
+
+ ``statustip``
+ A string for the action status tip.
+
+ ``checked``
+ A bool for the state. If ``None`` the Action is not checkable.
+
+ ``enabled``
+ False in case the action should be disabled.
+
+ ``visible``
+ False in case the action should be hidden.
+
+ ``separator``
+ True in case the action will be considered a separator.
+
+ ``data``
+ The action's data.
+
+ ``can_shortcuts``
+ Capability stating if this action can have shortcuts. If ``True`` the action is added to shortcut dialog
+
+ otherwise it it not. Define your shortcut in the :class:`~openlp.core.lib.Settings` class. *Note*: When *not*
+ ``True`` you *must not* set a shortcuts at all.
+
+ ``context``
+ A context for the shortcut execution.
+
+ ``category``
+ A category the action should be listed in the shortcut dialog.
+
+ ``triggers``
+ A slot which is connected to the actions ``triggered()`` slot.
+ """
+ action = QtWidgets.QAction(parent)
+ action.setObjectName(name)
+ if is_macosx():
+ action.setIconVisibleInMenu(False)
+ if kwargs.get('text'):
+ action.setText(kwargs.pop('text'))
+ if kwargs.get('icon'):
+ action.setIcon(