Merge lp:~suutari-olli/openlp/fix-openlp-importer into lp:openlp/2.4

Proposed by Azaziah
Status: Superseded
Proposed branch: lp:~suutari-olli/openlp/fix-openlp-importer
Merge into: lp:openlp/2.4
Diff against target: 114285 lines (+91012/-6647) (has conflicts)
394 files modified
.bzrignore (+6/-1)
.coveragerc (+0/-5)
nose2.cfg (+22/-0)
openlp/.version (+4/-0)
openlp/core/__init__.py (+61/-23)
openlp/core/common/__init__.py (+218/-17)
openlp/core/common/actions.py (+390/-0)
openlp/core/common/db.py (+72/-0)
openlp/core/common/httputils.py (+255/-0)
openlp/core/common/languagemanager.py (+207/-0)
openlp/core/common/languages.py (+201/-0)
openlp/core/common/openlpmixin.py (+9/-2)
openlp/core/common/registry.py (+39/-8)
openlp/core/common/settings.py (+25/-13)
openlp/core/common/uistrings.py (+40/-5)
openlp/core/common/versionchecker.py (+173/-0)
openlp/core/lib/__init__.py (+54/-43)
openlp/core/lib/db.py (+40/-37)
openlp/core/lib/exceptions.py (+7/-1)
openlp/core/lib/filedialog.py (+2/-2)
openlp/core/lib/htmlbuilder.py (+249/-235)
openlp/core/lib/imagemanager.py (+4/-4)
openlp/core/lib/mediamanageritem.py (+24/-31)
openlp/core/lib/plugin.py (+6/-7)
openlp/core/lib/pluginmanager.py (+14/-12)
openlp/core/lib/projector/constants.py (+233/-153)
openlp/core/lib/projector/db.py (+63/-45)
openlp/core/lib/projector/pjlink1.py (+101/-33)
openlp/core/lib/renderer.py (+19/-13)
openlp/core/lib/screen.py (+9/-7)
openlp/core/lib/searchedit.py (+11/-15)
openlp/core/lib/serviceitem.py (+15/-10)
openlp/core/lib/settingstab.py (+1/-1)
openlp/core/lib/theme.py (+32/-7)
openlp/core/lib/ui.py (+4/-4)
openlp/core/resources.py (+50027/-2)
openlp/core/ui/__init__.py (+1/-3)
openlp/core/ui/aboutdialog.py (+116/-97)
openlp/core/ui/aboutform.py (+2/-2)
openlp/core/ui/advancedtab.py (+84/-103)
openlp/core/ui/exceptiondialog.py (+19/-9)
openlp/core/ui/exceptionform.py (+49/-34)
openlp/core/ui/filerenamedialog.py (+1/-1)
openlp/core/ui/firsttimeform.py (+59/-115)
openlp/core/ui/firsttimelanguagedialog.py (+1/-1)
openlp/core/ui/firsttimelanguageform.py (+1/-1)
openlp/core/ui/firsttimewizard.py (+52/-22)
openlp/core/ui/formattingtagcontroller.py (+13/-10)
openlp/core/ui/formattingtagdialog.py (+1/-1)
openlp/core/ui/formattingtagform.py (+4/-3)
openlp/core/ui/generaltab.py (+88/-6)
openlp/core/ui/lib/__init__.py (+34/-0)
openlp/core/ui/lib/colorbutton.py (+82/-0)
openlp/core/ui/lib/dockwidget.py (+56/-0)
openlp/core/ui/lib/historycombobox.py (+84/-0)
openlp/core/ui/lib/listpreviewwidget.py (+239/-0)
openlp/core/ui/lib/listwidgetwithdnd.py (+139/-0)
openlp/core/ui/lib/mediadockmanager.py (+71/-0)
openlp/core/ui/lib/spelltextedit.py (+204/-0)
openlp/core/ui/lib/toolbar.py (+90/-0)
openlp/core/ui/lib/treewidgetwithdnd.py (+144/-0)
openlp/core/ui/lib/wizard.py (+311/-0)
openlp/core/ui/maindisplay.py (+44/-27)
openlp/core/ui/mainwindow.py (+152/-124)
openlp/core/ui/media/__init__.py (+16/-10)
openlp/core/ui/media/mediacontroller.py (+198/-108)
openlp/core/ui/media/mediaplayer.py (+69/-2)
openlp/core/ui/media/playertab.py (+10/-3)
openlp/core/ui/media/systemplayer.py (+76/-24)
openlp/core/ui/media/vendor/mediainfoWrapper.py (+140/-0)
openlp/core/ui/media/vlcplayer.py (+57/-21)
openlp/core/ui/media/webkitplayer.py (+62/-145)
openlp/core/ui/plugindialog.py (+6/-14)
openlp/core/ui/pluginform.py (+28/-28)
openlp/core/ui/printservicedialog.py (+3/-2)
openlp/core/ui/projector/editform.py (+35/-36)
openlp/core/ui/projector/manager.py (+128/-90)
openlp/core/ui/projector/sourceselectform.py (+65/-2)
openlp/core/ui/projector/tab.py (+1/-1)
openlp/core/ui/serviceitemeditdialog.py (+1/-1)
openlp/core/ui/servicemanager.py (+69/-37)
openlp/core/ui/servicenoteform.py (+1/-1)
openlp/core/ui/settingsdialog.py (+1/-1)
openlp/core/ui/settingsform.py (+5/-2)
openlp/core/ui/shortcutlistdialog.py (+1/-1)
openlp/core/ui/shortcutlistform.py (+6/-5)
openlp/core/ui/slidecontroller.py (+143/-80)
openlp/core/ui/starttimedialog.py (+1/-1)
openlp/core/ui/starttimeform.py (+3/-3)
openlp/core/ui/themeform.py (+52/-10)
openlp/core/ui/themelayoutdialog.py (+1/-1)
openlp/core/ui/thememanager.py (+38/-30)
openlp/core/ui/themewizard.py (+41/-15)
openlp/plugins/alerts/alertsplugin.py (+10/-7)
openlp/plugins/alerts/forms/alertdialog.py (+1/-1)
openlp/plugins/alerts/lib/alertsmanager.py (+1/-1)
openlp/plugins/alerts/lib/alertstab.py (+7/-5)
openlp/plugins/bibles/bibleplugin.py (+10/-49)
openlp/plugins/bibles/forms/__init__.py (+3/-21)
openlp/plugins/bibles/forms/bibleimportform.py (+261/-23)
openlp/plugins/bibles/forms/booknamedialog.py (+1/-1)
openlp/plugins/bibles/forms/editbibledialog.py (+7/-4)
openlp/plugins/bibles/forms/editbibleform.py (+8/-7)
openlp/plugins/bibles/forms/languagedialog.py (+1/-1)
openlp/plugins/bibles/forms/languageform.py (+5/-5)
openlp/plugins/bibles/lib/__init__.py (+10/-8)
openlp/plugins/bibles/lib/bibleimport.py (+234/-0)
openlp/plugins/bibles/lib/biblestab.py (+63/-5)
openlp/plugins/bibles/lib/db.py (+37/-263)
openlp/plugins/bibles/lib/importers/csvbible.py (+102/-103)
openlp/plugins/bibles/lib/importers/http.py (+62/-40)
openlp/plugins/bibles/lib/importers/opensong.py (+119/-121)
openlp/plugins/bibles/lib/importers/osis.py (+141/-165)
openlp/plugins/bibles/lib/importers/sword.py (+102/-0)
openlp/plugins/bibles/lib/importers/wordproject.py (+169/-0)
openlp/plugins/bibles/lib/importers/zefania.py (+29/-42)
openlp/plugins/bibles/lib/manager.py (+108/-80)
openlp/plugins/bibles/lib/mediaitem.py (+354/-79)
openlp/plugins/bibles/lib/upgrade.py (+2/-164)
openlp/plugins/bibles/lib/versereferencelist.py (+14/-8)
openlp/plugins/custom/forms/editcustomdialog.py (+1/-1)
openlp/plugins/custom/forms/editcustomslidedialog.py (+3/-2)
openlp/plugins/custom/lib/customxmlhandler.py (+1/-1)
openlp/plugins/custom/lib/db.py (+1/-1)
openlp/plugins/custom/lib/mediaitem.py (+5/-9)
openlp/plugins/images/imageplugin.py (+1/-1)
openlp/plugins/images/lib/imagetab.py (+3/-2)
openlp/plugins/images/lib/mediaitem.py (+26/-23)
openlp/plugins/media/forms/mediaclipselectorform.py (+21/-12)
openlp/plugins/media/lib/mediaitem.py (+48/-40)
openlp/plugins/media/lib/mediatab.py (+1/-1)
openlp/plugins/media/mediaplugin.py (+35/-9)
openlp/plugins/presentations/lib/impresscontroller.py (+6/-6)
openlp/plugins/presentations/lib/mediaitem.py (+13/-10)
openlp/plugins/presentations/lib/messagelistener.py (+23/-19)
openlp/plugins/presentations/lib/pdfcontroller.py (+50/-32)
openlp/plugins/presentations/lib/powerpointcontroller.py (+19/-10)
openlp/plugins/presentations/lib/pptviewcontroller.py (+4/-5)
openlp/plugins/presentations/lib/presentationcontroller.py (+6/-5)
openlp/plugins/presentations/lib/presentationtab.py (+7/-6)
openlp/plugins/presentations/presentationplugin.py (+3/-3)
openlp/plugins/remotes/html/assets/jquery.js (+9404/-0)
openlp/plugins/remotes/html/assets/jquery.min.js (+4/-0)
openlp/plugins/remotes/html/assets/jquery.mobile.js (+9357/-0)
openlp/plugins/remotes/html/assets/jquery.mobile.min.css (+2/-0)
openlp/plugins/remotes/html/assets/jquery.mobile.min.js (+2/-0)
openlp/plugins/remotes/html/css/main.css (+32/-0)
openlp/plugins/remotes/html/css/openlp.css (+31/-0)
openlp/plugins/remotes/html/css/stage.css (+64/-0)
openlp/plugins/remotes/html/index.html (+17/-0)
openlp/plugins/remotes/html/js/main.js (+45/-0)
openlp/plugins/remotes/html/js/openlp.js (+384/-0)
openlp/plugins/remotes/html/js/stage.js (+170/-0)
openlp/plugins/remotes/html/main.html (+4/-4)
openlp/plugins/remotes/html/stage.html (+4/-4)
openlp/plugins/remotes/lib/httprouter.py (+33/-24)
openlp/plugins/remotes/lib/httpserver.py (+5/-3)
openlp/plugins/remotes/lib/remotetab.py (+20/-8)
openlp/plugins/songs/forms/authorsdialog.py (+1/-1)
openlp/plugins/songs/forms/duplicatesongremovalform.py (+4/-3)
openlp/plugins/songs/forms/editsongdialog.py (+1/-1)
openlp/plugins/songs/forms/editsongform.py (+51/-31)
openlp/plugins/songs/forms/editversedialog.py (+3/-2)
openlp/plugins/songs/forms/editverseform.py (+5/-5)
openlp/plugins/songs/forms/mediafilesdialog.py (+1/-1)
openlp/plugins/songs/forms/mediafilesform.py (+1/-1)
openlp/plugins/songs/forms/songbookdialog.py (+1/-1)
openlp/plugins/songs/forms/songexportform.py (+11/-6)
openlp/plugins/songs/forms/songimportform.py (+7/-3)
openlp/plugins/songs/forms/songmaintenancedialog.py (+1/-1)
openlp/plugins/songs/forms/songmaintenanceform.py (+35/-15)
openlp/plugins/songs/forms/songreviewwidget.py (+1/-1)
openlp/plugins/songs/forms/songselectdialog.py (+3/-2)
openlp/plugins/songs/forms/songselectform.py (+6/-2)
openlp/plugins/songs/forms/topicsdialog.py (+1/-1)
openlp/plugins/songs/lib/__init__.py (+14/-9)
openlp/plugins/songs/lib/db.py (+6/-4)
openlp/plugins/songs/lib/importer.py (+80/-44)
openlp/plugins/songs/lib/importers/cclifile.py (+8/-7)
openlp/plugins/songs/lib/importers/dreambeam.py (+6/-4)
openlp/plugins/songs/lib/importers/easyslides.py (+12/-10)
openlp/plugins/songs/lib/importers/easyworship.py (+45/-7)
openlp/plugins/songs/lib/importers/foilpresenter.py (+3/-2)
openlp/plugins/songs/lib/importers/lyrix.py (+2/-2)
openlp/plugins/songs/lib/importers/mediashout.py (+23/-8)
openlp/plugins/songs/lib/importers/openlp.py (+38/-1)
openlp/plugins/songs/lib/importers/openlyrics.py (+6/-4)
openlp/plugins/songs/lib/importers/openoffice.py (+2/-3)
openlp/plugins/songs/lib/importers/opensong.py (+5/-5)
openlp/plugins/songs/lib/importers/opspro.py (+265/-0)
openlp/plugins/songs/lib/importers/powerpraise.py (+3/-2)
openlp/plugins/songs/lib/importers/powersong.py (+12/-11)
openlp/plugins/songs/lib/importers/presentationmanager.py (+5/-2)
openlp/plugins/songs/lib/importers/propresenter.py (+77/-17)
openlp/plugins/songs/lib/importers/songimport.py (+6/-5)
openlp/plugins/songs/lib/importers/songshowplus.py (+13/-9)
openlp/plugins/songs/lib/importers/sundayplus.py (+1/-1)
openlp/plugins/songs/lib/importers/videopsalm.py (+5/-1)
openlp/plugins/songs/lib/importers/wordsofworship.py (+6/-6)
openlp/plugins/songs/lib/importers/worshipassistant.py (+10/-7)
openlp/plugins/songs/lib/importers/worshipcenterpro.py (+4/-2)
openlp/plugins/songs/lib/importers/zionworx.py (+7/-6)
openlp/plugins/songs/lib/mediaitem.py (+203/-40)
openlp/plugins/songs/lib/openlyricsexport.py (+7/-6)
openlp/plugins/songs/lib/openlyricsxml.py (+18/-11)
openlp/plugins/songs/lib/songcompare.py (+3/-3)
openlp/plugins/songs/lib/songselect.py (+54/-3)
openlp/plugins/songs/lib/songstab.py (+4/-4)
openlp/plugins/songs/lib/upgrade.py (+1/-1)
openlp/plugins/songs/reporting.py (+106/-0)
openlp/plugins/songs/songsplugin.py (+30/-12)
openlp/plugins/songusage/forms/songusagedeletedialog.py (+1/-1)
openlp/plugins/songusage/forms/songusagedetaildialog.py (+1/-1)
openlp/plugins/songusage/forms/songusagedetailform.py (+12/-9)
openlp/plugins/songusage/songusageplugin.py (+1/-1)
pylintrc (+379/-0)
resources/images/openlp-2.qrc (+14/-31)
resources/images/openlp-about-logo.svg (+0/-323)
resources/images/openlp-default-dualdisplay.svg (+0/-489)
resources/images/openlp-logo.svg (+98/-347)
resources/images/openlp-splash-screen.svg (+0/-296)
scripts/appveyor-webhook.py (+137/-0)
scripts/appveyor.yml (+84/-0)
scripts/check_dependencies.py (+2/-0)
scripts/clean_up_resources.py (+99/-0)
scripts/jenkins_script.py (+2/-1)
scripts/translation_utils.py (+5/-1)
setup.py (+2/-2)
tests/functional/openlp_core/test_init.py (+12/-24)
tests/functional/openlp_core_common/test_actions.py (+259/-0)
tests/functional/openlp_core_common/test_applocation.py (+10/-10)
tests/functional/openlp_core_common/test_common.py (+9/-9)
tests/functional/openlp_core_common/test_db.py (+104/-0)
tests/functional/openlp_core_common/test_httputils.py (+274/-0)
tests/functional/openlp_core_common/test_init.py (+403/-0)
tests/functional/openlp_core_common/test_languagemanager.py (+66/-0)
tests/functional/openlp_core_common/test_languages.py (+109/-0)
tests/functional/openlp_core_common/test_projector_utilities.py (+9/-5)
tests/functional/openlp_core_common/test_registry.py (+41/-4)
tests/functional/openlp_core_common/test_registrymixin.py (+2/-2)
tests/functional/openlp_core_common/test_registryproperties.py (+74/-29)
tests/functional/openlp_core_common/test_settings.py (+6/-6)
tests/functional/openlp_core_common/test_uistrings.py (+1/-1)
tests/functional/openlp_core_common/test_versionchecker.py (+63/-0)
tests/functional/openlp_core_lib/test_db.py (+5/-5)
tests/functional/openlp_core_lib/test_file_dialog.py (+3/-3)
tests/functional/openlp_core_lib/test_formattingtags.py (+2/-2)
tests/functional/openlp_core_lib/test_htmlbuilder.py (+212/-174)
tests/functional/openlp_core_lib/test_image_manager.py (+3/-3)
tests/functional/openlp_core_lib/test_lib.py (+280/-83)
tests/functional/openlp_core_lib/test_mediamanageritem.py (+4/-4)
tests/functional/openlp_core_lib/test_pluginmanager.py (+22/-22)
tests/functional/openlp_core_lib/test_projector_pjlink1.py (+317/-0)
tests/functional/openlp_core_lib/test_projectordb.py (+115/-10)
tests/functional/openlp_core_lib/test_renderer.py (+50/-6)
tests/functional/openlp_core_lib/test_screen.py (+1/-1)
tests/functional/openlp_core_lib/test_serviceitem.py (+13/-12)
tests/functional/openlp_core_lib/test_theme.py (+72/-33)
tests/functional/openlp_core_lib/test_ui.py (+13/-13)
tests/functional/openlp_core_ui/test_aboutform.py (+36/-0)
tests/functional/openlp_core_ui/test_exceptionform.py (+207/-0)
tests/functional/openlp_core_ui/test_first_time.py (+57/-0)
tests/functional/openlp_core_ui/test_firsttimeform.py (+10/-24)
tests/functional/openlp_core_ui/test_formattingtagscontroller.py (+4/-4)
tests/functional/openlp_core_ui/test_formattingtagsform.py (+2/-2)
tests/functional/openlp_core_ui/test_maindisplay.py (+109/-8)
tests/functional/openlp_core_ui/test_mainwindow.py (+72/-16)
tests/functional/openlp_core_ui/test_servicemanager.py (+250/-9)
tests/functional/openlp_core_ui/test_settingsform.py (+11/-11)
tests/functional/openlp_core_ui/test_shortcutlistdialog.py (+60/-0)
tests/functional/openlp_core_ui/test_slidecontroller.py (+280/-60)
tests/functional/openlp_core_ui/test_themeform.py (+2/-2)
tests/functional/openlp_core_ui/test_thememanager.py (+9/-9)
tests/functional/openlp_core_ui/test_themetab.py (+84/-0)
tests/functional/openlp_core_ui_lib/test_color_button.py (+199/-0)
tests/functional/openlp_core_ui_lib/test_listpreviewwidget.py (+503/-0)
tests/functional/openlp_core_ui_lib/test_listwidgetwithdnd.py (+104/-0)
tests/functional/openlp_core_ui_media/test_mediacontroller.py (+22/-21)
tests/functional/openlp_core_ui_media/test_systemplayer.py (+547/-0)
tests/functional/openlp_core_ui_media/test_vlcplayer.py (+46/-53)
tests/functional/openlp_core_ui_media/test_webkitplayer.py (+2/-2)
tests/functional/openlp_plugins/alerts/test_manager.py (+3/-3)
tests/functional/openlp_plugins/bibles/test_bibleimport.py (+615/-0)
tests/functional/openlp_plugins/bibles/test_bibleserver.py (+12/-12)
tests/functional/openlp_plugins/bibles/test_csvimport.py (+273/-18)
tests/functional/openlp_plugins/bibles/test_db.py (+33/-0)
tests/functional/openlp_plugins/bibles/test_lib.py (+22/-18)
tests/functional/openlp_plugins/bibles/test_manager.py (+69/-0)
tests/functional/openlp_plugins/bibles/test_mediaitem.py (+118/-3)
tests/functional/openlp_plugins/bibles/test_opensongimport.py (+348/-36)
tests/functional/openlp_plugins/bibles/test_osisimport.py (+376/-23)
tests/functional/openlp_plugins/bibles/test_swordimport.py (+110/-0)
tests/functional/openlp_plugins/bibles/test_versereferencelist.py (+5/-5)
tests/functional/openlp_plugins/bibles/test_wordprojectimport.py (+220/-0)
tests/functional/openlp_plugins/bibles/test_zefaniaimport.py (+11/-13)
tests/functional/openlp_plugins/custom/test_mediaitem.py (+3/-3)
tests/functional/openlp_plugins/images/test_imagetab.py (+2/-2)
tests/functional/openlp_plugins/images/test_lib.py (+11/-11)
tests/functional/openlp_plugins/media/test_mediaplugin.py (+28/-4)
tests/functional/openlp_plugins/presentations/test_impresscontroller.py (+5/-5)
tests/functional/openlp_plugins/presentations/test_mediaitem.py (+3/-3)
tests/functional/openlp_plugins/presentations/test_messagelistener.py (+4/-4)
tests/functional/openlp_plugins/presentations/test_pdfcontroller.py (+77/-3)
tests/functional/openlp_plugins/presentations/test_powerpointcontroller.py (+10/-10)
tests/functional/openlp_plugins/presentations/test_pptviewcontroller.py (+7/-7)
tests/functional/openlp_plugins/presentations/test_presentationcontroller.py (+10/-10)
tests/functional/openlp_plugins/remotes/test_remotetab.py (+6/-6)
tests/functional/openlp_plugins/remotes/test_router.py (+39/-14)
tests/functional/openlp_plugins/songs/test_editsongform.py (+36/-2)
tests/functional/openlp_plugins/songs/test_editverseform.py (+1/-1)
tests/functional/openlp_plugins/songs/test_ewimport.py (+15/-15)
tests/functional/openlp_plugins/songs/test_foilpresenterimport.py (+5/-5)
tests/functional/openlp_plugins/songs/test_lib.py (+55/-28)
tests/functional/openlp_plugins/songs/test_mediaitem.py (+244/-9)
tests/functional/openlp_plugins/songs/test_mediashout.py (+228/-0)
tests/functional/openlp_plugins/songs/test_openlpimporter.py (+75/-0)
tests/functional/openlp_plugins/songs/test_openlyricsexport.py (+1/-1)
tests/functional/openlp_plugins/songs/test_openlyricsimport.py (+5/-5)
tests/functional/openlp_plugins/songs/test_openoffice.py (+2/-2)
tests/functional/openlp_plugins/songs/test_opensongimport.py (+3/-3)
tests/functional/openlp_plugins/songs/test_opsproimport.py (+173/-0)
tests/functional/openlp_plugins/songs/test_presentationmanagerimport.py (+0/-2)
tests/functional/openlp_plugins/songs/test_propresenterimport.py (+16/-4)
tests/functional/openlp_plugins/songs/test_songbeamerimport.py (+4/-4)
tests/functional/openlp_plugins/songs/test_songselect.py (+78/-32)
tests/functional/openlp_plugins/songs/test_songshowplusimport.py (+5/-5)
tests/functional/openlp_plugins/songs/test_sundayplusimport.py (+0/-2)
tests/functional/openlp_plugins/songs/test_videopsalm.py (+0/-3)
tests/functional/openlp_plugins/songs/test_worshipassistantimport.py (+0/-2)
tests/functional/openlp_plugins/songs/test_worshipcenterproimport.py (+34/-31)
tests/functional/openlp_plugins/songs/test_zionworximport.py (+1/-1)
tests/functional/openlp_plugins/songusage/test_songusage.py (+19/-3)
tests/functional/test_init.py (+4/-4)
tests/interfaces/openlp_core_common/test_utils.py (+81/-0)
tests/interfaces/openlp_core_lib/test_pluginmanager.py (+10/-0)
tests/interfaces/openlp_core_lib/test_searchedit.py (+23/-12)
tests/interfaces/openlp_core_ui/test_filerenamedialog.py (+3/-3)
tests/interfaces/openlp_core_ui/test_mainwindow.py (+3/-3)
tests/interfaces/openlp_core_ui/test_projectoreditform.py (+25/-27)
tests/interfaces/openlp_core_ui/test_projectormanager.py (+2/-2)
tests/interfaces/openlp_core_ui/test_projectorsourceform.py (+3/-3)
tests/interfaces/openlp_core_ui/test_servicemanager.py (+8/-8)
tests/interfaces/openlp_core_ui/test_servicenotedialog.py (+1/-1)
tests/interfaces/openlp_core_ui/test_settings_form.py (+7/-7)
tests/interfaces/openlp_core_ui/test_shortcutlistform.py (+10/-10)
tests/interfaces/openlp_core_ui/test_starttimedialog.py (+2/-2)
tests/interfaces/openlp_core_ui/test_thememanager.py (+4/-4)
tests/interfaces/openlp_core_ui_lib/__init__.py (+21/-0)
tests/interfaces/openlp_core_ui_lib/test_historycombobox.py (+64/-0)
tests/interfaces/openlp_core_ui_lib/test_listpreviewwidget.py (+106/-0)
tests/interfaces/openlp_core_ul_media_vendor/__init__.py (+21/-0)
tests/interfaces/openlp_core_ul_media_vendor/test_mediainfoWrapper.py (+51/-0)
tests/interfaces/openlp_plugins/bibles/forms/test_bibleimportform.py (+17/-3)
tests/interfaces/openlp_plugins/bibles/test_lib_http.py (+27/-11)
tests/interfaces/openlp_plugins/bibles/test_lib_manager.py (+4/-4)
tests/interfaces/openlp_plugins/bibles/test_lib_parse_reference.py (+5/-5)
tests/interfaces/openlp_plugins/custom/forms/test_customform.py (+24/-5)
tests/interfaces/openlp_plugins/custom/forms/test_customslideform.py (+2/-2)
tests/interfaces/openlp_plugins/media/forms/test_mediaclipselectorform.py (+4/-4)
tests/interfaces/openlp_plugins/songs/forms/test_authorsform.py (+7/-7)
tests/interfaces/openlp_plugins/songs/forms/test_editsongform.py (+7/-7)
tests/interfaces/openlp_plugins/songs/forms/test_editverseform.py (+5/-5)
tests/interfaces/openlp_plugins/songs/forms/test_topicsform.py (+3/-3)
tests/resources/bibles/dk1933.json (+10/-10)
tests/resources/bibles/kjv.json (+10/-10)
tests/resources/bibles/rst.json (+10/-10)
tests/resources/bibles/web.json (+10/-10)
tests/resources/bibles/wordproject_chapter.htm (+248/-0)
tests/resources/bibles/wordproject_index.htm (+222/-0)
tests/resources/opensongsongs/Amazing Grace with bad CCLI (+56/-0)
tests/resources/opensongsongs/Amazing Grace without CCLI.json (+42/-0)
tests/resources/opsprosongs/Amazing Grace.json (+21/-0)
tests/resources/opsprosongs/Amazing Grace3.json (+31/-0)
tests/resources/opsprosongs/You are so faithful.json (+31/-0)
tests/resources/opsprosongs/amazing grace.txt (+24/-0)
tests/resources/opsprosongs/amazing grace2.txt (+29/-0)
tests/resources/opsprosongs/amazing grace3.txt (+31/-0)
tests/resources/opsprosongs/you are so faithfull.txt (+37/-0)
tests/resources/presentationmanagersongs/Agnus Dei.json (+0/-14)
tests/resources/presentationmanagersongs/Agnus Dei.sng (+0/-34)
tests/resources/propresentersongs/Amazing Grace.pro5 (+520/-0)
tests/resources/propresentersongs/Amazing Grace.pro6 (+490/-0)
tests/resources/propresentersongs/Vaste Grond.json (+0/-34)
tests/resources/propresentersongs/Vaste Grond.pro4 (+0/-1)
tests/resources/songshowplussongs/cleanse-me.json (+38/-0)
tests/resources/sundayplussongs/Abba Fader.ptf (+0/-8)
tests/resources/sundayplussongs/abba-fader.json (+0/-13)
tests/resources/videopsalmsongs/as-safe-a-stronghold2.json (+35/-0)
tests/resources/videopsalmsongs/videopsalm-as-safe-a-stronghold2.json (+47/-0)
tests/resources/worshipassistantsongs/lift_up_your_heads.csv (+0/-40)
tests/resources/worshipassistantsongs/lift_up_your_heads.json (+0/-13)
tests/utils/__init__.py (+1/-1)
tests/utils/test_bzr_tags.py (+1/-1)
tests/utils/test_pylint.py (+113/-0)
Text conflict in .bzrignore
Text conflict in openlp/.version
Text conflict in openlp/core/common/__init__.py
Contents conflict in openlp/core/common/historycombobox.py
Contents conflict in openlp/core/lib/colorbutton.py
Contents conflict in openlp/core/lib/dockwidget.py
Contents conflict in openlp/core/lib/listwidgetwithdnd.py
Text conflict in openlp/core/lib/projector/constants.py
Text conflict in openlp/core/lib/projector/pjlink1.py
Contents conflict in openlp/core/lib/spelltextedit.py
Contents conflict in openlp/core/lib/toolbar.py
Contents conflict in openlp/core/lib/treewidgetwithdnd.py
Text conflict in openlp/core/resources.py
Text conflict in openlp/core/ui/aboutdialog.py
Text conflict in openlp/core/ui/exceptionform.py
Contents conflict in openlp/core/ui/listpreviewwidget.py
Text conflict in openlp/core/ui/mainwindow.py
Text conflict in openlp/core/ui/media/systemplayer.py
Contents conflict in openlp/core/ui/mediadockmanager.py
Text conflict in openlp/core/ui/projector/manager.py
Text conflict in openlp/core/ui/projector/sourceselectform.py
Contents conflict in openlp/core/ui/wizard.py
Conflict: can't delete openlp/core/utils because it is not empty.  Not deleting.
Conflict because openlp/core/utils is not versioned, but has versioned children.  Versioned directory.
Contents conflict in openlp/core/utils/__init__.py
Contents conflict in openlp/core/utils/actions.py
Contents conflict in openlp/core/utils/db.py
Contents conflict in openlp/core/utils/languagemanager.py
Contents conflict in openlp/plugins/bibles/forms/bibleupgradeform.py
Text conflict in openlp/plugins/bibles/lib/importers/http.py
Text conflict in openlp/plugins/bibles/lib/importers/zefania.py
Text conflict in openlp/plugins/bibles/lib/mediaitem.py
Text conflict in openlp/plugins/presentations/lib/pdfcontroller.py
Text conflict in openlp/plugins/remotes/html/index.html
Contents conflict in openlp/plugins/remotes/html/jquery.js
Contents conflict in openlp/plugins/remotes/html/jquery.min.js
Contents conflict in openlp/plugins/remotes/html/jquery.mobile.js
Contents conflict in openlp/plugins/remotes/html/jquery.mobile.min.css
Contents conflict in openlp/plugins/remotes/html/jquery.mobile.min.js
Contents conflict in openlp/plugins/remotes/html/main.css
Contents conflict in openlp/plugins/remotes/html/main.js
Contents conflict in openlp/plugins/remotes/html/openlp.css
Contents conflict in openlp/plugins/remotes/html/openlp.js
Contents conflict in openlp/plugins/remotes/html/stage.css
Contents conflict in openlp/plugins/remotes/html/stage.js
Text conflict in openlp/plugins/remotes/lib/remotetab.py
Text conflict in openlp/plugins/songs/lib/__init__.py
Text conflict in openlp/plugins/songs/lib/importers/easyworship.py
Text conflict in openlp/plugins/songs/lib/importers/mediashout.py
Text conflict in openlp/plugins/songs/lib/importers/openlp.py
Text conflict in openlp/plugins/songs/lib/importers/videopsalm.py
Text conflict in openlp/plugins/songs/lib/mediaitem.py
Text conflict in openlp/plugins/songs/lib/songselect.py
Conflict adding file resources/images/ios_app_qr.png.  Moved existing file to resources/images/ios_app_qr.png.moved.
Text conflict in scripts/translation_utils.py
Text conflict in tests/functional/openlp_core_common/test_projector_utilities.py
Text conflict in tests/functional/openlp_core_common/test_registryproperties.py
Contents conflict in tests/functional/openlp_core_lib/test_color_button.py
Text conflict in tests/functional/openlp_core_lib/test_htmlbuilder.py
Text conflict in tests/functional/openlp_core_lib/test_projector_pjlink1.py
Text conflict in tests/functional/openlp_core_ui/test_aboutform.py
Text conflict in tests/functional/openlp_core_ui/test_firsttimeform.py
Contents conflict in tests/functional/openlp_core_ui/test_listpreviewwidget.py
Text conflict in tests/functional/openlp_core_ui/test_servicemanager.py
Conflict adding file tests/functional/openlp_core_ui/test_shortcutlistdialog.py.  Moved existing file to tests/functional/openlp_core_ui/test_shortcutlistdialog.py.moved.
Conflict adding file tests/functional/openlp_core_ui/test_themetab.py.  Moved existing file to tests/functional/openlp_core_ui/test_themetab.py.moved.
Conflict adding file tests/functional/openlp_core_ui_media/test_systemplayer.py.  Moved existing file to tests/functional/openlp_core_ui_media/test_systemplayer.py.moved.
Conflict: can't delete tests/functional/openlp_core_utils because it is not empty.  Not deleting.
Conflict because tests/functional/openlp_core_utils is not versioned, but has versioned children.  Versioned directory.
Contents conflict in tests/functional/openlp_core_utils/__init__.py
Contents conflict in tests/functional/openlp_core_utils/test_actions.py
Contents conflict in tests/functional/openlp_core_utils/test_db.py
Contents conflict in tests/functional/openlp_core_utils/test_first_time.py
Contents conflict in tests/functional/openlp_core_utils/test_init.py
Contents conflict in tests/functional/openlp_core_utils/test_utils.py
Conflict adding file tests/functional/openlp_plugins/bibles/test_manager.py.  Moved existing file to tests/functional/openlp_plugins/bibles/test_manager.py.moved.
Text conflict in tests/functional/openlp_plugins/presentations/test_pdfcontroller.py
Text conflict in tests/functional/openlp_plugins/songs/test_editsongform.py
Text conflict in tests/functional/openlp_plugins/songs/test_lib.py
Conflict adding file tests/functional/openlp_plugins/songs/test_openlpimporter.py.  Moved existing file to tests/functional/openlp_plugins/songs/test_openlpimporter.py.moved.
Text conflict in tests/functional/openlp_plugins/songs/test_songselect.py
Contents conflict in tests/interfaces/openlp_core_common/test_historycombobox.py
Text conflict in tests/interfaces/openlp_core_lib/test_pluginmanager.py
Contents conflict in tests/interfaces/openlp_core_ui/test_listpreviewwidget.py
Conflict: can't delete tests/interfaces/openlp_core_utils because it is not empty.  Not deleting.
Conflict because tests/interfaces/openlp_core_utils is not versioned, but has versioned children.  Versioned directory.
Contents conflict in tests/interfaces/openlp_core_utils/__init__.py
Contents conflict in tests/interfaces/openlp_core_utils/test_utils.py
Text conflict in tests/interfaces/openlp_plugins/custom/forms/test_customform.py
Conflict adding file tests/resources/opensongsongs/Amazing Grace with bad CCLI.  Moved existing file to tests/resources/opensongsongs/Amazing Grace with bad CCLI.moved.
Conflict adding file tests/resources/opensongsongs/Amazing Grace without CCLI.json.  Moved existing file to tests/resources/opensongsongs/Amazing Grace without CCLI.json.moved.
Conflict adding file tests/resources/songshowplussongs/cleanse-me.json.  Moved existing file to tests/resources/songshowplussongs/cleanse-me.json.moved.
Conflict adding file tests/resources/songshowplussongs/cleanse-me.sbsong.  Moved existing file to tests/resources/songshowplussongs/cleanse-me.sbsong.moved.
Conflict adding file tests/resources/videopsalmsongs/as-safe-a-stronghold2.json.  Moved existing file to tests/resources/videopsalmsongs/as-safe-a-stronghold2.json.moved.
Conflict adding file tests/resources/videopsalmsongs/videopsalm-as-safe-a-stronghold2.json.  Moved existing file to tests/resources/videopsalmsongs/videopsalm-as-safe-a-stronghold2.json.moved.
To merge this branch: bzr merge lp:~suutari-olli/openlp/fix-openlp-importer
Reviewer Review Type Date Requested Status
OpenLP Core Pending
Review via email: mp+314295@code.launchpad.net

This proposal has been superseded by a proposal from 2017-01-08.

Description of the change

- For diff

To post a comment you must log in.
2717. By Azaziah

- Fixed bug #1632567

2718. By Azaziah

- This is broken, attempted to fix author types on import

Unmerged revisions

2718. By Azaziah

- This is broken, attempted to fix author types on import

2717. By Azaziah

- Fixed bug #1632567

2716. By Azaziah

- Better fix, now checks for songbook entry 1st.

2715. By Azaziah

- Fixed bug: 1652003

2714. By Tim Bentley

Update year in headers and about form

2713. By Tim Bentley

Move some http code around to allow it to be reused in the remote updates.

lp:~trb143/openlp/httpfixes (revision 2718)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1888/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1799/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1738/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1474/
[SUCCESS] https://ci.openlp.io/job/Branch-04b-Windows_Interface_Tests/1064/
[SUCCESS] https://ci.openlp.io/jo...

2712. By Tomas Groth

Update to the appveyor integration script to work with the new packaging builder.

2711. By Raoul Snyman

Fix segfault on click on a spinner on macOS
Fix spurious traceback on some platforms or configurations of media players
Make the tab style affect only the media library tabs, not everything else too
Hide the splash screen when the backup dialog shows and when the exception form shows

Add this to your merge proposal:
--------------------------------
lp:~raoul-snyman/openlp/mac-niggles (revision 2713)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1873/
[SUCCESS] https://ci.openlp.io/job/B...

2710. By Raoul Snyman

Fix bug #1642684 by removing the blank item and clearing the text box instead.

Add this to your merge proposal:
--------------------------------
lp:~raoul-snyman/openlp/bug-1642684 (revision 2710)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1869/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1780/
[SUCCESS] https://ci.openlp.io/job/Branch-03-Interface-Tests/1718/
[SUCCESS] https://ci.openlp.io/job/Branch-04a-Windows_Functional_Tests/1458/
[SUCCESS] https://ci.openlp.io/...

2709. By Raoul Snyman

Woo! Brand new OpenLP look and feel!

Unfortunately, the Crosswalk servers seem a little dodgy at the moment.

Add this to your merge proposal:
--------------------------------
lp:~raoul-snyman/openlp/new-branding (revision 2703)
[SUCCESS] https://ci.openlp.io/job/Branch-01-Pull/1862/
[SUCCESS] https://ci.openlp.io/job/Branch-02-Functional-Tests/1773/
[FAILURE] https://ci.openlp.io/job/Branch-03-Interface-Tests/1711/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2016-04-27 18:45:39 +0000
3+++ .bzrignore 2017-01-08 22:05:36 +0000
4@@ -45,4 +45,9 @@
5 *.kdev4
6 coverage
7 tags
8-output
9+<<<<<<< TREE
10+output
11+=======
12+output
13+htmlcov
14+>>>>>>> MERGE-SOURCE
15
16=== removed file '.coveragerc'
17--- .coveragerc 2016-01-04 00:01:23 +0000
18+++ .coveragerc 1970-01-01 00:00:00 +0000
19@@ -1,5 +0,0 @@
20-[run]
21-source = openlp
22-
23-[html]
24-directory = coverage
25
26=== added file 'nose2.cfg'
27--- nose2.cfg 1970-01-01 00:00:00 +0000
28+++ nose2.cfg 2017-01-08 22:05:36 +0000
29@@ -0,0 +1,22 @@
30+[unittest]
31+verbose = true
32+plugins = nose2.plugins.mp
33+
34+[log-capture]
35+always-on = true
36+clear-handlers = true
37+filter = -nose
38+log-level = ERROR
39+
40+[test-result]
41+always-on = true
42+descriptions = true
43+
44+[coverage]
45+always-on = true
46+coverage = openlp
47+coverage-report = html
48+
49+[multiprocess]
50+always-on = false
51+processes = 4
52
53=== modified file 'openlp/.version'
54--- openlp/.version 2016-11-26 15:09:53 +0000
55+++ openlp/.version 2017-01-08 22:05:36 +0000
56@@ -1,1 +1,5 @@
57+<<<<<<< TREE
58 2.4.4
59+=======
60+2.5.0
61+>>>>>>> MERGE-SOURCE
62
63=== modified file 'openlp/core/__init__.py'
64--- openlp/core/__init__.py 2016-12-31 11:05:48 +0000
65+++ openlp/core/__init__.py 2017-01-08 22:05:36 +0000
66@@ -27,26 +27,26 @@
67 logging and a plugin framework are contained within the openlp.core module.
68 """
69
70+import argparse
71+import logging
72 import os
73+import shutil
74 import sys
75-import logging
76-import argparse
77+import time
78 from traceback import format_exception
79-import shutil
80-import time
81+
82 from PyQt5 import QtCore, QtGui, QtWidgets
83
84-from openlp.core.common import Registry, OpenLPMixin, AppLocation, Settings, UiStrings, check_directory_exists, \
85- is_macosx, is_win, translate
86+from openlp.core.common import Registry, OpenLPMixin, AppLocation, LanguageManager, Settings, UiStrings, \
87+ check_directory_exists, is_macosx, is_win, translate
88+from openlp.core.common.versionchecker import VersionThread, get_application_version
89 from openlp.core.lib import ScreenList
90 from openlp.core.resources import qInitResources
91+from openlp.core.ui import SplashScreen
92+from openlp.core.ui.exceptionform import ExceptionForm
93+from openlp.core.ui.firsttimeform import FirstTimeForm
94+from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
95 from openlp.core.ui.mainwindow import MainWindow
96-from openlp.core.ui.firsttimelanguageform import FirstTimeLanguageForm
97-from openlp.core.ui.firsttimeform import FirstTimeForm
98-from openlp.core.ui.exceptionform import ExceptionForm
99-from openlp.core.ui import SplashScreen
100-from openlp.core.utils import LanguageManager, VersionThread, get_application_version
101-
102
103 __all__ = ['OpenLP', 'main']
104
105@@ -177,6 +177,38 @@
106 self.shared_memory.create(1)
107 return False
108
109+ def is_data_path_missing(self):
110+ """
111+ Check if the data folder path exists.
112+ """
113+ data_folder_path = AppLocation.get_data_path()
114+ if not os.path.exists(data_folder_path):
115+ log.critical('Database was not found in: ' + data_folder_path)
116+ status = QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'Data Directory Error'),
117+ translate('OpenLP', 'OpenLP data folder was not found in:\n\n{path}'
118+ '\n\nThe location of the data folder was '
119+ 'previously changed from the OpenLP\'s '
120+ 'default location. If the data was stored on '
121+ 'removable device, that device needs to be '
122+ 'made available.\n\nYou may reset the data '
123+ 'location back to the default location, '
124+ 'or you can try to make the current location '
125+ 'available.\n\nDo you want to reset to the '
126+ 'default data location? If not, OpenLP will be '
127+ 'closed so you can try to fix the the problem.')
128+ .format(path=data_folder_path),
129+ QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes |
130+ QtWidgets.QMessageBox.No),
131+ QtWidgets.QMessageBox.No)
132+ if status == QtWidgets.QMessageBox.No:
133+ # If answer was "No", return "True", it will shutdown OpenLP in def main
134+ log.info('User requested termination')
135+ return True
136+ # If answer was "Yes", remove the custom data path thus resetting the default location.
137+ Settings().remove('advanced/data path')
138+ log.info('Database location has been reset to the default settings.')
139+ return False
140+
141 def hook_exception(self, exc_type, value, traceback):
142 """
143 Add an exception hook so that any uncaught exceptions are displayed in this window rather than somewhere where
144@@ -205,6 +237,7 @@
145 Check if OpenLP has been upgraded, and ask if a backup of data should be made
146
147 :param has_run_wizard: OpenLP has been run before
148+ :param can_show_splash: Should OpenLP show the splash screen
149 """
150 data_version = Settings().value('core/application version')
151 openlp_version = get_application_version()['version']
152@@ -216,8 +249,8 @@
153 if self.splash.isVisible():
154 self.splash.hide()
155 if QtWidgets.QMessageBox.question(None, translate('OpenLP', 'Backup'),
156- translate('OpenLP', 'OpenLP has been upgraded, do you want to create '
157- 'a backup of OpenLPs data folder?'),
158+ translate('OpenLP', 'OpenLP has been upgraded, do you want to create\n'
159+ 'a backup of the old data folder?'),
160 QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
161 QtWidgets.QMessageBox.Yes) == QtWidgets.QMessageBox.Yes:
162 # Create copy of data folder
163@@ -230,10 +263,11 @@
164 QtWidgets.QMessageBox.warning(None, translate('OpenLP', 'Backup'),
165 translate('OpenLP', 'Backup of the data folder failed!'))
166 return
167- QtWidgets.QMessageBox.information(None, translate('OpenLP', 'Backup'),
168- translate('OpenLP',
169- 'A backup of the data folder has been created at %s')
170- % data_folder_backup_path)
171+ message = translate('OpenLP',
172+ 'A backup of the data folder has been created at:\n\n'
173+ '{text}').format(text=data_folder_backup_path)
174+ QtWidgets.QMessageBox.information(None, translate('OpenLP', 'Backup'), message)
175+
176 # Update the version in the settings
177 Settings().setValue('core/application version', openlp_version)
178 if can_show_splash:
179@@ -267,7 +301,7 @@
180 """
181 if event.type() == QtCore.QEvent.FileOpen:
182 file_name = event.file()
183- log.debug('Got open file event for %s!', file_name)
184+ log.debug('Got open file event for {name}!'.format(name=file_name))
185 self.args.insert(0, file_name)
186 return True
187 # Mac OS X should restore app window when user clicked on the OpenLP icon
188@@ -321,7 +355,7 @@
189 logfile.setFormatter(logging.Formatter('%(asctime)s %(name)-55s %(levelname)-8s %(message)s'))
190 log.addHandler(logfile)
191 if log.isEnabledFor(logging.DEBUG):
192- print('Logging to: %s' % filename)
193+ print('Logging to: {name}'.format(name=filename))
194
195
196 def main(args=None):
197@@ -362,12 +396,12 @@
198 log.info('Running portable')
199 portable_settings_file = os.path.abspath(os.path.join(application_path, '..', '..', 'Data', 'OpenLP.ini'))
200 # Make this our settings file
201- log.info('INI file: %s', portable_settings_file)
202+ log.info('INI file: {name}'.format(name=portable_settings_file))
203 Settings.set_filename(portable_settings_file)
204 portable_settings = Settings()
205 # Set our data path
206 data_path = os.path.abspath(os.path.join(application_path, '..', '..', 'Data',))
207- log.info('Data path: %s', data_path)
208+ log.info('Data path: {name}'.format(name=data_path))
209 # Point to our data path
210 portable_settings.setValue('advanced/data path', data_path)
211 portable_settings.setValue('advanced/is portable', True)
212@@ -378,9 +412,13 @@
213 Registry.create()
214 Registry().register('application', application)
215 application.setApplicationVersion(get_application_version()['version'])
216- # Instance check
217+ # Check if an instance of OpenLP is already running. Quit if there is a running instance and the user only wants one
218 if application.is_already_running():
219 sys.exit()
220+ # If the custom data path is missing and the user wants to restore the data path, quit OpenLP.
221+ if application.is_data_path_missing():
222+ application.shared_memory.detach()
223+ sys.exit()
224 # Remove/convert obsolete settings.
225 Settings().remove_obsolete_settings()
226 # First time checks in settings
227
228=== modified file 'openlp/core/common/__init__.py'
229--- openlp/core/common/__init__.py 2016-12-31 11:05:48 +0000
230+++ openlp/core/common/__init__.py 2017-01-08 22:05:36 +0000
231@@ -24,15 +24,18 @@
232 OpenLP work.
233 """
234 import hashlib
235+
236+import logging
237+import os
238 import re
239-import os
240-import logging
241 import sys
242 import traceback
243+from chardet.universaldetector import UniversalDetector
244 from ipaddress import IPv4Address, IPv6Address, AddressValueError
245-from codecs import decode, encode
246+from shutil import which
247+from subprocess import check_output, CalledProcessError, STDOUT
248
249-from PyQt5 import QtCore
250+from PyQt5 import QtCore, QtGui
251 from PyQt5.QtCore import QCryptographicHash as QHash
252
253 log = logging.getLogger(__name__ + '.__init__')
254@@ -40,6 +43,9 @@
255
256 FIRST_CAMEL_REGEX = re.compile('(.)([A-Z][a-z]+)')
257 SECOND_CAMEL_REGEX = re.compile('([a-z0-9])([A-Z])')
258+CONTROL_CHARS = re.compile(r'[\x00-\x1F\x7F-\x9F]', re.UNICODE)
259+INVALID_FILE_CHARS = re.compile(r'[\\/:\*\?"<>\|\+\[\]%]', re.UNICODE)
260+IMAGES_FILTER = None
261
262
263 def trace_error_handler(logger):
264@@ -50,7 +56,9 @@
265 """
266 log_string = "OpenLP Error trace"
267 for tb in traceback.extract_stack():
268- log_string = '%s\n File %s at line %d \n\t called %s' % (log_string, tb[0], tb[1], tb[3])
269+ log_string += '\n File {file} at line {line} \n\t called {data}'.format(file=tb[0],
270+ line=tb[1],
271+ data=tb[3])
272 logger.error(log_string)
273
274
275@@ -62,7 +70,7 @@
276 :param do_not_log: To not log anything. This is need for the start up, when the log isn't ready.
277 """
278 if not do_not_log:
279- log.debug('check_directory_exists %s' % directory)
280+ log.debug('check_directory_exists {text}'.format(text=directory))
281 try:
282 if not os.path.exists(directory):
283 os.makedirs(directory)
284@@ -188,7 +196,7 @@
285 return True if verify_ipv4(addr) else verify_ipv6(addr)
286
287
288-def md5_hash(salt, data=None):
289+def md5_hash(salt=None, data=None):
290 """
291 Returns the hashed output of md5sum on salt,data
292 using Python3 hashlib
293@@ -197,33 +205,49 @@
294 :param data: OPTIONAL Data to hash
295 :returns: str
296 """
297- log.debug('md5_hash(salt="%s")' % salt)
298+ log.debug('md5_hash(salt="{text}")'.format(text=salt))
299+ if not salt and not data:
300+ return None
301 hash_obj = hashlib.new('md5')
302- hash_obj.update(salt)
303+ if salt:
304+ hash_obj.update(salt)
305 if data:
306 hash_obj.update(data)
307 hash_value = hash_obj.hexdigest()
308- log.debug('md5_hash() returning "%s"' % hash_value)
309+ log.debug('md5_hash() returning "{text}"'.format(text=hash_value))
310 return hash_value
311
312
313-def qmd5_hash(salt, data=None):
314+def qmd5_hash(salt=None, data=None):
315 """
316 Returns the hashed output of MD5Sum on salt, data
317- using PyQt5.QCryptographicHash.
318+ using PyQt5.QCryptographicHash. Function returns a
319+ QByteArray instead of a text string.
320+ If you need a string instead, call with
321+
322+ result = str(qmd5_hash(salt=..., data=...), encoding='ascii')
323
324 :param salt: Initial salt
325 :param data: OPTIONAL Data to hash
326- :returns: str
327+ :returns: QByteArray
328 """
329- log.debug('qmd5_hash(salt="%s"' % salt)
330+ log.debug('qmd5_hash(salt="{text}"'.format(text=salt))
331+ if salt is None and data is None:
332+ return None
333 hash_obj = QHash(QHash.Md5)
334+<<<<<<< TREE
335 hash_obj.addData(salt)
336 if data:
337 hash_obj.addData(data)
338+=======
339+ if salt:
340+ hash_obj.addData(salt)
341+ if data:
342+ hash_obj.addData(data)
343+>>>>>>> MERGE-SOURCE
344 hash_value = hash_obj.result().toHex()
345- log.debug('qmd5_hash() returning "%s"' % hash_value)
346- return hash_value.data()
347+ log.debug('qmd5_hash() returning "{hash}"'.format(hash=hash_value))
348+ return hash_value
349
350
351 def clean_button_text(button_text):
352@@ -242,4 +266,181 @@
353 from .uistrings import UiStrings
354 from .settings import Settings
355 from .applocation import AppLocation
356-from .historycombobox import HistoryComboBox
357+from .actions import ActionList
358+from .languagemanager import LanguageManager
359+
360+if is_win():
361+ from subprocess import STARTUPINFO, STARTF_USESHOWWINDOW
362+
363+
364+def add_actions(target, actions):
365+ """
366+ Adds multiple actions to a menu or toolbar in one command.
367+
368+ :param target: The menu or toolbar to add actions to
369+ :param actions: The actions to be added. An action consisting of the keyword ``None``
370+ will result in a separator being inserted into the target.
371+ """
372+ for action in actions:
373+ if action is None:
374+ target.addSeparator()
375+ else:
376+ target.addAction(action)
377+
378+
379+def get_uno_command(connection_type='pipe'):
380+ """
381+ Returns the UNO command to launch an libreoffice.org instance.
382+ """
383+ for command in ['libreoffice', 'soffice']:
384+ if which(command):
385+ break
386+ else:
387+ raise FileNotFoundError('Command not found')
388+
389+ OPTIONS = '--nologo --norestore --minimized --nodefault --nofirststartwizard'
390+ if connection_type == 'pipe':
391+ CONNECTION = '"--accept=pipe,name=openlp_pipe;urp;"'
392+ else:
393+ CONNECTION = '"--accept=socket,host=localhost,port=2002;urp;"'
394+ return '{cmd} {opt} {conn}'.format(cmd=command, opt=OPTIONS, conn=CONNECTION)
395+
396+
397+def get_uno_instance(resolver, connection_type='pipe'):
398+ """
399+ Returns a running libreoffice.org instance.
400+
401+ :param resolver: The UNO resolver to use to find a running instance.
402+ """
403+ log.debug('get UNO Desktop Openoffice - resolve')
404+ if connection_type == 'pipe':
405+ return resolver.resolve('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')
406+ else:
407+ return resolver.resolve('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')
408+
409+
410+def get_filesystem_encoding():
411+ """
412+ Returns the name of the encoding used to convert Unicode filenames into system file names.
413+ """
414+ encoding = sys.getfilesystemencoding()
415+ if encoding is None:
416+ encoding = sys.getdefaultencoding()
417+ return encoding
418+
419+
420+def split_filename(path):
421+ """
422+ Return a list of the parts in a given path.
423+ """
424+ path = os.path.abspath(path)
425+ if not os.path.isfile(path):
426+ return path, ''
427+ else:
428+ return os.path.split(path)
429+
430+
431+def delete_file(file_path_name):
432+ """
433+ Deletes a file from the system.
434+
435+ :param file_path_name: The file, including path, to delete.
436+ """
437+ if not file_path_name:
438+ return False
439+ try:
440+ if os.path.exists(file_path_name):
441+ os.remove(file_path_name)
442+ return True
443+ except (IOError, OSError):
444+ log.exception("Unable to delete file {text}".format(text=file_path_name))
445+ return False
446+
447+
448+def get_images_filter():
449+ """
450+ Returns a filter string for a file dialog containing all the supported image formats.
451+ """
452+ global IMAGES_FILTER
453+ if not IMAGES_FILTER:
454+ log.debug('Generating images filter.')
455+ formats = list(map(bytes.decode, list(map(bytes, QtGui.QImageReader.supportedImageFormats()))))
456+ visible_formats = '(*.{text})'.format(text='; *.'.join(formats))
457+ actual_formats = '(*.{text})'.format(text=' *.'.join(formats))
458+ IMAGES_FILTER = '{text} {visible} {actual}'.format(text=translate('OpenLP', 'Image Files'),
459+ visible=visible_formats,
460+ actual=actual_formats)
461+ return IMAGES_FILTER
462+
463+
464+def is_not_image_file(file_name):
465+ """
466+ Validate that the file is not an image file.
467+
468+ :param file_name: File name to be checked.
469+ """
470+ if not file_name:
471+ return True
472+ else:
473+ formats = [bytes(fmt).decode().lower() for fmt in QtGui.QImageReader.supportedImageFormats()]
474+ file_part, file_extension = os.path.splitext(str(file_name))
475+ if file_extension[1:].lower() in formats and os.path.exists(file_name):
476+ return False
477+ return True
478+
479+
480+def clean_filename(filename):
481+ """
482+ Removes invalid characters from the given ``filename``.
483+
484+ :param filename: The "dirty" file name to clean.
485+ """
486+ if not isinstance(filename, str):
487+ filename = str(filename, 'utf-8')
488+ return INVALID_FILE_CHARS.sub('_', CONTROL_CHARS.sub('', filename))
489+
490+
491+def check_binary_exists(program_path):
492+ """
493+ Function that checks whether a binary exists.
494+
495+ :param program_path:The full path to the binary to check.
496+ :return: program output to be parsed
497+ """
498+ log.debug('testing program_path: {text}'.format(text=program_path))
499+ try:
500+ # Setup startupinfo options for check_output to avoid console popping up on windows
501+ if is_win():
502+ startupinfo = STARTUPINFO()
503+ startupinfo.dwFlags |= STARTF_USESHOWWINDOW
504+ else:
505+ startupinfo = None
506+ runlog = check_output([program_path, '--help'], stderr=STDOUT, startupinfo=startupinfo)
507+ except CalledProcessError as e:
508+ runlog = e.output
509+ except Exception:
510+ trace_error_handler(log)
511+ runlog = ''
512+ log.debug('check_output returned: {text}'.format(text=runlog))
513+ return runlog
514+
515+
516+def get_file_encoding(filename):
517+ """
518+ Utility function to incrementally detect the file encoding.
519+
520+ :param filename: Filename for the file to determine the encoding for. Str
521+ :return: A dict with the keys 'encoding' and 'confidence'
522+ """
523+ detector = UniversalDetector()
524+ try:
525+ with open(filename, 'rb') as detect_file:
526+ while not detector.done:
527+ chunk = detect_file.read(1024)
528+ if not chunk:
529+ break
530+ detector.feed(chunk)
531+ detector.close()
532+ return detector.result
533+ except OSError:
534+ log.exception('Error detecting file encoding')
535
536=== added file 'openlp/core/common/actions.py'
537--- openlp/core/common/actions.py 1970-01-01 00:00:00 +0000
538+++ openlp/core/common/actions.py 2017-01-08 22:05:36 +0000
539@@ -0,0 +1,390 @@
540+# -*- coding: utf-8 -*-
541+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
542+
543+###############################################################################
544+# OpenLP - Open Source Lyrics Projection #
545+# --------------------------------------------------------------------------- #
546+# Copyright (c) 2008-2017 OpenLP Developers #
547+# --------------------------------------------------------------------------- #
548+# This program is free software; you can redistribute it and/or modify it #
549+# under the terms of the GNU General Public License as published by the Free #
550+# Software Foundation; version 2 of the License. #
551+# #
552+# This program is distributed in the hope that it will be useful, but WITHOUT #
553+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
554+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
555+# more details. #
556+# #
557+# You should have received a copy of the GNU General Public License along #
558+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
559+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
560+###############################################################################
561+"""
562+The :mod:`~openlp.core.utils.actions` module provides action list classes used
563+by the shortcuts system.
564+"""
565+import logging
566+
567+from PyQt5 import QtCore, QtGui, QtWidgets
568+
569+from openlp.core.common import Settings
570+
571+
572+log = logging.getLogger(__name__)
573+
574+
575+class ActionCategory(object):
576+ """
577+ The :class:`~openlp.core.utils.ActionCategory` class encapsulates a category for the
578+ :class:`~openlp.core.utils.CategoryList` class.
579+ """
580+ def __init__(self, name, weight=0):
581+ """
582+ Constructor
583+ """
584+ self.name = name
585+ self.weight = weight
586+ self.actions = CategoryActionList()
587+
588+
589+class CategoryActionList(object):
590+ """
591+ The :class:`~openlp.core.utils.CategoryActionList` class provides a sorted list of actions within a category.
592+ """
593+ def __init__(self):
594+ """
595+ Constructor
596+ """
597+ self.index = 0
598+ self.actions = []
599+
600+ def __contains__(self, key):
601+ """
602+ Implement the __contains__() method to make this class a dictionary type
603+ """
604+ for weight, action in self.actions:
605+ if action == key:
606+ return True
607+ return False
608+
609+ def __len__(self):
610+ """
611+ Implement the __len__() method to make this class a dictionary type
612+ """
613+ return len(self.actions)
614+
615+ def __iter__(self):
616+ """
617+ Implement the __getitem__() method to make this class iterable
618+ """
619+ return self
620+
621+ def __next__(self):
622+ """
623+ Python 3 "next" method.
624+ """
625+ if self.index >= len(self.actions):
626+ self.index = 0
627+ raise StopIteration
628+ else:
629+ self.index += 1
630+ return self.actions[self.index - 1][1]
631+
632+ def append(self, action):
633+ """
634+ Append an action
635+ """
636+ weight = 0
637+ if self.actions:
638+ weight = self.actions[-1][0] + 1
639+ self.add(action, weight)
640+
641+ def add(self, action, weight=0):
642+ """
643+ Add an action.
644+ """
645+ self.actions.append((weight, action))
646+ self.actions.sort(key=lambda act: act[0])
647+
648+ def remove(self, action):
649+ """
650+ Remove an action
651+ """
652+ for item in self.actions:
653+ if item[1] == action:
654+ self.actions.remove(item)
655+ return
656+ raise ValueError('Action "{action}" does not exist.'.format(action=action))
657+
658+
659+class CategoryList(object):
660+ """
661+ The :class:`~openlp.core.utils.CategoryList` class encapsulates a category list for the
662+ :class:`~openlp.core.utils.ActionList` class and provides an iterator interface for walking through the list of
663+ actions in this category.
664+ """
665+
666+ def __init__(self):
667+ """
668+ Constructor
669+ """
670+ self.index = 0
671+ self.categories = []
672+
673+ def __getitem__(self, key):
674+ """
675+ Implement the __getitem__() method to make this class like a dictionary
676+ """
677+ for category in self.categories:
678+ if category.name == key:
679+ return category
680+ raise KeyError('Category "{key}" does not exist.'.format(key=key))
681+
682+ def __len__(self):
683+ """
684+ Implement the __len__() method to make this class like a dictionary
685+ """
686+ return len(self.categories)
687+
688+ def __iter__(self):
689+ """
690+ Implement the __iter__() method to make this class like a dictionary
691+ """
692+ return self
693+
694+ def __next__(self):
695+ """
696+ Python 3 "next" method for iterator.
697+ """
698+ if self.index >= len(self.categories):
699+ self.index = 0
700+ raise StopIteration
701+ else:
702+ self.index += 1
703+ return self.categories[self.index - 1]
704+
705+ def __contains__(self, key):
706+ """
707+ Implement the __contains__() method to make this class like a dictionary
708+ """
709+ for category in self.categories:
710+ if category.name == key:
711+ return True
712+ return False
713+
714+ def append(self, name, actions=None):
715+ """
716+ Append a category
717+ """
718+ weight = 0
719+ if self.categories:
720+ weight = self.categories[-1].weight + 1
721+ self.add(name, weight, actions)
722+
723+ def add(self, name, weight=0, actions=None):
724+ """
725+ Add a category
726+ """
727+ category = ActionCategory(name, weight)
728+ if actions:
729+ for action in actions:
730+ if isinstance(action, tuple):
731+ category.actions.add(action[0], action[1])
732+ else:
733+ category.actions.append(action)
734+ self.categories.append(category)
735+ self.categories.sort(key=lambda cat: cat.weight)
736+
737+ def remove(self, name):
738+ """
739+ Remove a category
740+ """
741+ for category in self.categories:
742+ if category.name == name:
743+ self.categories.remove(category)
744+ return
745+ raise ValueError('Category "{name}" does not exist.'.format(name=name))
746+
747+
748+class ActionList(object):
749+ """
750+ The :class:`~openlp.core.utils.ActionList` class contains a list of menu actions and categories associated with
751+ those actions. Each category also has a weight by which it is sorted when iterating through the list of actions or
752+ categories.
753+ """
754+ instance = None
755+ shortcut_map = {}
756+
757+ def __init__(self):
758+ """
759+ Constructor
760+ """
761+ self.categories = CategoryList()
762+
763+ @staticmethod
764+ def get_instance():
765+ """
766+ Get the instance of this class.
767+ """
768+ if ActionList.instance is None:
769+ ActionList.instance = ActionList()
770+ return ActionList.instance
771+
772+ def add_action(self, action, category=None, weight=None):
773+ """
774+ Add an action to the list of actions.
775+
776+ **Note**: The action's objectName must be set when you want to add it!
777+
778+ :param action: The action to add (QAction). **Note**, the action must not have an empty ``objectName``.
779+ :param category: The category this action belongs to. The category has to be a python string. . **Note**,
780+ if the category is ``None``, the category and its actions are being hidden in the shortcut dialog. However,
781+ if they are added, it is possible to avoid assigning shortcuts twice, which is important.
782+ :param weight: The weight specifies how important a category is. However, this only has an impact on the order
783+ the categories are displayed.
784+ """
785+ if category not in self.categories:
786+ self.categories.append(category)
787+ settings = Settings()
788+ settings.beginGroup('shortcuts')
789+ # Get the default shortcut from the config.
790+ action.default_shortcuts = settings.get_default_value(action.objectName())
791+ if weight is None:
792+ self.categories[category].actions.append(action)
793+ else:
794+ self.categories[category].actions.add(action, weight)
795+ # Load the shortcut from the config.
796+ shortcuts = settings.value(action.objectName())
797+ settings.endGroup()
798+ if not shortcuts:
799+ action.setShortcuts([])
800+ return
801+ # We have to do this to ensure that the loaded shortcut list e. g. STRG+O (German) is converted to CTRL+O,
802+ # which is only done when we convert the strings in this way (QKeySequencet -> uncode).
803+ shortcuts = list(map(QtGui.QKeySequence.toString, list(map(QtGui.QKeySequence, shortcuts))))
804+ # Check the alternate shortcut first, to avoid problems when the alternate shortcut becomes the primary shortcut
805+ # after removing the (initial) primary shortcut due to conflicts.
806+ if len(shortcuts) == 2:
807+ existing_actions = ActionList.shortcut_map.get(shortcuts[1], [])
808+ # Check for conflicts with other actions considering the shortcut context.
809+ if self._is_shortcut_available(existing_actions, action):
810+ actions = ActionList.shortcut_map.get(shortcuts[1], [])
811+ actions.append(action)
812+ ActionList.shortcut_map[shortcuts[1]] = actions
813+ else:
814+ log.warning('Shortcut "{shortcut}" is removed from "{action}" because another '
815+ 'action already uses this shortcut.'.format(shortcut=shortcuts[1],
816+ action=action.objectName()))
817+ shortcuts.remove(shortcuts[1])
818+ # Check the primary shortcut.
819+ existing_actions = ActionList.shortcut_map.get(shortcuts[0], [])
820+ # Check for conflicts with other actions considering the shortcut context.
821+ if self._is_shortcut_available(existing_actions, action):
822+ actions = ActionList.shortcut_map.get(shortcuts[0], [])
823+ actions.append(action)
824+ ActionList.shortcut_map[shortcuts[0]] = actions
825+ else:
826+ log.warning('Shortcut "{shortcut}" is removed from "{action}" '
827+ 'because another action already uses this shortcut.'.format(shortcut=shortcuts[0],
828+ action=action.objectName()))
829+ shortcuts.remove(shortcuts[0])
830+ action.setShortcuts([QtGui.QKeySequence(shortcut) for shortcut in shortcuts])
831+
832+ def remove_action(self, action, category=None):
833+ """
834+ This removes an action from its category. Empty categories are automatically removed.
835+
836+ :param action: The ``QAction`` object to be removed.
837+ :param category: The name (unicode string) of the category, which contains the action. Defaults to None.
838+ """
839+ if category not in self.categories:
840+ return
841+ self.categories[category].actions.remove(action)
842+ # Remove empty categories.
843+ if not self.categories[category].actions:
844+ self.categories.remove(category)
845+ shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
846+ for shortcut in shortcuts:
847+ # Remove action from the list of actions which are using this shortcut.
848+ ActionList.shortcut_map[shortcut].remove(action)
849+ # Remove empty entries.
850+ if not ActionList.shortcut_map[shortcut]:
851+ del ActionList.shortcut_map[shortcut]
852+
853+ def add_category(self, name, weight):
854+ """
855+ Add an empty category to the list of categories. This is only convenient for categories with a given weight.
856+
857+ :param name: The category's name.
858+ :param weight: The category's weight (int).
859+ """
860+ if name in self.categories:
861+ # Only change the weight and resort the categories again.
862+ for category in self.categories:
863+ if category.name == name:
864+ category.weight = weight
865+ self.categories.categories.sort(key=lambda cat: cat.weight)
866+ return
867+ self.categories.add(name, weight)
868+
869+ def update_shortcut_map(self, action, old_shortcuts):
870+ """
871+ Remove the action for the given ``old_shortcuts`` from the ``shortcut_map`` to ensure its up-to-dateness.
872+ **Note**: The new action's shortcuts **must** be assigned to the given ``action`` **before** calling this
873+ method.
874+
875+ :param action: The action whose shortcuts are supposed to be updated in the ``shortcut_map``.
876+ :param old_shortcuts: A list of unicode key sequences.
877+ """
878+ for old_shortcut in old_shortcuts:
879+ # Remove action from the list of actions which are using this shortcut.
880+ ActionList.shortcut_map[old_shortcut].remove(action)
881+ # Remove empty entries.
882+ if not ActionList.shortcut_map[old_shortcut]:
883+ del ActionList.shortcut_map[old_shortcut]
884+ new_shortcuts = list(map(QtGui.QKeySequence.toString, action.shortcuts()))
885+ # Add the new shortcuts to the map.
886+ for new_shortcut in new_shortcuts:
887+ existing_actions = ActionList.shortcut_map.get(new_shortcut, [])
888+ existing_actions.append(action)
889+ ActionList.shortcut_map[new_shortcut] = existing_actions
890+
891+ def _is_shortcut_available(self, existing_actions, action):
892+ """
893+ Checks if the given ``action`` may use its assigned shortcut(s) or not. Returns ``True`` or ``False.
894+
895+ :param existing_actions: A list of actions which already use a particular shortcut.
896+ :param action: The action which wants to use a particular shortcut.
897+ """
898+ global_context = action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]
899+ affected_actions = []
900+ if global_context:
901+ affected_actions = [a for a in self.get_all_child_objects(action.parent()) if isinstance(a,
902+ QtWidgets.QAction)]
903+ for existing_action in existing_actions:
904+ if action is existing_action:
905+ continue
906+ if existing_action in affected_actions:
907+ return False
908+ if existing_action.shortcutContext() in [QtCore.Qt.WindowShortcut, QtCore.Qt.ApplicationShortcut]:
909+ return False
910+ elif action in self.get_all_child_objects(existing_action.parent()):
911+ return False
912+ return True
913+
914+ def get_all_child_objects(self, qobject):
915+ """
916+ Goes recursively through the children of ``qobject`` and returns a list of all child objects.
917+ """
918+ children = qobject.children()
919+ # Append the children's children.
920+ children.extend(list(map(self.get_all_child_objects, children)))
921+ return children
922+
923+
924+class CategoryOrder(object):
925+ """
926+ An enumeration class for category weights.
927+ """
928+ standard_menu = -20
929+ standard_toolbar = -10
930
931=== added file 'openlp/core/common/db.py'
932--- openlp/core/common/db.py 1970-01-01 00:00:00 +0000
933+++ openlp/core/common/db.py 2017-01-08 22:05:36 +0000
934@@ -0,0 +1,72 @@
935+# -*- coding: utf-8 -*-
936+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
937+
938+###############################################################################
939+# OpenLP - Open Source Lyrics Projection #
940+# --------------------------------------------------------------------------- #
941+# Copyright (c) 2008-2017 OpenLP Developers #
942+# --------------------------------------------------------------------------- #
943+# This program is free software; you can redistribute it and/or modify it #
944+# under the terms of the GNU General Public License as published by the Free #
945+# Software Foundation; version 2 of the License. #
946+# #
947+# This program is distributed in the hope that it will be useful, but WITHOUT #
948+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
949+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
950+# more details. #
951+# #
952+# You should have received a copy of the GNU General Public License along #
953+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
954+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
955+###############################################################################
956+"""
957+The :mod:`db` module provides helper functions for database related methods.
958+"""
959+import logging
960+from copy import deepcopy
961+
962+import sqlalchemy
963+
964+
965+log = logging.getLogger(__name__)
966+
967+
968+def drop_column(op, tablename, columnname):
969+ drop_columns(op, tablename, [columnname])
970+
971+
972+def drop_columns(op, tablename, columns):
973+ """
974+ Column dropping functionality for SQLite, as there is no DROP COLUMN support in SQLite
975+
976+ From https://github.com/klugjohannes/alembic-sqlite
977+ """
978+
979+ # get the db engine and reflect database tables
980+ engine = op.get_bind()
981+ meta = sqlalchemy.MetaData(bind=engine)
982+ meta.reflect()
983+
984+ # create a select statement from the old table
985+ old_table = meta.tables[tablename]
986+ select = sqlalchemy.sql.select([c for c in old_table.c if c.name not in columns])
987+
988+ # get remaining columns without table attribute attached
989+ remaining_columns = [deepcopy(c) for c in old_table.columns if c.name not in columns]
990+ for column in remaining_columns:
991+ column.table = None
992+
993+ # create a temporary new table
994+ new_tablename = '{0}_new'.format(tablename)
995+ op.create_table(new_tablename, *remaining_columns)
996+ meta.reflect()
997+ new_table = meta.tables[new_tablename]
998+
999+ # copy data from old table
1000+ insert = sqlalchemy.sql.insert(new_table).from_select([c.name for c in remaining_columns], select)
1001+ engine.execute(insert)
1002+
1003+ # drop the old table and rename the new table to take the old tables
1004+ # position
1005+ op.drop_table(tablename)
1006+ op.rename_table(new_tablename, tablename)
1007
1008=== renamed file 'openlp/core/common/historycombobox.py' => 'openlp/core/common/historycombobox.py.THIS'
1009=== added file 'openlp/core/common/httputils.py'
1010--- openlp/core/common/httputils.py 1970-01-01 00:00:00 +0000
1011+++ openlp/core/common/httputils.py 2017-01-08 22:05:36 +0000
1012@@ -0,0 +1,255 @@
1013+# -*- coding: utf-8 -*-
1014+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1015+
1016+###############################################################################
1017+# OpenLP - Open Source Lyrics Projection #
1018+# --------------------------------------------------------------------------- #
1019+# Copyright (c) 2008-2017 OpenLP Developers #
1020+# --------------------------------------------------------------------------- #
1021+# This program is free software; you can redistribute it and/or modify it #
1022+# under the terms of the GNU General Public License as published by the Free #
1023+# Software Foundation; version 2 of the License. #
1024+# #
1025+# This program is distributed in the hope that it will be useful, but WITHOUT #
1026+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1027+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1028+# more details. #
1029+# #
1030+# You should have received a copy of the GNU General Public License along #
1031+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1032+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1033+###############################################################################
1034+"""
1035+The :mod:`openlp.core.utils` module provides the utility libraries for OpenLP.
1036+"""
1037+import hashlib
1038+import logging
1039+import os
1040+import socket
1041+import sys
1042+import time
1043+import urllib.error
1044+import urllib.parse
1045+import urllib.request
1046+from http.client import HTTPException
1047+from random import randint
1048+
1049+from openlp.core.common import Registry, trace_error_handler
1050+
1051+log = logging.getLogger(__name__ + '.__init__')
1052+
1053+USER_AGENTS = {
1054+ 'win32': [
1055+ 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
1056+ 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36',
1057+ 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36'
1058+ ],
1059+ 'darwin': [
1060+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31 (KHTML, like Gecko) '
1061+ 'Chrome/26.0.1410.43 Safari/537.31',
1062+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/536.11 (KHTML, like Gecko) '
1063+ 'Chrome/20.0.1132.57 Safari/536.11',
1064+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.11 (KHTML, like Gecko) '
1065+ 'Chrome/20.0.1132.47 Safari/536.11',
1066+ ],
1067+ 'linux2': [
1068+ 'Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Ubuntu Chromium/25.0.1364.160 '
1069+ 'Chrome/25.0.1364.160 Safari/537.22',
1070+ 'Mozilla/5.0 (X11; CrOS armv7l 2913.260.0) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.99 '
1071+ 'Safari/537.11',
1072+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1389.0 Safari/537.27'
1073+ ],
1074+ 'default': [
1075+ 'Mozilla/5.0 (X11; NetBSD amd64; rv:18.0) Gecko/20130120 Firefox/18.0'
1076+ ]
1077+}
1078+CONNECTION_TIMEOUT = 30
1079+CONNECTION_RETRIES = 2
1080+
1081+
1082+class HTTPRedirectHandlerFixed(urllib.request.HTTPRedirectHandler):
1083+ """
1084+ Special HTTPRedirectHandler used to work around http://bugs.python.org/issue22248
1085+ (Redirecting to urls with special chars)
1086+ """
1087+ def redirect_request(self, req, fp, code, msg, headers, new_url):
1088+ #
1089+ """
1090+ Test if the new_url can be decoded to ascii
1091+
1092+ :param req:
1093+ :param fp:
1094+ :param code:
1095+ :param msg:
1096+ :param headers:
1097+ :param new_url:
1098+ :return:
1099+ """
1100+ try:
1101+ new_url.encode('latin1').decode('ascii')
1102+ fixed_url = new_url
1103+ except Exception:
1104+ # The url could not be decoded to ascii, so we do some url encoding
1105+ fixed_url = urllib.parse.quote(new_url.encode('latin1').decode('utf-8', 'replace'), safe='/:')
1106+ return super(HTTPRedirectHandlerFixed, self).redirect_request(req, fp, code, msg, headers, fixed_url)
1107+
1108+
1109+def get_user_agent():
1110+ """
1111+ Return a user agent customised for the platform the user is on.
1112+ """
1113+ browser_list = USER_AGENTS.get(sys.platform, None)
1114+ if not browser_list:
1115+ browser_list = USER_AGENTS['default']
1116+ random_index = randint(0, len(browser_list) - 1)
1117+ return browser_list[random_index]
1118+
1119+
1120+def get_web_page(url, header=None, update_openlp=False):
1121+ """
1122+ Attempts to download the webpage at url and returns that page or None.
1123+
1124+ :param url: The URL to be downloaded.
1125+ :param header: An optional HTTP header to pass in the request to the web server.
1126+ :param update_openlp: Tells OpenLP to update itself if the page is successfully downloaded.
1127+ Defaults to False.
1128+ """
1129+ # TODO: Add proxy usage. Get proxy info from OpenLP settings, add to a
1130+ # proxy_handler, build into an opener and install the opener into urllib2.
1131+ # http://docs.python.org/library/urllib2.html
1132+ if not url:
1133+ return None
1134+ # This is needed to work around http://bugs.python.org/issue22248 and https://bugs.launchpad.net/openlp/+bug/1251437
1135+ opener = urllib.request.build_opener(HTTPRedirectHandlerFixed())
1136+ urllib.request.install_opener(opener)
1137+ req = urllib.request.Request(url)
1138+ if not header or header[0].lower() != 'user-agent':
1139+ user_agent = get_user_agent()
1140+ req.add_header('User-Agent', user_agent)
1141+ if header:
1142+ req.add_header(header[0], header[1])
1143+ log.debug('Downloading URL = %s' % url)
1144+ retries = 0
1145+ while retries <= CONNECTION_RETRIES:
1146+ retries += 1
1147+ time.sleep(0.1)
1148+ try:
1149+ page = urllib.request.urlopen(req, timeout=CONNECTION_TIMEOUT)
1150+ log.debug('Downloaded page {text}'.format(text=page.geturl()))
1151+ break
1152+ except urllib.error.URLError as err:
1153+ log.exception('URLError on {text}'.format(text=url))
1154+ log.exception('URLError: {text}'.format(text=err.reason))
1155+ page = None
1156+ if retries > CONNECTION_RETRIES:
1157+ raise
1158+ except socket.timeout:
1159+ log.exception('Socket timeout: {text}'.format(text=url))
1160+ page = None
1161+ if retries > CONNECTION_RETRIES:
1162+ raise
1163+ except socket.gaierror:
1164+ log.exception('Socket gaierror: {text}'.format(text=url))
1165+ page = None
1166+ if retries > CONNECTION_RETRIES:
1167+ raise
1168+ except ConnectionRefusedError:
1169+ log.exception('ConnectionRefused: {text}'.format(text=url))
1170+ page = None
1171+ if retries > CONNECTION_RETRIES:
1172+ raise
1173+ break
1174+ except ConnectionError:
1175+ log.exception('Connection error: {text}'.format(text=url))
1176+ page = None
1177+ if retries > CONNECTION_RETRIES:
1178+ raise
1179+ except HTTPException:
1180+ log.exception('HTTPException error: {text}'.format(text=url))
1181+ page = None
1182+ if retries > CONNECTION_RETRIES:
1183+ raise
1184+ except:
1185+ # Don't know what's happening, so reraise the original
1186+ raise
1187+ if update_openlp:
1188+ Registry().get('application').process_events()
1189+ if not page:
1190+ log.exception('{text} could not be downloaded'.format(text=url))
1191+ return None
1192+ log.debug(page)
1193+ return page
1194+
1195+
1196+def get_url_file_size(url):
1197+ """
1198+ Get the size of a file.
1199+
1200+ :param url: The URL of the file we want to download.
1201+ """
1202+ retries = 0
1203+ while True:
1204+ try:
1205+ site = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
1206+ meta = site.info()
1207+ return int(meta.get("Content-Length"))
1208+ except urllib.error.URLError:
1209+ if retries > CONNECTION_RETRIES:
1210+ raise
1211+ else:
1212+ retries += 1
1213+ time.sleep(0.1)
1214+ continue
1215+
1216+
1217+def url_get_file(callback, url, f_path, sha256=None):
1218+ """"
1219+ Download a file given a URL. The file is retrieved in chunks, giving the ability to cancel the download at any
1220+ point. Returns False on download error.
1221+
1222+ :param callback: the class which needs to be updated
1223+ :param url: URL to download
1224+ :param f_path: Destination file
1225+ :param sha256: The check sum value to be checked against the download value
1226+ """
1227+ block_count = 0
1228+ block_size = 4096
1229+ retries = 0
1230+ while True:
1231+ try:
1232+ filename = open(f_path, "wb")
1233+ url_file = urllib.request.urlopen(url, timeout=CONNECTION_TIMEOUT)
1234+ if sha256:
1235+ hasher = hashlib.sha256()
1236+ # Download until finished or canceled.
1237+ while not callback.was_cancelled:
1238+ data = url_file.read(block_size)
1239+ if not data:
1240+ break
1241+ filename.write(data)
1242+ if sha256:
1243+ hasher.update(data)
1244+ block_count += 1
1245+ callback._download_progress(block_count, block_size)
1246+ filename.close()
1247+ if sha256 and hasher.hexdigest() != sha256:
1248+ log.error('sha256 sums did not match for file: {file}'.format(file=f_path))
1249+ os.remove(f_path)
1250+ return False
1251+ except (urllib.error.URLError, socket.timeout) as err:
1252+ trace_error_handler(log)
1253+ filename.close()
1254+ os.remove(f_path)
1255+ if retries > CONNECTION_RETRIES:
1256+ return False
1257+ else:
1258+ retries += 1
1259+ time.sleep(0.1)
1260+ continue
1261+ break
1262+ # Delete file if cancelled, it may be a partial file.
1263+ if callback.was_cancelled:
1264+ os.remove(f_path)
1265+ return True
1266+
1267+__all__ = ['get_web_page']
1268
1269=== added file 'openlp/core/common/languagemanager.py'
1270--- openlp/core/common/languagemanager.py 1970-01-01 00:00:00 +0000
1271+++ openlp/core/common/languagemanager.py 2017-01-08 22:05:36 +0000
1272@@ -0,0 +1,207 @@
1273+# -*- coding: utf-8 -*-
1274+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1275+
1276+###############################################################################
1277+# OpenLP - Open Source Lyrics Projection #
1278+# --------------------------------------------------------------------------- #
1279+# Copyright (c) 2008-2017 OpenLP Developers #
1280+# --------------------------------------------------------------------------- #
1281+# This program is free software; you can redistribute it and/or modify it #
1282+# under the terms of the GNU General Public License as published by the Free #
1283+# Software Foundation; version 2 of the License. #
1284+# #
1285+# This program is distributed in the hope that it will be useful, but WITHOUT #
1286+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1287+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1288+# more details. #
1289+# #
1290+# You should have received a copy of the GNU General Public License along #
1291+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1292+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1293+###############################################################################
1294+"""
1295+The :mod:`languagemanager` module provides all the translation settings and language file loading for OpenLP.
1296+"""
1297+import locale
1298+import logging
1299+import re
1300+
1301+from PyQt5 import QtCore, QtWidgets
1302+
1303+
1304+from openlp.core.common import AppLocation, Settings, translate, is_win, is_macosx
1305+
1306+log = logging.getLogger(__name__)
1307+
1308+ICU_COLLATOR = None
1309+DIGITS_OR_NONDIGITS = re.compile(r'\d+|\D+', re.UNICODE)
1310+
1311+
1312+class LanguageManager(object):
1313+ """
1314+ Helper for Language selection
1315+ """
1316+ __qm_list__ = {}
1317+ auto_language = False
1318+
1319+ @staticmethod
1320+ def get_translator(language):
1321+ """
1322+ Set up a translator to use in this instance of OpenLP
1323+
1324+ :param language: The language to load into the translator
1325+ """
1326+ if LanguageManager.auto_language:
1327+ language = QtCore.QLocale.system().name()
1328+ lang_path = AppLocation.get_directory(AppLocation.LanguageDir)
1329+ app_translator = QtCore.QTranslator()
1330+ app_translator.load(language, lang_path)
1331+ # A translator for buttons and other default strings provided by Qt.
1332+ if not is_win() and not is_macosx():
1333+ lang_path = QtCore.QLibraryInfo.location(QtCore.QLibraryInfo.TranslationsPath)
1334+ default_translator = QtCore.QTranslator()
1335+ default_translator.load('qt_%s' % language, lang_path)
1336+ return app_translator, default_translator
1337+
1338+ @staticmethod
1339+ def find_qm_files():
1340+ """
1341+ Find all available language files in this OpenLP install
1342+ """
1343+ log.debug('Translation files: {files}'.format(files=AppLocation.get_directory(AppLocation.LanguageDir)))
1344+ trans_dir = QtCore.QDir(AppLocation.get_directory(AppLocation.LanguageDir))
1345+ file_names = trans_dir.entryList(['*.qm'], QtCore.QDir.Files, QtCore.QDir.Name)
1346+ # Remove qm files from the list which start with "qt_".
1347+ file_names = [file_ for file_ in file_names if not file_.startswith('qt_')]
1348+ return list(map(trans_dir.filePath, file_names))
1349+
1350+ @staticmethod
1351+ def language_name(qm_file):
1352+ """
1353+ Load the language name from a language file
1354+
1355+ :param qm_file: The file to obtain the name from
1356+ """
1357+ translator = QtCore.QTranslator()
1358+ translator.load(qm_file)
1359+ return translator.translate('OpenLP.MainWindow', 'English', 'Please add the name of your language here')
1360+
1361+ @staticmethod
1362+ def get_language():
1363+ """
1364+ Retrieve a saved language to use from settings
1365+ """
1366+ language = Settings().value('core/language')
1367+ language = str(language)
1368+ log.info("Language file: '{language}' Loaded from conf file".format(language=language))
1369+ if re.match(r'[[].*[]]', language):
1370+ LanguageManager.auto_language = True
1371+ language = re.sub(r'[\[\]]', '', language)
1372+ return language
1373+
1374+ @staticmethod
1375+ def set_language(action, message=True):
1376+ """
1377+ Set the language to translate OpenLP into
1378+
1379+ :param action: The language menu option
1380+ :param message: Display the message option
1381+ """
1382+ language = 'en'
1383+ if action:
1384+ action_name = str(action.objectName())
1385+ if action_name == 'autoLanguageItem':
1386+ LanguageManager.auto_language = True
1387+ else:
1388+ LanguageManager.auto_language = False
1389+ qm_list = LanguageManager.get_qm_list()
1390+ language = str(qm_list[action_name])
1391+ if LanguageManager.auto_language:
1392+ language = '[{language}]'.format(language=language)
1393+ Settings().setValue('core/language', language)
1394+ log.info("Language file: '{language}' written to conf file".format(language=language))
1395+ if message:
1396+ QtWidgets.QMessageBox.information(None,
1397+ translate('OpenLP.LanguageManager', 'Language'),
1398+ translate('OpenLP.LanguageManager',
1399+ 'Please restart OpenLP to use your new language setting.'))
1400+
1401+ @staticmethod
1402+ def init_qm_list():
1403+ """
1404+ Initialise the list of available translations
1405+ """
1406+ LanguageManager.__qm_list__ = {}
1407+ qm_files = LanguageManager.find_qm_files()
1408+ for counter, qmf in enumerate(qm_files):
1409+ reg_ex = QtCore.QRegExp("^.*i18n/(.*).qm")
1410+ if reg_ex.exactMatch(qmf):
1411+ name = '{regex}'.format(regex=reg_ex.cap(1))
1412+ # TODO: Test before converting to python3 string format
1413+ LanguageManager.__qm_list__['%#2i %s' % (counter + 1, LanguageManager.language_name(qmf))] = name
1414+
1415+ @staticmethod
1416+ def get_qm_list():
1417+ """
1418+ Return the list of available translations
1419+ """
1420+ if not LanguageManager.__qm_list__:
1421+ LanguageManager.init_qm_list()
1422+ return LanguageManager.__qm_list__
1423+
1424+
1425+def format_time(text, local_time):
1426+ """
1427+ Workaround for Python built-in time formatting function time.strftime().
1428+
1429+ time.strftime() accepts only ascii characters. This function accepts
1430+ unicode string and passes individual % placeholders to time.strftime().
1431+ This ensures only ascii characters are passed to time.strftime().
1432+
1433+ :param text: The text to be processed.
1434+ :param local_time: The time to be used to add to the string. This is a time object
1435+ """
1436+
1437+ def match_formatting(match):
1438+ """
1439+ Format the match
1440+ """
1441+ return local_time.strftime(match.group())
1442+
1443+ return re.sub(r'\%[a-zA-Z]', match_formatting, text)
1444+
1445+
1446+def get_locale_key(string):
1447+ """
1448+ Creates a key for case insensitive, locale aware string sorting.
1449+
1450+ :param string: The corresponding string.
1451+ """
1452+ string = string.lower()
1453+ # ICU is the prefered way to handle locale sort key, we fallback to locale.strxfrm which will work in most cases.
1454+ global ICU_COLLATOR
1455+ try:
1456+ if ICU_COLLATOR is None:
1457+ import icu
1458+ language = LanguageManager.get_language()
1459+ icu_locale = icu.Locale(language)
1460+ ICU_COLLATOR = icu.Collator.createInstance(icu_locale)
1461+ return ICU_COLLATOR.getSortKey(string)
1462+ except:
1463+ return locale.strxfrm(string).encode()
1464+
1465+
1466+def get_natural_key(string):
1467+ """
1468+ Generate a key for locale aware natural string sorting.
1469+
1470+ :param string: string to be sorted by
1471+ Returns a list of string compare keys and integers.
1472+ """
1473+ key = DIGITS_OR_NONDIGITS.findall(string)
1474+ key = [int(part) if part.isdigit() else get_locale_key(part) for part in key]
1475+ # Python 3 does not support comparison of different types anymore. So make sure, that we do not compare str
1476+ # and int.
1477+ if string and string[0].isdigit():
1478+ return [b''] + key
1479+ return key
1480
1481=== added file 'openlp/core/common/languages.py'
1482--- openlp/core/common/languages.py 1970-01-01 00:00:00 +0000
1483+++ openlp/core/common/languages.py 2017-01-08 22:05:36 +0000
1484@@ -0,0 +1,201 @@
1485+# -*- coding: utf-8 -*-
1486+# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4
1487+
1488+###############################################################################
1489+# OpenLP - Open Source Lyrics Projection #
1490+# --------------------------------------------------------------------------- #
1491+# Copyright (c) 2008-2017 OpenLP Developers #
1492+# --------------------------------------------------------------------------- #
1493+# This program is free software; you can redistribute it and/or modify it #
1494+# under the terms of the GNU General Public License as published by the Free #
1495+# Software Foundation; version 2 of the License. #
1496+# #
1497+# This program is distributed in the hope that it will be useful, but WITHOUT #
1498+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
1499+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
1500+# more details. #
1501+# #
1502+# You should have received a copy of the GNU General Public License along #
1503+# with this program; if not, write to the Free Software Foundation, Inc., 59 #
1504+# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
1505+###############################################################################
1506+"""
1507+The :mod:`languages` module provides a list of language names with utility functions.
1508+"""
1509+from collections import namedtuple
1510+
1511+from openlp.core.common import translate
1512+
1513+
1514+Language = namedtuple('Language', ['id', 'name', 'code'])
1515+languages = sorted([
1516+ Language(1, translate('common.languages', '(Afan) Oromo', 'Language code: om'), 'om'),
1517+ Language(2, translate('common.languages', 'Abkhazian', 'Language code: ab'), 'ab'),
1518+ Language(3, translate('common.languages', 'Afar', 'Language code: aa'), 'aa'),
1519+ Language(4, translate('common.languages', 'Afrikaans', 'Language code: af'), 'af'),
1520+ Language(5, translate('common.languages', 'Albanian', 'Language code: sq'), 'sq'),
1521+ Language(6, translate('common.languages', 'Amharic', 'Language code: am'), 'am'),
1522+ Language(140, translate('common.languages', 'Amuzgo', 'Language code: amu'), 'amu'),
1523+ Language(152, translate('common.languages', 'Ancient Greek', 'Language code: grc'), 'grc'),
1524+ Language(7, translate('common.languages', 'Arabic', 'Language code: ar'), 'ar'),
1525+ Language(8, translate('common.languages', 'Armenian', 'Language code: hy'), 'hy'),
1526+ Language(9, translate('common.languages', 'Assamese', 'Language code: as'), 'as'),
1527+ Language(10, translate('common.languages', 'Aymara', 'Language code: ay'), 'ay'),
1528+ Language(11, translate('common.languages', 'Azerbaijani', 'Language code: az'), 'az'),
1529+ Language(12, translate('common.languages', 'Bashkir', 'Language code: ba'), 'ba'),
1530+ Language(13, translate('common.languages', 'Basque', 'Language code: eu'), 'eu'),
1531+ Language(14, translate('common.languages', 'Bengali', 'Language code: bn'), 'bn'),
1532+ Language(15, translate('common.languages', 'Bhutani', 'Language code: dz'), 'dz'),
1533+ Language(16, translate('common.languages', 'Bihari', 'Language code: bh'), 'bh'),
1534+ Language(17, translate('common.languages', 'Bislama', 'Language code: bi'), 'bi'),
1535+ Language(18, translate('common.languages', 'Breton', 'Language code: br'), 'br'),
1536+ Language(19, translate('common.languages', 'Bulgarian', 'Language code: bg'), 'bg'),
1537+ Language(20, translate('common.languages', 'Burmese', 'Language code: my'), 'my'),
1538+ Language(21, translate('common.languages', 'Byelorussian', 'Language code: be'), 'be'),
1539+ Language(142, translate('common.languages', 'Cakchiquel', 'Language code: cak'), 'cak'),
1540+ Language(22, translate('common.languages', 'Cambodian', 'Language code: km'), 'km'),
1541+ Language(23, translate('common.languages', 'Catalan', 'Language code: ca'), 'ca'),
1542+ Language(24, translate('common.languages', 'Chinese', 'Language code: zh'), 'zh'),
1543+ Language(141, translate('common.languages', 'Comaltepec Chinantec', 'Language code: cco'), 'cco'),
1544+ Language(25, translate('common.languages', 'Corsican', 'Language code: co'), 'co'),
1545+ Language(26, translate('common.languages', 'Croatian', 'Language code: hr'), 'hr'),
1546+ Language(27, translate('common.languages', 'Czech', 'Language code: cs'), 'cs'),
1547+ Language(28, translate('common.languages', 'Danish', 'Language code: da'), 'da'),
1548+ Language(29, translate('common.languages', 'Dutch', 'Language code: nl'), 'nl'),
1549+ Language(30, translate('common.languages', 'English', 'Language code: en'), 'en'),
1550+ Language(31, translate('common.languages', 'Esperanto', 'Language code: eo'), 'eo'),
1551+ Language(32, translate('common.languages', 'Estonian', 'Language code: et'), 'et'),
1552+ Language(33, translate('common.languages', 'Faeroese', 'Language code: fo'), 'fo'),
1553+ Language(34, translate('common.languages', 'Fiji', 'Language code: fj'), 'fj'),
1554+ Language(35, translate('common.languages', 'Finnish', 'Language code: fi'), 'fi'),
1555+ Language(36, translate('common.languages', 'French', 'Language code: fr'), 'fr'),
1556+ Language(37, translate('common.languages', 'Frisian', 'Language code: fy'), 'fy'),
1557+ Language(38, translate('common.languages', 'Galician', 'Language code: gl'), 'gl'),
1558+ Language(39, translate('common.languages', 'Georgian', 'Language code: ka'), 'ka'),
1559+ Language(40, translate('common.languages', 'German', 'Language code: de'), 'de'),
1560+ Language(41, translate('common.languages', 'Greek', 'Language code: el'), 'el'),
1561+ Language(42, translate('common.languages', 'Greenlandic', 'Language code: kl'), 'kl'),
1562+ Language(43, translate('common.languages', 'Guarani', 'Language code: gn'), 'gn'),
1563+ Language(44, translate('common.languages', 'Gujarati', 'Language code: gu'), 'gu'),
1564+ Language(143, translate('common.languages', 'Haitian Creole', 'Language code: ht'), 'ht'),
1565+ Language(45, translate('common.languages', 'Hausa', 'Language code: ha'), 'ha'),
1566+ Language(46, translate('common.languages', 'Hebrew (former iw)', 'Language code: he'), 'he'),
1567+ Language(144, translate('common.languages', 'Hiligaynon', 'Language code: hil'), 'hil'),
1568+ Language(47, translate('common.languages', 'Hindi', 'Language code: hi'), 'hi'),
1569+ Language(48, translate('common.languages', 'Hungarian', 'Language code: hu'), 'hu'),
1570+ Language(49, translate('common.languages', 'Icelandic', 'Language code: is'), 'is'),
1571+ Language(50, translate('common.languages', 'Indonesian (former in)', 'Language code: id'), 'id'),
1572+ Language(51, translate('common.languages', 'Interlingua', 'Language code: ia'), 'ia'),
1573+ Language(52, translate('common.languages', 'Interlingue', 'Language code: ie'), 'ie'),
1574+ Language(54, translate('common.languages', 'Inuktitut (Eskimo)', 'Language code: iu'), 'iu'),
1575+ Language(53, translate('common.languages', 'Inupiak', 'Language code: ik'), 'ik'),
1576+ Language(55, translate('common.languages', 'Irish', 'Language code: ga'), 'ga'),
1577+ Language(56, translate('common.languages', 'Italian', 'Language code: it'), 'it'),
1578+ Language(145, translate('common.languages', 'Jakalteko', 'Language code: jac'), 'jac'),
1579+ Language(57, translate('common.languages', 'Japanese', 'Language code: ja'), 'ja'),
1580+ Language(58, translate('common.languages', 'Javanese', 'Language code: jw'), 'jw'),
1581+ Language(150, translate('common.languages', 'K\'iche\'', 'Language code: quc'), 'quc'),
1582+ Language(59, translate('common.languages', 'Kannada', 'Language code: kn'), 'kn'),
1583+ Language(60, translate('common.languages', 'Kashmiri', 'Language code: ks'), 'ks'),
1584+ Language(61, translate('common.languages', 'Kazakh', 'Language code: kk'), 'kk'),
1585+ Language(146, translate('common.languages', 'Kekchí ', 'Language code: kek'), 'kek'),
1586+ Language(62, translate('common.languages', 'Kinyarwanda', 'Language code: rw'), 'rw'),
1587+ Language(63, translate('common.languages', 'Kirghiz', 'Language code: ky'), 'ky'),
1588+ Language(64, translate('common.languages', 'Kirundi', 'Language code: rn'), 'rn'),
1589+ Language(65, translate('common.languages', 'Korean', 'Language code: ko'), 'ko'),
1590+ Language(66, translate('common.languages', 'Kurdish', 'Language code: ku'), 'ku'),
1591+ Language(67, translate('common.languages', 'Laothian', 'Language code: lo'), 'lo'),
1592+ Language(68, translate('common.languages', 'Latin', 'Language code: la'), 'la'),
1593+ Language(69, translate('common.languages', 'Latvian, Lettish', 'Language code: lv'), 'lv'),
1594+ Language(70, translate('common.languages', 'Lingala', 'Language code: ln'), 'ln'),
1595+ Language(71, translate('common.languages', 'Lithuanian', 'Language code: lt'), 'lt'),
1596+ Language(72, translate('common.languages', 'Macedonian', 'Language code: mk'), 'mk'),
1597+ Language(73, translate('common.languages', 'Malagasy', 'Language code: mg'), 'mg'),
1598+ Language(74, translate('common.languages', 'Malay', 'Language code: ms'), 'ms'),
1599+ Language(75, translate('common.languages', 'Malayalam', 'Language code: ml'), 'ml'),
1600+ Language(76, translate('common.languages', 'Maltese', 'Language code: mt'), 'mt'),
1601+ Language(148, translate('common.languages', 'Mam', 'Language code: mam'), 'mam'),
1602+ Language(77, translate('common.languages', 'Maori', 'Language code: mi'), 'mi'),
1603+ Language(147, translate('common.languages', 'Maori', 'Language code: mri'), 'mri'),
1604+ Language(78, translate('common.languages', 'Marathi', 'Language code: mr'), 'mr'),
1605+ Language(79, translate('common.languages', 'Moldavian', 'Language code: mo'), 'mo'),
1606+ Language(80, translate('common.languages', 'Mongolian', 'Language code: mn'), 'mn'),
1607+ Language(149, translate('common.languages', 'Nahuatl', 'Language code: nah'), 'nah'),
1608+ Language(81, translate('common.languages', 'Nauru', 'Language code: na'), 'na'),
1609+ Language(82, translate('common.languages', 'Nepali', 'Language code: ne'), 'ne'),
1610+ Language(83, translate('common.languages', 'Norwegian', 'Language code: no'), 'no'),
1611+ Language(84, translate('common.languages', 'Occitan', 'Language code: oc'), 'oc'),
1612+ Language(85, translate('common.languages', 'Oriya', 'Language code: or'), 'or'),
1613+ Language(86, translate('common.languages', 'Pashto, Pushto', 'Language code: ps'), 'ps'),
1614+ Language(87, translate('common.languages', 'Persian', 'Language code: fa'), 'fa'),
1615+ Language(151, translate('common.languages', 'Plautdietsch', 'Language code: pdt'), 'pdt'),
1616+ Language(88, translate('common.languages', 'Polish', 'Language code: pl'), 'pl'),
1617+ Language(89, translate('common.languages', 'Portuguese', 'Language code: pt'), 'pt'),
1618+ Language(90, translate('common.languages', 'Punjabi', 'Language code: pa'), 'pa'),
1619+ Language(91, translate('common.languages', 'Quechua', 'Language code: qu'), 'qu'),
1620+ Language(92, translate('common.languages', 'Rhaeto-Romance', 'Language code: rm'), 'rm'),
1621+ Language(93, translate('common.languages', 'Romanian', 'Language code: ro'), 'ro'),
1622+ Language(94, translate('common.languages', 'Russian', 'Language code: ru'), 'ru'),
1623+ Language(95, translate('common.languages', 'Samoan', 'Language code: sm'), 'sm'),
1624+ Language(96, translate('common.languages', 'Sangro', 'Language code: sg'), 'sg'),
1625+ Language(97, translate('common.languages', 'Sanskrit', 'Language code: sa'), 'sa'),
1626+ Language(98, translate('common.languages', 'Scots Gaelic', 'Language code: gd'), 'gd'),
1627+ Language(99, translate('common.languages', 'Serbian', 'Language code: sr'), 'sr'),
1628+ Language(100, translate('common.languages', 'Serbo-Croatian', 'Language code: sh'), 'sh'),
1629+ Language(101, translate('common.languages', 'Sesotho', 'Language code: st'), 'st'),
1630+ Language(102, translate('common.languages', 'Setswana', 'Language code: tn'), 'tn'),
1631+ Language(103, translate('common.languages', 'Shona', 'Language code: sn'), 'sn'),
1632+ Language(104, translate('common.languages', 'Sindhi', 'Language code: sd'), 'sd'),
1633+ Language(105, translate('common.languages', 'Singhalese', 'Language code: si'), 'si'),
1634+ Language(106, translate('common.languages', 'Siswati', 'Language code: ss'), 'ss'),
1635+ Language(107, translate('common.languages', 'Slovak', 'Language code: sk'), 'sk'),
1636+ Language(108, translate('common.languages', 'Slovenian', 'Language code: sl'), 'sl'),
1637+ Language(109, translate('common.languages', 'Somali', 'Language code: so'), 'so'),
1638+ Language(110, translate('common.languages', 'Spanish', 'Language code: es'), 'es'),
1639+ Language(111, translate('common.languages', 'Sudanese', 'Language code: su'), 'su'),
1640+ Language(112, translate('common.languages', 'Swahili', 'Language code: sw'), 'sw'),
1641+ Language(113, translate('common.languages', 'Swedish', 'Language code: sv'), 'sv'),
1642+ Language(114, translate('common.languages', 'Tagalog', 'Language code: tl'), 'tl'),
1643+ Language(115, translate('common.languages', 'Tajik', 'Language code: tg'), 'tg'),
1644+ Language(116, translate('common.languages', 'Tamil', 'Language code: ta'), 'ta'),
1645+ Language(117, translate('common.languages', 'Tatar', 'Language code: tt'), 'tt'),
1646+ Language(118, translate('common.languages', 'Tegulu', 'Language code: te'), 'te'),
1647+ Language(119, translate('common.languages', 'Thai', 'Language code: th'), 'th'),
1648+ Language(120, translate('common.languages', 'Tibetan', 'Language code: bo'), 'bo'),
1649+ Language(121, translate('common.languages', 'Tigrinya', 'Language code: ti'), 'ti'),
1650+ Language(122, translate('common.languages', 'Tonga', 'Language code: to'), 'to'),
1651+ Language(123, translate('common.languages', 'Tsonga', 'Language code: ts'), 'ts'),
1652+ Language(124, translate('common.languages', 'Turkish', 'Language code: tr'), 'tr'),
1653+ Language(125, translate('common.languages', 'Turkmen', 'Language code: tk'), 'tk'),
1654+ Language(126, translate('common.languages', 'Twi', 'Language code: tw'), 'tw'),
1655+ Language(127, translate('common.languages', 'Uigur', 'Language code: ug'), 'ug'),
1656+ Language(128, translate('common.languages', 'Ukrainian', 'Language code: uk'), 'uk'),
1657+ Language(129, translate('common.languages', 'Urdu', 'Language code: ur'), 'ur'),
1658+ Language(153, translate('common.languages', 'Uspanteco', 'Language code: usp'), 'usp'),
1659+ Language(130, translate('common.languages', 'Uzbek', 'Language code: uz'), 'uz'),
1660+ Language(131, translate('common.languages', 'Vietnamese', 'Language code: vi'), 'vi'),
1661+ Language(132, translate('common.languages', 'Volapuk', 'Language code: vo'), 'vo'),
1662+ Language(133, translate('common.languages', 'Welch', 'Language code: cy'), 'cy'),
1663+ Language(134, translate('common.languages', 'Wolof', 'Language code: wo'), 'wo'),
1664+ Language(135, translate('common.languages', 'Xhosa', 'Language code: xh'), 'xh'),
1665+ Language(136, translate('common.languages', 'Yiddish (former ji)', 'Language code: yi'), 'yi'),
1666+ Language(137, translate('common.languages', 'Yoruba', 'Language code: yo'), 'yo'),
1667+ Language(138, translate('common.languages', 'Zhuang', 'Language code: za'), 'za'),
1668+ Language(139, translate('common.languages', 'Zulu', 'Language code: zu'), 'zu')
1669+], key=lambda language: language.name)
1670+
1671+
1672+def get_language(name):
1673+ """
1674+ Find the language by its name or code.
1675+
1676+ :param name: The name or abbreviation of the language.
1677+ :return: The first match as a Language namedtuple or None
1678+ """
1679+ if name:
1680+ name_lower = name.lower()
1681+ name_title = name_lower[:1].upper() + name_lower[1:]
1682+ for language in languages:
1683+ if language.name == name_title or language.code == name_lower:
1684+ return language
1685+ return None
1686
1687=== modified file 'openlp/core/common/openlpmixin.py'
1688--- openlp/core/common/openlpmixin.py 2016-12-31 11:05:48 +0000
1689+++ openlp/core/common/openlpmixin.py 2017-01-08 22:05:36 +0000
1690@@ -49,12 +49,13 @@
1691 Code to added debug wrapper to work on called functions within a decorated class.
1692 """
1693 def wrapped(*args, **kwargs):
1694- parent.logger.debug("Entering %s" % func.__name__)
1695+ parent.logger.debug("Entering {function}".format(function=func.__name__))
1696 try:
1697 return func(*args, **kwargs)
1698 except Exception as e:
1699 if parent.logger.getEffectiveLevel() <= logging.ERROR:
1700- parent.logger.error('Exception in %s : %s' % (func.__name__, e))
1701+ parent.logger.error('Exception in {function} : {error}'.format(function=func.__name__,
1702+ error=e))
1703 raise e
1704 return wrapped
1705
1706@@ -70,6 +71,12 @@
1707 """
1708 self.logger.info(message)
1709
1710+ def log_warning(self, message):
1711+ """
1712+ Common log warning handler
1713+ """
1714+ self.logger.warning(message)
1715+
1716 def log_error(self, message):
1717 """
1718 Common log error handler which prints the calling path
1719
1720=== modified file 'openlp/core/common/registry.py'
1721--- openlp/core/common/registry.py 2016-12-31 11:05:48 +0000
1722+++ openlp/core/common/registry.py 2017-01-08 22:05:36 +0000
1723@@ -55,6 +55,7 @@
1724 registry = cls()
1725 registry.service_list = {}
1726 registry.functions_list = {}
1727+ registry.working_flags = {}
1728 # Allow the tests to remove Registry entries but not the live system
1729 registry.running_under_test = 'nose' in sys.argv[0]
1730 registry.initialising = True
1731@@ -71,8 +72,8 @@
1732 else:
1733 if not self.initialising:
1734 trace_error_handler(log)
1735- log.error('Service %s not found in list' % key)
1736- raise KeyError('Service %s not found in list' % key)
1737+ log.error('Service {key} not found in list'.format(key=key))
1738+ raise KeyError('Service {key} not found in list'.format(key=key))
1739
1740 def register(self, key, reference):
1741 """
1742@@ -83,15 +84,14 @@
1743 """
1744 if key in self.service_list:
1745 trace_error_handler(log)
1746- log.error('Duplicate service exception %s' % key)
1747- raise KeyError('Duplicate service exception %s' % key)
1748+ log.error('Duplicate service exception {key}'.format(key=key))
1749+ raise KeyError('Duplicate service exception {key}'.format(key=key))
1750 else:
1751 self.service_list[key] = reference
1752
1753 def remove(self, key):
1754 """
1755- Removes the registry value from the list based on the key passed in (Only valid and active for testing
1756- framework).
1757+ Removes the registry value from the list based on the key passed in.
1758
1759 :param key: The service to be deleted.
1760 """
1761@@ -140,8 +140,39 @@
1762 except TypeError:
1763 # Who has called me can help in debugging
1764 trace_error_handler(log)
1765- log.exception('Exception for function %s', function)
1766+ log.exception('Exception for function {function}'.format(function=function))
1767 else:
1768 trace_error_handler(log)
1769- log.error("Event %s called but not registered" % event)
1770+ log.error("Event {event} called but not registered".format(event=event))
1771 return results
1772+
1773+ def get_flag(self, key):
1774+ """
1775+ Extracts the working_flag value from the list based on the key passed in
1776+
1777+ :param key: The flag to be retrieved.
1778+ """
1779+ if key in self.working_flags:
1780+ return self.working_flags[key]
1781+ else:
1782+ trace_error_handler(log)
1783+ log.error('Working Flag {key} not found in list'.format(key=key))
1784+ raise KeyError('Working Flag {key} not found in list'.format(key=key))
1785+
1786+ def set_flag(self, key, reference):
1787+ """
1788+ Sets a working_flag based on the key passed in.
1789+
1790+ :param key: The working_flag to be created this is usually a major class like "renderer" or "main_window" .
1791+ :param reference: The data to be saved.
1792+ """
1793+ self.working_flags[key] = reference
1794+
1795+ def remove_flag(self, key):
1796+ """
1797+ Removes the working flags value from the list based on the key passed.
1798+
1799+ :param key: The working_flag to be deleted.
1800+ """
1801+ if key in self.working_flags:
1802+ del self.working_flags[key]
1803
1804=== modified file 'openlp/core/common/settings.py'
1805--- openlp/core/common/settings.py 2016-12-31 11:05:48 +0000
1806+++ openlp/core/common/settings.py 2017-01-08 22:05:36 +0000
1807@@ -26,7 +26,7 @@
1808 import logging
1809 import os
1810
1811-from PyQt5 import QtCore, QtGui, QtWidgets
1812+from PyQt5 import QtCore, QtGui
1813
1814 from openlp.core.common import ThemeLevel, SlideLimits, UiStrings, is_win, is_linux
1815
1816@@ -107,10 +107,9 @@
1817 __default_settings__ = {
1818 'advanced/add page break': False,
1819 'advanced/alternate rows': not is_win(),
1820+ 'advanced/autoscrolling': {'dist': 1, 'pos': 0},
1821 'advanced/current media plugin': -1,
1822 'advanced/data path': '',
1823- 'advanced/default color': '#ffffff',
1824- 'advanced/default image': ':/graphics/openlp-splash-screen.png',
1825 # 7 stands for now, 0 to 6 is Monday to Sunday.
1826 'advanced/default service day': 7,
1827 'advanced/default service enabled': True,
1828@@ -130,7 +129,9 @@
1829 'advanced/recent file count': 4,
1830 'advanced/save current plugin': False,
1831 'advanced/slide limits': SlideLimits.End,
1832+ 'advanced/slide max height': -4,
1833 'advanced/single click preview': False,
1834+ 'advanced/single click service preview': False,
1835 'advanced/x11 bypass wm': X11_BYPASS_DEFAULT,
1836 'advanced/search as type': True,
1837 'crashreport/last directory': '',
1838@@ -140,6 +141,7 @@
1839 'core/auto preview': False,
1840 'core/audio start paused': True,
1841 'core/auto unblank': False,
1842+ 'core/click live slide to unblank': False,
1843 'core/blank warning': False,
1844 'core/ccli number': '',
1845 'core/has run wizard': False,
1846@@ -150,6 +152,9 @@
1847 'core/save prompt': False,
1848 'core/screen blank': False,
1849 'core/show splash': True,
1850+ 'core/logo background color': '#ffffff',
1851+ 'core/logo file': ':/graphics/openlp-splash-screen.png',
1852+ 'core/logo hide on startup': False,
1853 'core/songselect password': '',
1854 'core/songselect username': '',
1855 'core/update check': True,
1856@@ -178,13 +183,15 @@
1857 'themes/wrap footer': False,
1858 'user interface/live panel': True,
1859 'user interface/live splitter geometry': QtCore.QByteArray(),
1860- 'user interface/lock panel': False,
1861+ 'user interface/lock panel': True,
1862 'user interface/main window geometry': QtCore.QByteArray(),
1863 'user interface/main window position': QtCore.QPoint(0, 0),
1864 'user interface/main window splitter geometry': QtCore.QByteArray(),
1865 'user interface/main window state': QtCore.QByteArray(),
1866 'user interface/preview panel': True,
1867 'user interface/preview splitter geometry': QtCore.QByteArray(),
1868+ 'user interface/is preset layout': False,
1869+ 'projector/show after wizard': False,
1870 'projector/db type': 'sqlite',
1871 'projector/db username': '',
1872 'projector/db password': '',
1873@@ -205,7 +212,12 @@
1874 # ('general/recent files', 'core/recent files', [(recent_files_conv, None)]),
1875 ('songs/search as type', 'advanced/search as type', []),
1876 ('media/players', 'media/players_temp', [(media_players_conv, None)]), # Convert phonon to system
1877- ('media/players_temp', 'media/players', []) # Move temp setting from above to correct setting
1878+ ('media/players_temp', 'media/players', []), # Move temp setting from above to correct setting
1879+ ('advanced/default color', 'core/logo background color', []), # Default image renamed + moved to general > 2.4.
1880+ ('advanced/default image', 'core/logo file', []), # Default image renamed + moved to general after 2.4.
1881+ ('shortcuts/escapeItem', 'shortcuts/desktopScreenEnable', []), # Escape item was removed in 2.6.
1882+ ('shortcuts/offlineHelpItem', 'shortcuts/userManualItem', []), # Online and Offline help were combined in 2.6.
1883+ ('shortcuts/onlineHelpItem', 'shortcuts/userManualItem', []) # Online and Offline help were combined in 2.6.
1884 ]
1885
1886 @staticmethod
1887@@ -252,10 +264,10 @@
1888 'shortcuts/blankScreen': [QtGui.QKeySequence(QtCore.Qt.Key_Period)],
1889 'shortcuts/collapse': [QtGui.QKeySequence(QtCore.Qt.Key_Minus)],
1890 'shortcuts/desktopScreen': [QtGui.QKeySequence(QtCore.Qt.Key_D)],
1891+ 'shortcuts/desktopScreenEnable': [QtGui.QKeySequence(QtCore.Qt.Key_Escape)],
1892 'shortcuts/delete': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
1893 'shortcuts/down': [QtGui.QKeySequence(QtCore.Qt.Key_Down)],
1894 'shortcuts/editSong': [],
1895- 'shortcuts/escapeItem': [QtGui.QKeySequence(QtCore.Qt.Key_Escape)],
1896 'shortcuts/expand': [QtGui.QKeySequence(QtCore.Qt.Key_Plus)],
1897 'shortcuts/exportThemeItem': [],
1898 'shortcuts/fileNewItem': [QtGui.QKeySequence(QtGui.QKeySequence.New)],
1899@@ -264,6 +276,7 @@
1900 'shortcuts/fileSaveItem': [QtGui.QKeySequence(QtGui.QKeySequence.Save)],
1901 'shortcuts/fileOpenItem': [QtGui.QKeySequence(QtGui.QKeySequence.Open)],
1902 'shortcuts/goLive': [],
1903+ 'shortcuts/userManualItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)],
1904 'shortcuts/importThemeItem': [],
1905 'shortcuts/importBibleItem': [],
1906 'shortcuts/listViewBiblesDeleteItem': [QtGui.QKeySequence(QtGui.QKeySequence.Delete)],
1907@@ -324,8 +337,6 @@
1908 QtGui.QKeySequence(QtCore.Qt.Key_PageDown)],
1909 'shortcuts/nextService': [QtGui.QKeySequence(QtCore.Qt.Key_Right)],
1910 'shortcuts/newService': [],
1911- 'shortcuts/offlineHelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)],
1912- 'shortcuts/onlineHelpItem': [QtGui.QKeySequence(QtGui.QKeySequence.HelpContents)],
1913 'shortcuts/openService': [],
1914 'shortcuts/saveService': [],
1915 'shortcuts/previousItem_live': [QtGui.QKeySequence(QtCore.Qt.Key_Up),
1916@@ -370,6 +381,7 @@
1917 'shortcuts/themeScreen': [QtGui.QKeySequence(QtCore.Qt.Key_T)],
1918 'shortcuts/toolsReindexItem': [],
1919 'shortcuts/toolsFindDuplicates': [],
1920+ 'shortcuts/toolsSongListReport': [],
1921 'shortcuts/toolsAlertItem': [QtGui.QKeySequence(QtCore.Qt.Key_F7)],
1922 'shortcuts/toolsFirstTimeWizard': [],
1923 'shortcuts/toolsOpenDataFolder': [],
1924@@ -479,16 +491,16 @@
1925 # Do NOT do this anywhere else!
1926 settings = QtCore.QSettings(self.fileName(), Settings.IniFormat)
1927 settings.beginGroup(plugin.settings_section)
1928- if settings.contains('%s count' % plugin.name):
1929+ if settings.contains('{name} count'.format(name=plugin.name)):
1930 # Get the count.
1931- list_count = int(settings.value('%s count' % plugin.name, 0))
1932+ list_count = int(settings.value('{name} count'.format(name=plugin.name), 0))
1933 if list_count:
1934 for counter in range(list_count):
1935 # The keys were named e. g.: "image 0"
1936- item = settings.value('%s %d' % (plugin.name, counter), '')
1937+ item = settings.value('{name} {counter:d}'.format(name=plugin.name, counter=counter), '')
1938 if item:
1939 files_list.append(item)
1940- settings.remove('%s %d' % (plugin.name, counter))
1941- settings.remove('%s count' % plugin.name)
1942+ settings.remove('{name} {counter:d}'.format(name=plugin.name, counter=counter))
1943+ settings.remove('{name} count'.format(name=plugin.name))
1944 settings.endGroup()
1945 return files_list
1946
1947=== modified file 'openlp/core/common/uistrings.py'
1948--- openlp/core/common/uistrings.py 2016-12-31 11:05:48 +0000
1949+++ openlp/core/common/uistrings.py 2017-01-08 22:05:36 +0000
1950@@ -23,6 +23,7 @@
1951 The :mod:`uistrings` module provides standard strings for OpenLP.
1952 """
1953 import logging
1954+import itertools
1955
1956 from openlp.core.common import translate
1957
1958@@ -52,10 +53,12 @@
1959 self.About = translate('OpenLP.Ui', 'About')
1960 self.Add = translate('OpenLP.Ui', '&Add')
1961 self.AddGroup = translate('OpenLP.Ui', 'Add group')
1962+ self.AddGroupDot = translate('OpenLP.Ui', 'Add group.')
1963 self.Advanced = translate('OpenLP.Ui', 'Advanced')
1964 self.AllFiles = translate('OpenLP.Ui', 'All Files')
1965 self.Automatic = translate('OpenLP.Ui', 'Automatic')
1966 self.BackgroundColor = translate('OpenLP.Ui', 'Background Color')
1967+ self.BackgroundColorColon = translate('OpenLP.Ui', 'Background color:')
1968 self.Bottom = translate('OpenLP.Ui', 'Bottom')
1969 self.Browse = translate('OpenLP.Ui', 'Browse...')
1970 self.Cancel = translate('OpenLP.Ui', 'Cancel')
1971@@ -67,7 +70,7 @@
1972 self.Default = translate('OpenLP.Ui', 'Default')
1973 self.DefaultColor = translate('OpenLP.Ui', 'Default Color:')
1974 self.DefaultServiceName = translate('OpenLP.Ui', 'Service %Y-%m-%d %H-%M',
1975- 'This may not contain any of the following characters: /\\?*|<>\[\]":+\n'
1976+ 'This may not contain any of the following characters: /\\?*|<>[]":+\n'
1977 'See http://docs.python.org/library/datetime'
1978 '.html#strftime-strptime-behavior for more information.')
1979 self.Delete = translate('OpenLP.Ui', '&Delete')
1980@@ -79,7 +82,8 @@
1981 self.Export = translate('OpenLP.Ui', 'Export')
1982 self.File = translate('OpenLP.Ui', 'File')
1983 self.FileNotFound = translate('OpenLP.Ui', 'File Not Found')
1984- self.FileNotFoundMessage = translate('OpenLP.Ui', 'File %s not found.\nPlease try selecting it individually.')
1985+ self.FileNotFoundMessage = translate('OpenLP.Ui',
1986+ 'File {name} not found.\nPlease try selecting it individually.')
1987 self.FontSizePtUnit = translate('OpenLP.Ui', 'pt', 'Abbreviated font pointsize unit')
1988 self.Help = translate('OpenLP.Ui', 'Help')
1989 self.Hours = translate('OpenLP.Ui', 'h', 'The abbreviated unit for hours')
1990@@ -108,9 +112,10 @@
1991 self.NFSp = translate('OpenLP.Ui', 'No Files Selected', 'Plural')
1992 self.NISs = translate('OpenLP.Ui', 'No Item Selected', 'Singular')
1993 self.NISp = translate('OpenLP.Ui', 'No Items Selected', 'Plural')
1994+ self.NoResults = translate('OpenLP.Ui', 'No Search Results')
1995 self.OLP = translate('OpenLP.Ui', 'OpenLP')
1996- self.OLPV2 = "%s %s" % (self.OLP, "2")
1997- self.OLPV2x = "%s %s" % (self.OLP, "2.4")
1998+ self.OLPV2 = "{name} {version}".format(name=self.OLP, version="2")
1999+ self.OLPV2x = "{name} {version}".format(name=self.OLP, version="2.4")
2000 self.OpenLPStart = translate('OpenLP.Ui', 'OpenLP is already running. Do you wish to continue?')
2001 self.OpenService = translate('OpenLP.Ui', 'Open service.')
2002 self.PlaySlidesInLoop = translate('OpenLP.Ui', 'Play Slides in Loop')
2003@@ -135,10 +140,12 @@
2004 self.Settings = translate('OpenLP.Ui', 'Settings')
2005 self.SaveService = translate('OpenLP.Ui', 'Save Service')
2006 self.Service = translate('OpenLP.Ui', 'Service')
2007+ self.ShortResults = translate('OpenLP.Ui', 'Please type more text to use \'Search As You Type\'')
2008 self.Split = translate('OpenLP.Ui', 'Optional &Split')
2009 self.SplitToolTip = translate('OpenLP.Ui',
2010 'Split a slide into two only if it does not fit on the screen as one slide.')
2011- self.StartTimeCode = translate('OpenLP.Ui', 'Start %s')
2012+ # TODO: WHERE is this used at? cannot find where it's used at in code.
2013+ self.StartTimeCode = translate('OpenLP.Ui', 'Start {code}')
2014 self.StopPlaySlidesInLoop = translate('OpenLP.Ui', 'Stop Play Slides in Loop')
2015 self.StopPlaySlidesToEnd = translate('OpenLP.Ui', 'Stop Play Slides to End')
2016 self.Theme = translate('OpenLP.Ui', 'Theme', 'Singular')
2017@@ -151,3 +158,31 @@
2018 self.Version = translate('OpenLP.Ui', 'Version')
2019 self.View = translate('OpenLP.Ui', 'View')
2020 self.ViewMode = translate('OpenLP.Ui', 'View Mode')
2021+ self.Video = translate('OpenLP.Ui', 'Video')
2022+ self.BibleShortSearchTitle = translate('OpenLP.Ui', 'Search is Empty or too Short')
2023+ self.BibleShortSearch = translate('OpenLP.Ui', '<strong>The search you have entered is empty or shorter '
2024+ 'than 3 characters long.</strong><br><br>Please try again with '
2025+ 'a longer search.')
2026+ self.BibleNoBiblesTitle = translate('OpenLP.Ui', 'No Bibles Available')
2027+ self.BibleNoBibles = translate('OpenLP.Ui', '<strong>There are no Bibles currently installed.</strong><br><br>'
2028+ 'Please use the Import Wizard to install one or more Bibles.')
2029+ book_chapter = translate('OpenLP.Ui', 'Book Chapter')
2030+ chapter = translate('OpenLP.Ui', 'Chapter')
2031+ verse = translate('OpenLP.Ui', 'Verse')
2032+ gap = ' | '
2033+ psalm = translate('OpenLP.Ui', 'Psalm')
2034+ may_shorten = translate('OpenLP.Ui', 'Book names may be shortened from full names, for an example Ps 23 = '
2035+ 'Psalm 23')
2036+ bible_scripture_items = \
2037+ [book_chapter, gap, psalm, ' 23<br>',
2038+ book_chapter, '%(range)s', chapter, gap, psalm, ' 23%(range)s24<br>',
2039+ book_chapter, '%(verse)s', verse, '%(range)s', verse, gap, psalm, ' 23%(verse)s1%(range)s2<br>',
2040+ book_chapter, '%(verse)s', verse, '%(range)s', verse, '%(list)s', verse, '%(range)s', verse, gap, psalm,
2041+ ' 23%(verse)s1%(range)s2%(list)s5%(range)s6<br>',
2042+ book_chapter, '%(verse)s', verse, '%(range)s', verse, '%(list)s', chapter, '%(verse)s', verse, '%(range)s',
2043+ verse, gap, psalm, ' 23%(verse)s1%(range)s2%(list)s24%(verse)s1%(range)s3<br>',
2044+ book_chapter, '%(verse)s', verse, '%(range)s', chapter, '%(verse)s', verse, gap, psalm,
2045+ ' 23%(verse)s1%(range)s24%(verse)s1<br><br>', may_shorten]
2046+ itertools.chain.from_iterable(itertools.repeat(strings, 1) if isinstance(strings, str)
2047+ else strings for strings in bible_scripture_items)
2048+ self.BibleScriptureError = ''.join(str(joined) for joined in bible_scripture_items)
2049
2050=== added file 'openlp/core/common/versionchecker.py'
2051--- openlp/core/common/versionchecker.py 1970-01-01 00:00:00 +0000
2052+++ openlp/core/common/versionchecker.py 2017-01-08 22:05:36 +0000
2053@@ -0,0 +1,173 @@
2054+import logging
2055+import os
2056+import platform
2057+import sys
2058+import time
2059+import urllib.error
2060+import urllib.parse
2061+import urllib.request
2062+from datetime import datetime
2063+from distutils.version import LooseVersion
2064+from subprocess import Popen, PIPE
2065+
2066+from PyQt5 import QtCore
2067+
2068+from openlp.core.common import AppLocation, Settings
2069+
2070+log = logging.getLogger(__name__)
2071+
2072+APPLICATION_VERSION = {}
2073+CONNECTION_TIMEOUT = 30
2074+CONNECTION_RETRIES = 2
2075+
2076+
2077+class VersionThread(QtCore.QThread):
2078+ """
2079+ A special Qt thread class to fetch the version of OpenLP from the website.
2080+ This is threaded so that it doesn't affect the loading time of OpenLP.
2081+ """
2082+ def __init__(self, main_window):
2083+ """
2084+ Constructor for the thread class.
2085+
2086+ :param main_window: The main window Object.
2087+ """
2088+ log.debug("VersionThread - Initialise")
2089+ super(VersionThread, self).__init__(None)
2090+ self.main_window = main_window
2091+
2092+ def run(self):
2093+ """
2094+ Run the thread.
2095+ """
2096+ self.sleep(1)
2097+ log.debug('Version thread - run')
2098+ app_version = get_application_version()
2099+ version = check_latest_version(app_version)
2100+ log.debug("Versions {version1} and {version2} ".format(version1=LooseVersion(str(version)),
2101+ version2=LooseVersion(str(app_version['full']))))
2102+ if LooseVersion(str(version)) > LooseVersion(str(app_version['full'])):
2103+ self.main_window.openlp_version_check.emit('{version}'.format(version=version))
2104+
2105+
2106+def get_application_version():
2107+ """
2108+ Returns the application version of the running instance of OpenLP::
2109+
2110+ {'full': '1.9.4-bzr1249', 'version': '1.9.4', 'build': 'bzr1249'}
2111+ """
2112+ global APPLICATION_VERSION
2113+ if APPLICATION_VERSION:
2114+ return APPLICATION_VERSION
2115+ if '--dev-version' in sys.argv or '-d' in sys.argv:
2116+ # NOTE: The following code is a duplicate of the code in setup.py. Any fix applied here should also be applied
2117+ # there.
2118+
2119+ # Get the revision of this tree.
2120+ bzr = Popen(('bzr', 'revno'), stdout=PIPE)
2121+ tree_revision, error = bzr.communicate()
2122+ tree_revision = tree_revision.decode()
2123+ code = bzr.wait()
2124+ if code != 0:
2125+ raise Exception('Error running bzr log')
2126+
2127+ # Get all tags.
2128+ bzr = Popen(('bzr', 'tags'), stdout=PIPE)
2129+ output, error = bzr.communicate()
2130+ code = bzr.wait()
2131+ if code != 0:
2132+ raise Exception('Error running bzr tags')
2133+ tags = list(map(bytes.decode, output.splitlines()))
2134+ if not tags:
2135+ tag_version = '0.0.0'
2136+ tag_revision = '0'
2137+ else:
2138+ # Remove any tag that has "?" as revision number. A "?" as revision number indicates, that this tag is from
2139+ # another series.
2140+ tags = [tag for tag in tags if tag.split()[-1].strip() != '?']
2141+ # Get the last tag and split it in a revision and tag name.
2142+ tag_version, tag_revision = tags[-1].split()
2143+ # If they are equal, then this tree is tarball with the source for the release. We do not want the revision
2144+ # number in the full version.
2145+ if tree_revision == tag_revision:
2146+ full_version = tag_version.strip()
2147+ else:
2148+ full_version = '{tag}-bzr{tree}'.format(tag=tag_version.strip(), tree=tree_revision.strip())
2149+ else:
2150+ # We're not running the development version, let's use the file.
2151+ file_path = AppLocation.get_directory(AppLocation.VersionDir)
2152+ file_path = os.path.join(file_path, '.version')
2153+ version_file = None
2154+ try:
2155+ version_file = open(file_path, 'r')
2156+ full_version = str(version_file.read()).rstrip()
2157+ except IOError:
2158+ log.exception('Error in version file.')
2159+ full_version = '0.0.0-bzr000'
2160+ finally:
2161+ if version_file:
2162+ version_file.close()
2163+ bits = full_version.split('-')
2164+ APPLICATION_VERSION = {
2165+ 'full': full_version,
2166+ 'version': bits[0],
2167+ 'build': bits[1] if len(bits) > 1 else None
2168+ }
2169+ if APPLICATION_VERSION['build']:
2170+ log.info('Openlp version {version} build {build}'.format(version=APPLICATION_VERSION['version'],
2171+ build=APPLICATION_VERSION['build']))
2172+ else:
2173+ log.info('Openlp version {version}'.format(version=APPLICATION_VERSION['version']))
2174+ return APPLICATION_VERSION
2175+
2176+
2177+def check_latest_version(current_version):
2178+ """
2179+ Check the latest version of OpenLP against the version file on the OpenLP
2180+ site.
2181+
2182+ **Rules around versions and version files:**
2183+
2184+ * If a version number has a build (i.e. -bzr1234), then it is a nightly.
2185+ * If a version number's minor version is an odd number, it is a development release.
2186+ * If a version number's minor version is an even number, it is a stable release.
2187+
2188+ :param current_version: The current version of OpenLP.
2189+ """
2190+ version_string = current_version['full']
2191+ # set to prod in the distribution config file.
2192+ settings = Settings()
2193+ settings.beginGroup('core')
2194+ last_test = settings.value('last version test')
2195+ this_test = str(datetime.now().date())
2196+ settings.setValue('last version test', this_test)
2197+ settings.endGroup()
2198+ if last_test != this_test:
2199+ if current_version['build']:
2200+ req = urllib.request.Request('http://www.openlp.org/files/nightly_version.txt')
2201+ else:
2202+ version_parts = current_version['version'].split('.')
2203+ if int(version_parts[1]) % 2 != 0:
2204+ req = urllib.request.Request('http://www.openlp.org/files/dev_version.txt')
2205+ else:
2206+ req = urllib.request.Request('http://www.openlp.org/files/version.txt')
2207+ req.add_header('User-Agent', 'OpenLP/{version} {system}/{release}; '.format(version=current_version['full'],
2208+ system=platform.system(),
2209+ release=platform.release()))
2210+ remote_version = None
2211+ retries = 0
2212+ while True:
2213+ try:
2214+ remote_version = str(urllib.request.urlopen(req, None,
2215+ timeout=CONNECTION_TIMEOUT).read().decode()).strip()
2216+ except (urllib.error.URLError, ConnectionError):
2217+ if retries > CONNECTION_RETRIES:
2218+ log.exception('Failed to download the latest OpenLP version file')
2219+ else:
2220+ retries += 1
2221+ time.sleep(0.1)
2222+ continue
2223+ break
2224+ if remote_version:
2225+ version_string = remote_version
2226+ return version_string
2227
2228=== modified file 'openlp/core/lib/__init__.py'
2229--- openlp/core/lib/__init__.py 2016-12-31 11:05:48 +0000
2230+++ openlp/core/lib/__init__.py 2017-01-08 22:05:36 +0000
2231@@ -24,13 +24,12 @@
2232 OpenLP work.
2233 """
2234
2235-from distutils.version import LooseVersion
2236 import logging
2237 import os
2238+from distutils.version import LooseVersion
2239
2240 from PyQt5 import QtCore, QtGui, Qt, QtWidgets
2241
2242-
2243 from openlp.core.common import translate
2244
2245 log = logging.getLogger(__name__ + '.__init__')
2246@@ -55,9 +54,13 @@
2247
2248 ``Theme``
2249 This says, that the image is used by a theme.
2250+
2251+ ``CommandPlugins``
2252+ This states that an image is being used by a command plugin.
2253 """
2254 ImagePlugin = 1
2255 Theme = 2
2256+ CommandPlugins = 3
2257
2258
2259 class MediaType(object):
2260@@ -92,12 +95,12 @@
2261 content = None
2262 try:
2263 file_handle = open(text_file, 'r', encoding='utf-8')
2264- if not file_handle.read(3) == '\xEF\xBB\xBF':
2265+ if file_handle.read(3) != '\xEF\xBB\xBF':
2266 # no BOM was found
2267 file_handle.seek(0)
2268 content = file_handle.read()
2269 except (IOError, UnicodeError):
2270- log.exception('Failed to open text file %s' % text_file)
2271+ log.exception('Failed to open text file {text}'.format(text=text_file))
2272 finally:
2273 if file_handle:
2274 file_handle.close()
2275@@ -126,16 +129,16 @@
2276 location like ``/path/to/file.png``. However, the **recommended** way is to specify a resource string.
2277 :return: The build icon.
2278 """
2279+ if isinstance(icon, QtGui.QIcon):
2280+ return icon
2281+ pix_map = None
2282 button_icon = QtGui.QIcon()
2283- if isinstance(icon, QtGui.QIcon):
2284- button_icon = icon
2285- elif isinstance(icon, str):
2286- if icon.startswith(':/'):
2287- button_icon.addPixmap(QtGui.QPixmap(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off)
2288- else:
2289- button_icon.addPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(icon)), QtGui.QIcon.Normal, QtGui.QIcon.Off)
2290+ if isinstance(icon, str):
2291+ pix_map = QtGui.QPixmap(icon)
2292 elif isinstance(icon, QtGui.QImage):
2293- button_icon.addPixmap(QtGui.QPixmap.fromImage(icon), QtGui.QIcon.Normal, QtGui.QIcon.Off)
2294+ pix_map = QtGui.QPixmap.fromImage(icon)
2295+ if pix_map:
2296+ button_icon.addPixmap(pix_map, QtGui.QIcon.Normal, QtGui.QIcon.Off)
2297 return button_icon
2298
2299
2300@@ -174,10 +177,30 @@
2301 ext = os.path.splitext(thumb_path)[1].lower()
2302 reader = QtGui.QImageReader(image_path)
2303 if size is None:
2304- ratio = reader.size().width() / reader.size().height()
2305+ # No size given; use default height of 88
2306+ if reader.size().isEmpty():
2307+ ratio = 1
2308+ else:
2309+ ratio = reader.size().width() / reader.size().height()
2310 reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
2311- else:
2312+ elif size.isValid():
2313+ # Complete size given
2314 reader.setScaledSize(size)
2315+ else:
2316+ # Invalid size given
2317+ if reader.size().isEmpty():
2318+ ratio = 1
2319+ else:
2320+ ratio = reader.size().width() / reader.size().height()
2321+ if size.width() >= 0:
2322+ # Valid width; scale height
2323+ reader.setScaledSize(QtCore.QSize(size.width(), int(size.width() / ratio)))
2324+ elif size.height() >= 0:
2325+ # Valid height; scale width
2326+ reader.setScaledSize(QtCore.QSize(int(ratio * size.height()), size.height()))
2327+ else:
2328+ # Invalid; use default height of 88
2329+ reader.setScaledSize(QtCore.QSize(int(ratio * 88), 88))
2330 thumb = reader.read()
2331 thumb.save(thumb_path, ext[1:])
2332 if not return_icon:
2333@@ -287,46 +310,34 @@
2334
2335 def create_separated_list(string_list):
2336 """
2337- Returns a string that represents a join of a list of strings with a localized separator. This function corresponds
2338-
2339- to QLocale::createSeparatedList which was introduced in Qt 4.8 and implements the algorithm from
2340- http://www.unicode.org/reports/tr35/#ListPatterns
2341-
2342- :param string_list: List of unicode strings
2343+ Returns a string that represents a join of a list of strings with a localized separator.
2344+ Localized separation will be done via the translate() function by the translators.
2345+
2346+ :param string_list: List of unicode strings
2347+ :return: Formatted string
2348 """
2349- if LooseVersion(Qt.PYQT_VERSION_STR) >= LooseVersion('4.9') and LooseVersion(Qt.qVersion()) >= LooseVersion('4.8'):
2350- return QtCore.QLocale().createSeparatedList(string_list)
2351- if not string_list:
2352- return ''
2353- elif len(string_list) == 1:
2354- return string_list[0]
2355- elif len(string_list) == 2:
2356- return translate('OpenLP.core.lib', '%s and %s',
2357- 'Locale list separator: 2 items') % (string_list[0], string_list[1])
2358+ list_length = len(string_list)
2359+ if list_length == 1:
2360+ list_to_string = string_list[0]
2361+ elif list_length == 2:
2362+ list_to_string = translate('OpenLP.core.lib', '{one} and {two}').format(one=string_list[0], two=string_list[1])
2363+ elif list_length > 2:
2364+ list_to_string = translate('OpenLP.core.lib', '{first} and {last}').format(first=', '.join(string_list[:-1]),
2365+ last=string_list[-1])
2366 else:
2367- merged = translate('OpenLP.core.lib', '%s, and %s',
2368- 'Locale list separator: end') % (string_list[-2], string_list[-1])
2369- for index in reversed(list(range(1, len(string_list) - 2))):
2370- merged = translate('OpenLP.core.lib', '%s, %s',
2371- 'Locale list separator: middle') % (string_list[index], merged)
2372- return translate('OpenLP.core.lib', '%s, %s', 'Locale list separator: start') % (string_list[0], merged)
2373-
2374-
2375-from .colorbutton import ColorButton
2376+ list_to_string = ''
2377+ return list_to_string
2378+
2379+
2380 from .exceptions import ValidationError
2381 from .filedialog import FileDialog
2382 from .screen import ScreenList
2383-from .listwidgetwithdnd import ListWidgetWithDnD
2384-from .treewidgetwithdnd import TreeWidgetWithDnD
2385 from .formattingtags import FormattingTags
2386-from .spelltextedit import SpellTextEdit
2387 from .plugin import PluginStatus, StringContent, Plugin
2388 from .pluginmanager import PluginManager
2389 from .settingstab import SettingsTab
2390 from .serviceitem import ServiceItem, ServiceItemType, ItemCapabilities
2391 from .htmlbuilder import build_html, build_lyrics_format_css, build_lyrics_outline_css
2392-from .toolbar import OpenLPToolbar
2393-from .dockwidget import OpenLPDockWidget
2394 from .imagemanager import ImageManager
2395 from .renderer import Renderer
2396 from .mediamanageritem import MediaManagerItem
2397
2398=== renamed file 'openlp/core/lib/colorbutton.py' => 'openlp/core/lib/colorbutton.py.THIS'
2399=== modified file 'openlp/core/lib/db.py'
2400--- openlp/core/lib/db.py 2016-12-31 11:05:48 +0000
2401+++ openlp/core/lib/db.py 2017-01-08 22:05:36 +0000
2402@@ -34,9 +34,8 @@
2403 from alembic.migration import MigrationContext
2404 from alembic.operations import Operations
2405
2406-from openlp.core.common import AppLocation, Settings, translate
2407+from openlp.core.common import AppLocation, Settings, translate, delete_file
2408 from openlp.core.lib.ui import critical_error_message_box
2409-from openlp.core.utils import delete_file
2410
2411 log = logging.getLogger(__name__)
2412
2413@@ -69,9 +68,11 @@
2414 :return: The path to the database as type str
2415 """
2416 if db_file_name is None:
2417- return 'sqlite:///%s/%s.sqlite' % (AppLocation.get_section_data_path(plugin_name), plugin_name)
2418+ return 'sqlite:///{path}/{plugin}.sqlite'.format(path=AppLocation.get_section_data_path(plugin_name),
2419+ plugin=plugin_name)
2420 else:
2421- return 'sqlite:///%s/%s' % (AppLocation.get_section_data_path(plugin_name), db_file_name)
2422+ return 'sqlite:///{path}/{name}'.format(path=AppLocation.get_section_data_path(plugin_name),
2423+ name=db_file_name)
2424
2425
2426 def handle_db_error(plugin_name, db_file_name):
2427@@ -83,10 +84,10 @@
2428 :return: None
2429 """
2430 db_path = get_db_path(plugin_name, db_file_name)
2431- log.exception('Error loading database: %s', db_path)
2432+ log.exception('Error loading database: {db}'.format(db=db_path))
2433 critical_error_message_box(translate('OpenLP.Manager', 'Database Error'),
2434- translate('OpenLP.Manager', 'OpenLP cannot load your database.\n\nDatabase: %s')
2435- % db_path)
2436+ translate('OpenLP.Manager',
2437+ 'OpenLP cannot load your database.\n\nDatabase: {db}').format(db=db_path))
2438
2439
2440 def init_url(plugin_name, db_file_name=None):
2441@@ -102,10 +103,11 @@
2442 if db_type == 'sqlite':
2443 db_url = get_db_path(plugin_name, db_file_name)
2444 else:
2445- db_url = '%s://%s:%s@%s/%s' % (db_type, urlquote(settings.value('db username')),
2446- urlquote(settings.value('db password')),
2447- urlquote(settings.value('db hostname')),
2448- urlquote(settings.value('db database')))
2449+ db_url = '{type}://{user}:{password}@{host}/{db}'.format(type=db_type,
2450+ user=urlquote(settings.value('db username')),
2451+ password=urlquote(settings.value('db password')),
2452+ host=urlquote(settings.value('db hostname')),
2453+ db=urlquote(settings.value('db database')))
2454 settings.endGroup()
2455 return db_url
2456
2457@@ -120,6 +122,21 @@
2458 return Operations(context)
2459
2460
2461+class BaseModel(object):
2462+ """
2463+ BaseModel provides a base object with a set of generic functions
2464+ """
2465+ @classmethod
2466+ def populate(cls, **kwargs):
2467+ """
2468+ Creates an instance of a class and populates it, returning the instance
2469+ """
2470+ instance = cls()
2471+ for key, value in kwargs.items():
2472+ instance.__setattr__(key, value)
2473+ return instance
2474+
2475+
2476 def upgrade_db(url, upgrade):
2477 """
2478 Upgrade a database.
2479@@ -158,10 +175,10 @@
2480 return version, upgrade.__version__
2481 version += 1
2482 try:
2483- while hasattr(upgrade, 'upgrade_%d' % version):
2484- log.debug('Running upgrade_%d', version)
2485+ while hasattr(upgrade, 'upgrade_{version:d}'.format(version=version)):
2486+ log.debug('Running upgrade_{version:d}'.format(version=version))
2487 try:
2488- upgrade_func = getattr(upgrade, 'upgrade_%d' % version)
2489+ upgrade_func = getattr(upgrade, 'upgrade_{version:d}'.format(version=version))
2490 upgrade_func(session, metadata)
2491 session.commit()
2492 # Update the version number AFTER a commit so that we are sure the previous transaction happened
2493@@ -169,16 +186,16 @@
2494 session.commit()
2495 version += 1
2496 except (SQLAlchemyError, DBAPIError):
2497- log.exception('Could not run database upgrade script "upgrade_%s", upgrade process has been halted.',
2498- version)
2499+ log.exception('Could not run database upgrade script '
2500+ '"upgrade_{version:d}", upgrade process has been halted.'.format(version=version))
2501 break
2502 except (SQLAlchemyError, DBAPIError):
2503 version_meta = Metadata.populate(key='version', value=int(upgrade.__version__))
2504 session.commit()
2505 upgrade_version = upgrade.__version__
2506- version_meta = int(version_meta.value)
2507+ version = int(version_meta.value)
2508 session.close()
2509- return version_meta, upgrade_version
2510+ return version, upgrade_version
2511
2512
2513 def delete_database(plugin_name, db_file_name=None):
2514@@ -195,21 +212,6 @@
2515 return delete_file(db_file_path)
2516
2517
2518-class BaseModel(object):
2519- """
2520- BaseModel provides a base object with a set of generic functions
2521- """
2522- @classmethod
2523- def populate(cls, **kwargs):
2524- """
2525- Creates an instance of a class and populates it, returning the instance
2526- """
2527- instance = cls()
2528- for key, value in kwargs.items():
2529- instance.__setattr__(key, value)
2530- return instance
2531-
2532-
2533 class Manager(object):
2534 """
2535 Provide generic object persistence management
2536@@ -243,9 +245,10 @@
2537 critical_error_message_box(
2538 translate('OpenLP.Manager', 'Database Error'),
2539 translate('OpenLP.Manager', 'The database being loaded was created in a more recent version of '
2540- 'OpenLP. The database is version %d, while OpenLP expects version %d. The database will '
2541- 'not be loaded.\n\nDatabase: %s') % (db_ver, up_ver, self.db_url)
2542- )
2543+ 'OpenLP. The database is version {db_ver}, while OpenLP expects version {db_up}. '
2544+ 'The database will not be loaded.\n\nDatabase: {db_name}').format(db_ver=db_ver,
2545+ db_up=up_ver,
2546+ db_name=self.db_url))
2547 return
2548 if not session:
2549 try:
2550@@ -461,7 +464,7 @@
2551 raise
2552 except InvalidRequestError:
2553 self.session.rollback()
2554- log.exception('Failed to delete %s records', object_class.__name__)
2555+ log.exception('Failed to delete {name} records'.format(name=object_class.__name__))
2556 return False
2557 except:
2558 self.session.rollback()
2559
2560=== renamed file 'openlp/core/lib/dockwidget.py' => 'openlp/core/lib/dockwidget.py.THIS'
2561=== modified file 'openlp/core/lib/exceptions.py'
2562--- openlp/core/lib/exceptions.py 2016-12-31 11:05:48 +0000
2563+++ openlp/core/lib/exceptions.py 2017-01-08 22:05:36 +0000
2564@@ -24,9 +24,15 @@
2565 """
2566
2567
2568+# TODO: Test __init__ & __str__
2569 class ValidationError(Exception):
2570 """
2571 The :class:`~openlp.core.lib.exceptions.ValidationError` exception provides a custom exception for validating
2572 import files.
2573 """
2574- pass
2575+
2576+ def __init__(self, msg="Validation Error"):
2577+ self.msg = msg
2578+
2579+ def __str__(self):
2580+ return '{error_message}'.format(error_message=self.msg)
2581
2582=== modified file 'openlp/core/lib/filedialog.py'
2583--- openlp/core/lib/filedialog.py 2016-12-31 11:05:48 +0000
2584+++ openlp/core/lib/filedialog.py 2017-01-08 22:05:36 +0000
2585@@ -50,9 +50,9 @@
2586 log.info('File not found. Attempting to unquote.')
2587 file = parse.unquote(file)
2588 if not os.path.exists(file):
2589- log.error('File %s not found.' % file)
2590+ log.error('File {text} not found.'.format(text=file))
2591 QtWidgets.QMessageBox.information(parent, UiStrings().FileNotFound,
2592- UiStrings().FileNotFoundMessage % file)
2593+ UiStrings().FileNotFoundMessage.format(name=file))
2594 continue
2595 file_list.append(file)
2596 return file_list
2597
2598=== modified file 'openlp/core/lib/htmlbuilder.py'
2599--- openlp/core/lib/htmlbuilder.py 2016-12-31 11:05:48 +0000
2600+++ openlp/core/lib/htmlbuilder.py 2017-01-08 22:05:36 +0000
2601@@ -389,6 +389,7 @@
2602 """
2603 import logging
2604
2605+from string import Template
2606 from PyQt5 import QtWebKit
2607
2608 from openlp.core.common import Settings
2609@@ -396,156 +397,200 @@
2610
2611 log = logging.getLogger(__name__)
2612
2613-HTMLSRC = """
2614-<!DOCTYPE html>
2615-<html>
2616-<head>
2617-<title>OpenLP Display</title>
2618-<style>
2619-*{
2620+HTML_SRC = Template("""
2621+ <!DOCTYPE html>
2622+ <html>
2623+ <head>
2624+ <title>OpenLP Display</title>
2625+ <style>
2626+ *{
2627+ margin: 0;
2628+ padding: 0;
2629+ border: 0;
2630+ overflow: hidden;
2631+ -webkit-user-select: none;
2632+ }
2633+ body {
2634+ ${bg_css};
2635+ }
2636+ .size {
2637+ position: absolute;
2638+ left: 0px;
2639+ top: 0px;
2640+ width: 100%;
2641+ height: 100%;
2642+ }
2643+ #black {
2644+ z-index: 8;
2645+ background-color: black;
2646+ display: none;
2647+ }
2648+ #bgimage {
2649+ z-index: 1;
2650+ }
2651+ #image {
2652+ z-index: 2;
2653+ }
2654+ ${css_additions}
2655+ #footer {
2656+ position: absolute;
2657+ z-index: 6;
2658+ ${footer_css}
2659+ }
2660+ /* lyric css */${lyrics_css}
2661+ sup {
2662+ font-size: 0.6em;
2663+ vertical-align: top;
2664+ position: relative;
2665+ top: -0.3em;
2666+ }
2667+ </style>
2668+ <script>
2669+ var timer = null;
2670+ var transition = ${transitions};
2671+ ${js_additions}
2672+
2673+ function show_image(src){
2674+ var img = document.getElementById('image');
2675+ img.src = src;
2676+ if(src == '')
2677+ img.style.display = 'none';
2678+ else
2679+ img.style.display = 'block';
2680+ }
2681+
2682+ function show_blank(state){
2683+ var black = 'none';
2684+ var lyrics = '';
2685+ switch(state){
2686+ case 'theme':
2687+ lyrics = 'hidden';
2688+ break;
2689+ case 'black':
2690+ black = 'block';
2691+ break;
2692+ case 'desktop':
2693+ break;
2694+ }
2695+ document.getElementById('black').style.display = black;
2696+ document.getElementById('lyricsmain').style.visibility = lyrics;
2697+ document.getElementById('image').style.visibility = lyrics;
2698+ document.getElementById('footer').style.visibility = lyrics;
2699+ }
2700+
2701+ function show_footer(footertext){
2702+ document.getElementById('footer').innerHTML = footertext;
2703+ }
2704+
2705+ function show_text(new_text){
2706+ var match = /-webkit-text-fill-color:[^;\"]+/gi;
2707+ if(timer != null)
2708+ clearTimeout(timer);
2709+ /*
2710+ QtWebkit bug with outlines and justify causing outline alignment
2711+ problems. (Bug 859950) Surround each word with a <span> to workaround,
2712+ but only in this scenario.
2713+ */
2714+ var txt = document.getElementById('lyricsmain');
2715+ if(window.getComputedStyle(txt).textAlign == 'justify'){
2716+ if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
2717+ new_text = new_text.replace(/(\s|&nbsp;)+(?![^<]*>)/g,
2718+ function(match) {
2719+ return '</span>' + match + '<span>';
2720+ });
2721+ new_text = '<span>' + new_text + '</span>';
2722+ }
2723+ }
2724+ text_fade('lyricsmain', new_text);
2725+ }
2726+
2727+ function text_fade(id, new_text){
2728+ /*
2729+ Show the text.
2730+ */
2731+ var text = document.getElementById(id);
2732+ if(text == null) return;
2733+ if(!transition){
2734+ text.innerHTML = new_text;
2735+ return;
2736+ }
2737+ // Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
2738+ text.style.opacity = '0.1';
2739+ // Fade new text in after the old text has finished fading out.
2740+ timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
2741+ }
2742+
2743+ function _show_text(text, new_text) {
2744+ /*
2745+ Helper function to show the new_text delayed.
2746+ */
2747+ text.innerHTML = new_text;
2748+ text.style.opacity = '1';
2749+ // Wait until the text is completely visible. We want to save the timer id, to be able to call
2750+ // clearTimeout(timer) when the text has changed before finishing fading.
2751+ timer = window.setTimeout(function(){timer = null;}, 400);
2752+ }
2753+
2754+ function show_text_completed(){
2755+ return (timer == null);
2756+ }
2757+ </script>
2758+ </head>
2759+ <body>
2760+ <img id="bgimage" class="size" ${bg_image} />
2761+ <img id="image" class="size" ${image} />
2762+ ${html_additions}
2763+ <div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
2764+ <div id="footer" class="footer"></div>
2765+ <div id="black" class="size"></div>
2766+ </body>
2767+ </html>
2768+ """)
2769+
2770+LYRICS_SRC = Template("""
2771+ .lyricstable {
2772+ z-index: 5;
2773+ position: absolute;
2774+ display: table;
2775+ ${stable}
2776+ }
2777+ .lyricscell {
2778+ display: table-cell;
2779+ word-wrap: break-word;
2780+ -webkit-transition: opacity 0.4s ease;
2781+ ${lyrics}
2782+ }
2783+ .lyricsmain {
2784+ ${main}
2785+ }
2786+ """)
2787+
2788+FOOTER_SRC = Template("""
2789+ left: ${left}px;
2790+ bottom: ${bottom}px;
2791+ width: ${width}px;
2792+ font-family: ${family};
2793+ font-size: ${size}pt;
2794+ color: ${color};
2795+ text-align: left;
2796+ white-space: ${space};
2797+ """)
2798+
2799+LYRICS_FORMAT_SRC = Template("""
2800+ ${justify}word-wrap: break-word;
2801+ text-align: ${align};
2802+ vertical-align: ${valign};
2803+ font-family: ${font};
2804+ font-size: ${size}pt;
2805+ color: ${color};
2806+ line-height: ${line}%;
2807 margin: 0;
2808 padding: 0;
2809- border: 0;
2810- overflow: hidden;
2811- -webkit-user-select: none;
2812-}
2813-body {
2814- %s;
2815-}
2816-.size {
2817- position: absolute;
2818- left: 0px;
2819- top: 0px;
2820- width: 100%%;
2821- height: 100%%;
2822-}
2823-#black {
2824- z-index: 8;
2825- background-color: black;
2826- display: none;
2827-}
2828-#bgimage {
2829- z-index: 1;
2830-}
2831-#image {
2832- z-index: 2;
2833-}
2834-%s
2835-#footer {
2836- position: absolute;
2837- z-index: 6;
2838- %s
2839-}
2840-/* lyric css */
2841-%s
2842-sup {
2843- font-size: 0.6em;
2844- vertical-align: top;
2845- position: relative;
2846- top: -0.3em;
2847-}
2848-</style>
2849-<script>
2850- var timer = null;
2851- var transition = %s;
2852- %s
2853-
2854- function show_image(src){
2855- var img = document.getElementById('image');
2856- img.src = src;
2857- if(src == '')
2858- img.style.display = 'none';
2859- else
2860- img.style.display = 'block';
2861- }
2862-
2863- function show_blank(state){
2864- var black = 'none';
2865- var lyrics = '';
2866- switch(state){
2867- case 'theme':
2868- lyrics = 'hidden';
2869- break;
2870- case 'black':
2871- black = 'block';
2872- break;
2873- case 'desktop':
2874- break;
2875- }
2876- document.getElementById('black').style.display = black;
2877- document.getElementById('lyricsmain').style.visibility = lyrics;
2878- document.getElementById('image').style.visibility = lyrics;
2879- document.getElementById('footer').style.visibility = lyrics;
2880- }
2881-
2882- function show_footer(footertext){
2883- document.getElementById('footer').innerHTML = footertext;
2884- }
2885-
2886- function show_text(new_text){
2887- var match = /-webkit-text-fill-color:[^;\"]+/gi;
2888- if(timer != null)
2889- clearTimeout(timer);
2890- /*
2891- QtWebkit bug with outlines and justify causing outline alignment
2892- problems. (Bug 859950) Surround each word with a <span> to workaround,
2893- but only in this scenario.
2894- */
2895- var txt = document.getElementById('lyricsmain');
2896- if(window.getComputedStyle(txt).textAlign == 'justify'){
2897- if(window.getComputedStyle(txt).webkitTextStrokeWidth != '0px'){
2898- new_text = new_text.replace(/(\s|&nbsp;)+(?![^<]*>)/g,
2899- function(match) {
2900- return '</span>' + match + '<span>';
2901- });
2902- new_text = '<span>' + new_text + '</span>';
2903- }
2904- }
2905- text_fade('lyricsmain', new_text);
2906- }
2907-
2908- function text_fade(id, new_text){
2909- /*
2910- Show the text.
2911- */
2912- var text = document.getElementById(id);
2913- if(text == null) return;
2914- if(!transition){
2915- text.innerHTML = new_text;
2916- return;
2917- }
2918- // Fade text out. 0.1 to minimize the time "nothing" is shown on the screen.
2919- text.style.opacity = '0.1';
2920- // Fade new text in after the old text has finished fading out.
2921- timer = window.setTimeout(function(){_show_text(text, new_text)}, 400);
2922- }
2923-
2924- function _show_text(text, new_text) {
2925- /*
2926- Helper function to show the new_text delayed.
2927- */
2928- text.innerHTML = new_text;
2929- text.style.opacity = '1';
2930- // Wait until the text is completely visible. We want to save the timer id, to be able to call
2931- // clearTimeout(timer) when the text has changed before finishing fading.
2932- timer = window.setTimeout(function(){timer = null;}, 400);
2933- }
2934-
2935- function show_text_completed(){
2936- return (timer == null);
2937- }
2938-</script>
2939-</head>
2940-<body>
2941-<img id="bgimage" class="size" %s />
2942-<img id="image" class="size" %s />
2943-%s
2944-<div class="lyricstable"><div id="lyricsmain" style="opacity:1" class="lyricscell lyricsmain"></div></div>
2945-<div id="footer" class="footer"></div>
2946-<div id="black" class="size"></div>
2947-</body>
2948-</html>
2949-"""
2950+ padding-bottom: ${bottom};
2951+ padding-left: ${left}px;
2952+ width: ${width}px;
2953+ height: ${height}px;${font_style}${font_weight}
2954+ """)
2955
2956
2957 def build_html(item, screen, is_live, background, image=None, plugins=None):
2958@@ -564,13 +609,13 @@
2959 theme_data = item.theme_data
2960 # Image generated and poked in
2961 if background:
2962- bgimage_src = 'src="data:image/png;base64,%s"' % background
2963+ bgimage_src = 'src="data:image/png;base64,{image}"'.format(image=background)
2964 elif item.bg_image_bytes:
2965- bgimage_src = 'src="data:image/png;base64,%s"' % item.bg_image_bytes
2966+ bgimage_src = 'src="data:image/png;base64,{image}"'.format(image=item.bg_image_bytes)
2967 else:
2968 bgimage_src = 'style="display:none;"'
2969 if image:
2970- image_src = 'src="data:image/png;base64,%s"' % image
2971+ image_src = 'src="data:image/png;base64,{image}"'.format(image=image)
2972 else:
2973 image_src = 'style="display:none;"'
2974 css_additions = ''
2975@@ -581,18 +626,17 @@
2976 css_additions += plugin.get_display_css()
2977 js_additions += plugin.get_display_javascript()
2978 html_additions += plugin.get_display_html()
2979- html = HTMLSRC % (
2980- build_background_css(item, width),
2981- css_additions,
2982- build_footer_css(item, height),
2983- build_lyrics_css(item),
2984- 'true' if theme_data and theme_data.display_slide_transition and is_live else 'false',
2985- js_additions,
2986- bgimage_src,
2987- image_src,
2988- html_additions
2989- )
2990- return html
2991+ return HTML_SRC.substitute(bg_css=build_background_css(item, width),
2992+ css_additions=css_additions,
2993+ footer_css=build_footer_css(item, height),
2994+ lyrics_css=build_lyrics_css(item),
2995+ transitions='true' if (theme_data and
2996+ theme_data.display_slide_transition and
2997+ is_live) else 'false',
2998+ js_additions=js_additions,
2999+ bg_image=bgimage_src,
3000+ image=image_src,
3001+ html_additions=html_additions)
3002
3003
3004 def webkit_version():
3005@@ -601,9 +645,9 @@
3006 """
3007 try:
3008 webkit_ver = float(QtWebKit.qWebKitVersion())
3009- log.debug('Webkit version = %s' % webkit_ver)
3010+ log.debug('Webkit version = {version}'.format(version=webkit_ver))
3011 except AttributeError:
3012- webkit_ver = 0
3013+ webkit_ver = 0.0
3014 return webkit_ver
3015
3016
3017@@ -621,23 +665,25 @@
3018 if theme.background_type == BackgroundType.to_string(BackgroundType.Transparent):
3019 background = ''
3020 elif theme.background_type == BackgroundType.to_string(BackgroundType.Solid):
3021- background = 'background-color: %s' % theme.background_color
3022+ background = 'background-color: {theme}'.format(theme=theme.background_color)
3023 else:
3024 if theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Horizontal):
3025- background = 'background: -webkit-gradient(linear, left top, left bottom, from(%s), to(%s)) fixed' \
3026- % (theme.background_start_color, theme.background_end_color)
3027+ background = 'background: -webkit-gradient(linear, left top, left bottom, from({start}), to({end})) ' \
3028+ 'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
3029 elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftTop):
3030- background = 'background: -webkit-gradient(linear, left top, right bottom, from(%s), to(%s)) fixed' \
3031- % (theme.background_start_color, theme.background_end_color)
3032+ background = 'background: -webkit-gradient(linear, left top, right bottom, from({start}), to({end})) ' \
3033+ 'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
3034 elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.LeftBottom):
3035- background = 'background: -webkit-gradient(linear, left bottom, right top, from(%s), to(%s)) fixed' \
3036- % (theme.background_start_color, theme.background_end_color)
3037+ background = 'background: -webkit-gradient(linear, left bottom, right top, from({start}), to({end})) ' \
3038+ 'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
3039 elif theme.background_direction == BackgroundGradientType.to_string(BackgroundGradientType.Vertical):
3040- background = 'background: -webkit-gradient(linear, left top, right top, from(%s), to(%s)) fixed' % \
3041- (theme.background_start_color, theme.background_end_color)
3042+ background = 'background: -webkit-gradient(linear, left top, right top, from({start}), to({end})) ' \
3043+ 'fixed'.format(start=theme.background_start_color, end=theme.background_end_color)
3044 else:
3045- background = 'background: -webkit-gradient(radial, %s 50%%, 100, %s 50%%, %s, from(%s), to(%s)) fixed'\
3046- % (width, width, width, theme.background_start_color, theme.background_end_color)
3047+ background = 'background: -webkit-gradient(radial, {width} 50%, 100, {width} 50%, {width}, ' \
3048+ 'from({start}), to({end})) fixed'.format(width=width,
3049+ start=theme.background_start_color,
3050+ end=theme.background_end_color)
3051 return background
3052
3053
3054@@ -647,36 +693,19 @@
3055
3056 :param item: Service Item containing theme and location information
3057 """
3058- style = """
3059-.lyricstable {
3060- z-index: 5;
3061- position: absolute;
3062- display: table;
3063- %s
3064-}
3065-.lyricscell {
3066- display: table-cell;
3067- word-wrap: break-word;
3068- -webkit-transition: opacity 0.4s ease;
3069- %s
3070-}
3071-.lyricsmain {
3072- %s
3073-}
3074-"""
3075 theme_data = item.theme_data
3076 lyricstable = ''
3077 lyrics = ''
3078 lyricsmain = ''
3079 if theme_data and item.main:
3080- lyricstable = 'left: %spx; top: %spx;' % (item.main.x(), item.main.y())
3081+ lyricstable = 'left: {left}px; top: {top}px;'.format(left=item.main.x(), top=item.main.y())
3082 lyrics = build_lyrics_format_css(theme_data, item.main.width(), item.main.height())
3083 lyricsmain += build_lyrics_outline_css(theme_data)
3084 if theme_data.font_main_shadow:
3085- lyricsmain += ' text-shadow: %s %spx %spx;' % \
3086- (theme_data.font_main_shadow_color, theme_data.font_main_shadow_size, theme_data.font_main_shadow_size)
3087- lyrics_css = style % (lyricstable, lyrics, lyricsmain)
3088- return lyrics_css
3089+ lyricsmain += ' text-shadow: {theme} {shadow}px ' \
3090+ '{shadow}px;'.format(theme=theme_data.font_main_shadow_color,
3091+ shadow=theme_data.font_main_shadow_size)
3092+ return LYRICS_SRC.substitute(stable=lyricstable, lyrics=lyrics, main=lyricsmain)
3093
3094
3095 def build_lyrics_outline_css(theme_data):
3096@@ -689,7 +718,9 @@
3097 size = float(theme_data.font_main_outline_size) / 16
3098 fill_color = theme_data.font_main_color
3099 outline_color = theme_data.font_main_outline_color
3100- return ' -webkit-text-stroke: %sem %s; -webkit-text-fill-color: %s; ' % (size, outline_color, fill_color)
3101+ return ' -webkit-text-stroke: {size}em {color}; -webkit-text-fill-color: {fill}; '.format(size=size,
3102+ color=outline_color,
3103+ fill=fill_color)
3104 return ''
3105
3106
3107@@ -703,30 +734,23 @@
3108 """
3109 align = HorizontalType.Names[theme_data.display_horizontal_align]
3110 valign = VerticalType.Names[theme_data.display_vertical_align]
3111- if theme_data.font_main_outline:
3112- left_margin = int(theme_data.font_main_outline_size) * 2
3113- else:
3114- left_margin = 0
3115- justify = 'white-space:pre-wrap;'
3116+ left_margin = (int(theme_data.font_main_outline_size) * 2) if theme_data.font_main_outline else 0
3117 # fix tag incompatibilities
3118- if theme_data.display_horizontal_align == HorizontalType.Justify:
3119- justify = ''
3120- if theme_data.display_vertical_align == VerticalType.Bottom:
3121- padding_bottom = '0.5em'
3122- else:
3123- padding_bottom = '0'
3124- lyrics = '%s word-wrap: break-word; ' \
3125- 'text-align: %s; vertical-align: %s; font-family: %s; ' \
3126- 'font-size: %spt; color: %s; line-height: %d%%; margin: 0;' \
3127- 'padding: 0; padding-bottom: %s; padding-left: %spx; width: %spx; height: %spx; ' % \
3128- (justify, align, valign, theme_data.font_main_name, theme_data.font_main_size,
3129- theme_data.font_main_color, 100 + int(theme_data.font_main_line_adjustment), padding_bottom,
3130- left_margin, width, height)
3131- if theme_data.font_main_italics:
3132- lyrics += 'font-style:italic; '
3133- if theme_data.font_main_bold:
3134- lyrics += 'font-weight:bold; '
3135- return lyrics
3136+ justify = '' if (theme_data.display_horizontal_align == HorizontalType.Justify) else ' white-space: pre-wrap;\n'
3137+ padding_bottom = '0.5em' if (theme_data.display_vertical_align == VerticalType.Bottom) else '0'
3138+ return LYRICS_FORMAT_SRC.substitute(justify=justify,
3139+ align=align,
3140+ valign=valign,
3141+ font=theme_data.font_main_name,
3142+ size=theme_data.font_main_size,
3143+ color=theme_data.font_main_color,
3144+ line='{line:d}'.format(line=100 + int(theme_data.font_main_line_adjustment)),
3145+ bottom=padding_bottom,
3146+ left=left_margin,
3147+ width=width,
3148+ height=height,
3149+ font_style='\n font-style: italic;' if theme_data.font_main_italics else '',
3150+ font_weight='\n font-weight: bold;' if theme_data.font_main_bold else '')
3151
3152
3153 def build_footer_css(item, height):
3154@@ -736,21 +760,11 @@
3155 :param item: Service Item to be processed.
3156 :param height:
3157 """
3158- style = """
3159- left: %spx;
3160- bottom: %spx;
3161- width: %spx;
3162- font-family: %s;
3163- font-size: %spt;
3164- color: %s;
3165- text-align: left;
3166- white-space: %s;
3167- """
3168 theme = item.theme_data
3169 if not theme or not item.footer:
3170 return ''
3171 bottom = height - int(item.footer.y()) - int(item.footer.height())
3172 whitespace = 'normal' if Settings().value('themes/wrap footer') else 'nowrap'
3173- lyrics_html = style % (item.footer.x(), bottom, item.footer.width(),
3174- theme.font_footer_name, theme.font_footer_size, theme.font_footer_color, whitespace)
3175- return lyrics_html
3176+ return FOOTER_SRC.substitute(left=item.footer.x(), bottom=bottom, width=item.footer.width(),
3177+ family=theme.font_footer_name, size=theme.font_footer_size,
3178+ color=theme.font_footer_color, space=whitespace)
3179
3180=== modified file 'openlp/core/lib/imagemanager.py'
3181--- openlp/core/lib/imagemanager.py 2016-12-31 11:05:48 +0000
3182+++ openlp/core/lib/imagemanager.py 2017-01-08 22:05:36 +0000
3183@@ -236,7 +236,7 @@
3184 """
3185 Return the ``QImage`` from the cache. If not present wait for the background thread to process it.
3186 """
3187- log.debug('getImage %s' % path)
3188+ log.debug('getImage {path}'.format(path=path))
3189 image = self._cache[(path, source, width, height)]
3190 if image.image is None:
3191 self._conversion_queue.modify_priority(image, Priority.High)
3192@@ -256,7 +256,7 @@
3193 """
3194 Returns the byte string for an image. If not present wait for the background thread to process it.
3195 """
3196- log.debug('get_image_bytes %s' % path)
3197+ log.debug('get_image_bytes {path}'.format(path=path))
3198 image = self._cache[(path, source, width, height)]
3199 if image.image_bytes is None:
3200 self._conversion_queue.modify_priority(image, Priority.Urgent)
3201@@ -271,8 +271,8 @@
3202 """
3203 Add image to cache if it is not already there.
3204 """
3205- log.debug('add_image %s' % path)
3206- if not (path, source, width, height) in self._cache:
3207+ log.debug('add_image {path}'.format(path=path))
3208+ if (path, source, width, height) not in self._cache:
3209 image = Image(path, source, background, width, height)
3210 self._cache[(path, source, width, height)] = image
3211 self._conversion_queue.put((image.priority, image.secondary_priority, image))
3212
3213=== renamed file 'openlp/core/lib/listwidgetwithdnd.py' => 'openlp/core/lib/listwidgetwithdnd.py.THIS'
3214=== modified file 'openlp/core/lib/mediamanageritem.py'
3215--- openlp/core/lib/mediamanageritem.py 2016-12-31 11:05:48 +0000
3216+++ openlp/core/lib/mediamanageritem.py 2017-01-08 22:05:36 +0000
3217@@ -29,10 +29,11 @@
3218 from PyQt5 import QtCore, QtGui, QtWidgets
3219
3220 from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings, translate
3221-from openlp.core.lib import FileDialog, OpenLPToolbar, ServiceItem, StringContent, ListWidgetWithDnD, \
3222- ServiceItemContext
3223+from openlp.core.lib import FileDialog, ServiceItem, StringContent, ServiceItemContext
3224 from openlp.core.lib.searchedit import SearchEdit
3225 from openlp.core.lib.ui import create_widget_action, critical_error_message_box
3226+from openlp.core.ui.lib.listwidgetwithdnd import ListWidgetWithDnD
3227+from openlp.core.ui.lib.toolbar import OpenLPToolbar
3228
3229 log = logging.getLogger(__name__)
3230
3231@@ -185,7 +186,7 @@
3232 for action in toolbar_actions:
3233 if action[0] == StringContent.Preview:
3234 self.toolbar.addSeparator()
3235- self.toolbar.add_toolbar_action('%s%sAction' % (self.plugin.name, action[0]),
3236+ self.toolbar.add_toolbar_action('{name}{action}Action'.format(name=self.plugin.name, action=action[0]),
3237 text=self.plugin.get_string(action[1])['title'], icon=action[2],
3238 tooltip=self.plugin.get_string(action[1])['tooltip'],
3239 triggers=action[3])
3240@@ -199,7 +200,7 @@
3241 self.list_view.setSpacing(1)
3242 self.list_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
3243 self.list_view.setAlternatingRowColors(True)
3244- self.list_view.setObjectName('%sListView' % self.plugin.name)
3245+ self.list_view.setObjectName('{name}ListView'.format(name=self.plugin.name))
3246 # Add to page_layout
3247 self.page_layout.addWidget(self.list_view)
3248 # define and add the context menu
3249@@ -211,19 +212,22 @@
3250 triggers=self.on_edit_click)
3251 create_widget_action(self.list_view, separator=True)
3252 create_widget_action(self.list_view,
3253- 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Preview.title()),
3254+ 'listView{plugin}{preview}Item'.format(plugin=self.plugin.name.title(),
3255+ preview=StringContent.Preview.title()),
3256 text=self.plugin.get_string(StringContent.Preview)['title'],
3257 icon=':/general/general_preview.png',
3258 can_shortcuts=True,
3259 triggers=self.on_preview_click)
3260 create_widget_action(self.list_view,
3261- 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Live.title()),
3262+ 'listView{plugin}{live}Item'.format(plugin=self.plugin.name.title(),
3263+ live=StringContent.Live.title()),
3264 text=self.plugin.get_string(StringContent.Live)['title'],
3265 icon=':/general/general_live.png',
3266 can_shortcuts=True,
3267 triggers=self.on_live_click)
3268 create_widget_action(self.list_view,
3269- 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Service.title()),
3270+ 'listView{plugin}{service}Item'.format(plugin=self.plugin.name.title(),
3271+ service=StringContent.Service.title()),
3272 can_shortcuts=True,
3273 text=self.plugin.get_string(StringContent.Service)['title'],
3274 icon=':/general/general_add.png',
3275@@ -231,7 +235,8 @@
3276 if self.has_delete_icon:
3277 create_widget_action(self.list_view, separator=True)
3278 create_widget_action(self.list_view,
3279- 'listView%s%sItem' % (self.plugin.name.title(), StringContent.Delete.title()),
3280+ 'listView{plugin}{delete}Item'.format(plugin=self.plugin.name.title(),
3281+ delete=StringContent.Delete.title()),
3282 text=self.plugin.get_string(StringContent.Delete)['title'],
3283 icon=':/general/general_delete.png',
3284 can_shortcuts=True, triggers=self.on_delete_click)
3285@@ -261,7 +266,7 @@
3286 self.search_text_layout.setObjectName('search_text_layout')
3287 self.search_text_label = QtWidgets.QLabel(self.search_widget)
3288 self.search_text_label.setObjectName('search_text_label')
3289- self.search_text_edit = SearchEdit(self.search_widget)
3290+ self.search_text_edit = SearchEdit(self.search_widget, self.settings_section)
3291 self.search_text_edit.setObjectName('search_text_edit')
3292 self.search_text_label.setBuddy(self.search_text_edit)
3293 self.search_text_layout.addRow(self.search_text_label, self.search_text_edit)
3294@@ -312,7 +317,7 @@
3295 files = FileDialog.getOpenFileNames(self, self.on_new_prompt,
3296 Settings().value(self.settings_section + '/last directory'),
3297 self.on_new_file_masks)
3298- log.info('New files(s) %s' % files)
3299+ log.info('New files(s) {files}'.format(files=files))
3300 if files:
3301 self.application.set_busy_cursor()
3302 self.validate_and_load(files)
3303@@ -332,7 +337,8 @@
3304 if not error_shown:
3305 critical_error_message_box(translate('OpenLP.MediaManagerItem', 'Invalid File Type'),
3306 translate('OpenLP.MediaManagerItem',
3307- 'Invalid File %s.\nSuffix not supported') % file_name)
3308+ 'Invalid File {name}.\n'
3309+ 'Suffix not supported').format(name=file_name))
3310 error_shown = True
3311 else:
3312 new_files.append(file_name)
3313@@ -374,7 +380,8 @@
3314 self.load_list(full_list, target_group)
3315 last_dir = os.path.split(files[0])[0]
3316 Settings().setValue(self.settings_section + '/last directory', last_dir)
3317- Settings().setValue('%s/%s files' % (self.settings_section, self.settings_section), self.get_file_list())
3318+ Settings().setValue('{section}/{section} files'.format(section=self.settings_section),
3319+ self.get_file_list())
3320 if duplicates_found:
3321 critical_error_message_box(UiStrings().Duplicate,
3322 translate('OpenLP.MediaManagerItem',
3323@@ -390,8 +397,6 @@
3324 # Decide if we have to show the context menu or not.
3325 if item is None:
3326 return
3327- if not item.flags() & QtCore.Qt.ItemIsSelectable:
3328- return
3329 self.menu.exec(self.list_view.mapToGlobal(point))
3330
3331 def get_file_list(self):
3332@@ -481,6 +486,7 @@
3333 'You must select one or more items to preview.'))
3334 else:
3335 log.debug('%s Preview requested' % self.plugin.name)
3336+ Registry().set_flag('has doubleclick added item to service', False)
3337 service_item = self.build_service_item()
3338 if service_item:
3339 service_item.from_plugin = True
3340@@ -549,7 +555,7 @@
3341 # Is it possible to process multiple list items to generate
3342 # multiple service items?
3343 if self.single_service_item:
3344- log.debug('%s Add requested', self.plugin.name)
3345+ log.debug('{plugin} Add requested'.format(plugin=self.plugin.name))
3346 self.add_to_service(replace=self.remote_triggered)
3347 else:
3348 items = self.list_view.selectedIndexes()
3349@@ -590,7 +596,7 @@
3350 translate('OpenLP.MediaManagerItem',
3351 'You must select one or more items.'))
3352 else:
3353- log.debug('%s Add requested', self.plugin.name)
3354+ log.debug('{plugin} Add requested'.format(plugin=self.plugin.name))
3355 service_item = self.service_manager.get_service_item()
3356 if not service_item:
3357 QtWidgets.QMessageBox.information(self, UiStrings().NISs,
3358@@ -603,7 +609,8 @@
3359 # Turn off the remote edit update message indicator
3360 QtWidgets.QMessageBox.information(self, translate('OpenLP.MediaManagerItem', 'Invalid Service Item'),
3361 translate('OpenLP.MediaManagerItem',
3362- 'You must select a %s service item.') % self.title)
3363+ 'You must select a {title} '
3364+ 'service item.').format(title=self.title))
3365
3366 def build_service_item(self, item=None, xml_version=False, remote=False, context=ServiceItemContext.Live):
3367 """
3368@@ -629,20 +636,6 @@
3369 """
3370 return item
3371
3372- def check_search_result(self):
3373- """
3374- Checks if the list_view is empty and adds a "No Search Results" item.
3375- """
3376- if self.list_view.count():
3377- return
3378- message = translate('OpenLP.MediaManagerItem', 'No Search Results')
3379- item = QtWidgets.QListWidgetItem(message)
3380- item.setFlags(QtCore.Qt.NoItemFlags)
3381- font = QtGui.QFont()
3382- font.setItalic(True)
3383- item.setFont(font)
3384- self.list_view.addItem(item)
3385-
3386 def _get_id_of_item_to_generate(self, item, remote_item):
3387 """
3388 Utility method to check items being submitted for slide generation.
3389
3390=== modified file 'openlp/core/lib/plugin.py'
3391--- openlp/core/lib/plugin.py 2016-12-31 11:05:48 +0000
3392+++ openlp/core/lib/plugin.py 2017-01-08 22:05:36 +0000
3393@@ -24,11 +24,10 @@
3394 """
3395 import logging
3396
3397-
3398 from PyQt5 import QtCore
3399
3400 from openlp.core.common import Registry, RegistryProperties, Settings, UiStrings
3401-from openlp.core.utils import get_application_version
3402+from openlp.core.common.versionchecker import get_application_version
3403
3404 log = logging.getLogger(__name__)
3405
3406@@ -131,7 +130,7 @@
3407 :param settings_tab_class: The class name of the plugin's settings tab.
3408 :param version: Defaults to *None*, which means that the same version number is used as OpenLP's version number.
3409 """
3410- log.debug('Plugin %s initialised' % name)
3411+ log.debug('Plugin {plugin} initialised'.format(plugin=name))
3412 super(Plugin, self).__init__()
3413 self.name = name
3414 self.text_strings = {}
3415@@ -155,11 +154,11 @@
3416 # Append a setting for files in the mediamanager (note not all plugins
3417 # which have a mediamanager need this).
3418 if media_item_class is not None:
3419- default_settings['%s/%s files' % (name, name)] = []
3420+ default_settings['{name}/{name} files'.format(name=name)] = []
3421 # Add settings to the dict of all settings.
3422 Settings.extend_default_settings(default_settings)
3423- Registry().register_function('%s_add_service_item' % self.name, self.process_add_service_event)
3424- Registry().register_function('%s_config_updated' % self.name, self.config_update)
3425+ Registry().register_function('{name}_add_service_item'.format(name=self.name), self.process_add_service_event)
3426+ Registry().register_function('{name}_config_updated'.format(name=self.name), self.config_update)
3427
3428 def check_pre_conditions(self):
3429 """
3430@@ -257,7 +256,7 @@
3431 """
3432 Generic Drag and drop handler triggered from service_manager.
3433 """
3434- log.debug('process_add_service_event event called for plugin %s' % self.name)
3435+ log.debug('process_add_service_event event called for plugin {name}'.format(name=self.name))
3436 if replace:
3437 self.media_item.on_add_edit_click()
3438 else:
3439
3440=== modified file 'openlp/core/lib/pluginmanager.py'
3441--- openlp/core/lib/pluginmanager.py 2016-12-31 11:05:48 +0000
3442+++ openlp/core/lib/pluginmanager.py 2017-01-08 22:05:36 +0000
3443@@ -23,7 +23,6 @@
3444 Provide plugin management
3445 """
3446 import os
3447-import sys
3448 import imp
3449
3450 from openlp.core.lib import Plugin, PluginStatus
3451@@ -43,7 +42,7 @@
3452 super(PluginManager, self).__init__(parent)
3453 self.log_info('Plugin manager Initialising')
3454 self.base_path = os.path.abspath(AppLocation.get_directory(AppLocation.PluginsDir))
3455- self.log_debug('Base path %s ' % self.base_path)
3456+ self.log_debug('Base path {path}'.format(path=self.base_path))
3457 self.plugins = []
3458 self.log_info('Plugin manager Initialised')
3459
3460@@ -73,7 +72,7 @@
3461 """
3462 start_depth = len(os.path.abspath(self.base_path).split(os.sep))
3463 present_plugin_dir = os.path.join(self.base_path, 'presentations')
3464- self.log_debug('finding plugins in %s at depth %d' % (self.base_path, start_depth))
3465+ self.log_debug('finding plugins in {path} at depth {depth:d}'.format(path=self.base_path, depth=start_depth))
3466 for root, dirs, files in os.walk(self.base_path):
3467 for name in files:
3468 if name.endswith('.py') and not name.startswith('__'):
3469@@ -84,7 +83,9 @@
3470 break
3471 module_name = name[:-3]
3472 # import the modules
3473- self.log_debug('Importing %s from %s. Depth %d' % (module_name, root, this_depth))
3474+ self.log_debug('Importing {name} from {root}. Depth {depth:d}'.format(name=module_name,
3475+ root=root,
3476+ depth=this_depth))
3477 try:
3478 # Use the "imp" library to try to get around a problem with the PyUNO library which
3479 # monkey-patches the __import__ function to do some magic. This causes issues with our tests.
3480@@ -93,21 +94,21 @@
3481 # Then load the module (do the actual import) using the details from find_module()
3482 imp.load_module(module_name, fp, path_name, description)
3483 except ImportError as e:
3484- self.log_exception('Failed to import module %s on path %s: %s'
3485- % (module_name, path, e.args[0]))
3486+ self.log_exception('Failed to import module {name} on path {path}: '
3487+ '{args}'.format(name=module_name, path=path, args=e.args[0]))
3488 plugin_classes = Plugin.__subclasses__()
3489 plugin_objects = []
3490 for p in plugin_classes:
3491 try:
3492 plugin = p()
3493- self.log_debug('Loaded plugin %s' % str(p))
3494+ self.log_debug('Loaded plugin {plugin}'.format(plugin=str(p)))
3495 plugin_objects.append(plugin)
3496 except TypeError:
3497- self.log_exception('Failed to load plugin %s' % str(p))
3498+ self.log_exception('Failed to load plugin {plugin}'.format(plugin=str(p)))
3499 plugins_list = sorted(plugin_objects, key=lambda plugin: plugin.weight)
3500 for plugin in plugins_list:
3501 if plugin.check_pre_conditions():
3502- self.log_debug('Plugin %s active' % str(plugin.name))
3503+ self.log_debug('Plugin {plugin} active'.format(plugin=str(plugin.name)))
3504 plugin.set_status()
3505 else:
3506 plugin.status = PluginStatus.Disabled
3507@@ -175,10 +176,11 @@
3508 Loop through all the plugins and give them an opportunity to initialise themselves.
3509 """
3510 for plugin in self.plugins:
3511- self.log_info('initialising plugins %s in a %s state' % (plugin.name, plugin.is_active()))
3512+ self.log_info('initialising plugins {plugin} in a {state} state'.format(plugin=plugin.name,
3513+ state=plugin.is_active()))
3514 if plugin.is_active():
3515 plugin.initialise()
3516- self.log_info('Initialisation Complete for %s ' % plugin.name)
3517+ self.log_info('Initialisation Complete for {plugin}'.format(plugin=plugin.name))
3518
3519 def finalise_plugins(self):
3520 """
3521@@ -187,7 +189,7 @@
3522 for plugin in self.plugins:
3523 if plugin.is_active():
3524 plugin.finalise()
3525- self.log_info('Finalisation Complete for %s ' % plugin.name)
3526+ self.log_info('Finalisation Complete for {plugin}'.format(plugin=plugin.name))
3527
3528 def get_plugin_by_name(self, name):
3529 """
3530
3531=== modified file 'openlp/core/lib/projector/constants.py'
3532--- openlp/core/lib/projector/constants.py 2016-12-31 11:05:48 +0000
3533+++ openlp/core/lib/projector/constants.py 2017-01-08 22:05:36 +0000
3534@@ -131,169 +131,181 @@
3535 S_NETWORK_SENDING = 400
3536 S_NETWORK_RECEIVED = 401
3537
3538-CONNECTION_ERRORS = {E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS,
3539- E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION,
3540- E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT,
3541- E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE,
3542- E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED,
3543- E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED,
3544- E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND,
3545- E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR
3546- }
3547+CONNECTION_ERRORS = {
3548+ E_NOT_CONNECTED, E_NO_AUTHENTICATION, E_AUTHENTICATION, E_CLASS,
3549+ E_PREFIX, E_CONNECTION_REFUSED, E_REMOTE_HOST_CLOSED_CONNECTION,
3550+ E_HOST_NOT_FOUND, E_SOCKET_ACCESS, E_SOCKET_RESOURCE, E_SOCKET_TIMEOUT,
3551+ E_DATAGRAM_TOO_LARGE, E_NETWORK, E_ADDRESS_IN_USE, E_SOCKET_ADDRESS_NOT_AVAILABLE,
3552+ E_UNSUPPORTED_SOCKET_OPERATION, E_PROXY_AUTHENTICATION_REQUIRED,
3553+ E_SLS_HANDSHAKE_FAILED, E_UNFINISHED_SOCKET_OPERATION, E_PROXY_CONNECTION_REFUSED,
3554+ E_PROXY_CONNECTION_CLOSED, E_PROXY_CONNECTION_TIMEOUT, E_PROXY_NOT_FOUND,
3555+ E_PROXY_PROTOCOL, E_UNKNOWN_SOCKET_ERROR
3556+}
3557
3558-PJLINK_ERRORS = {'ERRA': E_AUTHENTICATION, # Authentication error
3559- 'ERR1': E_UNDEFINED, # Undefined command error
3560- 'ERR2': E_PARAMETER, # Invalid parameter error
3561- 'ERR3': E_UNAVAILABLE, # Projector busy
3562- 'ERR4': E_PROJECTOR, # Projector or display failure
3563- E_AUTHENTICATION: 'ERRA',
3564- E_UNDEFINED: 'ERR1',
3565- E_PARAMETER: 'ERR2',
3566- E_UNAVAILABLE: 'ERR3',
3567- E_PROJECTOR: 'ERR4'}
3568+PJLINK_ERRORS = {
3569+ 'ERRA': E_AUTHENTICATION, # Authentication error
3570+ 'ERR1': E_UNDEFINED, # Undefined command error
3571+ 'ERR2': E_PARAMETER, # Invalid parameter error
3572+ 'ERR3': E_UNAVAILABLE, # Projector busy
3573+ 'ERR4': E_PROJECTOR, # Projector or display failure
3574+ E_AUTHENTICATION: 'ERRA',
3575+ E_UNDEFINED: 'ERR1',
3576+ E_PARAMETER: 'ERR2',
3577+ E_UNAVAILABLE: 'ERR3',
3578+ E_PROJECTOR: 'ERR4'
3579+}
3580
3581 # Map error/status codes to string
3582-ERROR_STRING = {0: 'S_OK',
3583- E_GENERAL: 'E_GENERAL',
3584- E_NOT_CONNECTED: 'E_NOT_CONNECTED',
3585- E_FAN: 'E_FAN',
3586- E_LAMP: 'E_LAMP',
3587- E_TEMP: 'E_TEMP',
3588- E_COVER: 'E_COVER',
3589- E_FILTER: 'E_FILTER',
3590- E_AUTHENTICATION: 'E_AUTHENTICATION',
3591- E_NO_AUTHENTICATION: 'E_NO_AUTHENTICATION',
3592- E_UNDEFINED: 'E_UNDEFINED',
3593- E_PARAMETER: 'E_PARAMETER',
3594- E_UNAVAILABLE: 'E_UNAVAILABLE',
3595- E_PROJECTOR: 'E_PROJECTOR',
3596- E_INVALID_DATA: 'E_INVALID_DATA',
3597- E_WARN: 'E_WARN',
3598- E_ERROR: 'E_ERROR',
3599- E_CLASS: 'E_CLASS',
3600- E_PREFIX: 'E_PREFIX', # Last projector error
3601- E_CONNECTION_REFUSED: 'E_CONNECTION_REFUSED', # First QtSocket error
3602- E_REMOTE_HOST_CLOSED_CONNECTION: 'E_REMOTE_HOST_CLOSED_CONNECTION',
3603- E_HOST_NOT_FOUND: 'E_HOST_NOT_FOUND',
3604- E_SOCKET_ACCESS: 'E_SOCKET_ACCESS',
3605- E_SOCKET_RESOURCE: 'E_SOCKET_RESOURCE',
3606- E_SOCKET_TIMEOUT: 'E_SOCKET_TIMEOUT',
3607- E_DATAGRAM_TOO_LARGE: 'E_DATAGRAM_TOO_LARGE',
3608- E_NETWORK: 'E_NETWORK',
3609- E_ADDRESS_IN_USE: 'E_ADDRESS_IN_USE',
3610- E_SOCKET_ADDRESS_NOT_AVAILABLE: 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
3611- E_UNSUPPORTED_SOCKET_OPERATION: 'E_UNSUPPORTED_SOCKET_OPERATION',
3612- E_PROXY_AUTHENTICATION_REQUIRED: 'E_PROXY_AUTHENTICATION_REQUIRED',
3613- E_SLS_HANDSHAKE_FAILED: 'E_SLS_HANDSHAKE_FAILED',
3614- E_UNFINISHED_SOCKET_OPERATION: 'E_UNFINISHED_SOCKET_OPERATION',
3615- E_PROXY_CONNECTION_REFUSED: 'E_PROXY_CONNECTION_REFUSED',
3616- E_PROXY_CONNECTION_CLOSED: 'E_PROXY_CONNECTION_CLOSED',
3617- E_PROXY_CONNECTION_TIMEOUT: 'E_PROXY_CONNECTION_TIMEOUT',
3618- E_PROXY_NOT_FOUND: 'E_PROXY_NOT_FOUND',
3619- E_PROXY_PROTOCOL: 'E_PROXY_PROTOCOL',
3620- E_UNKNOWN_SOCKET_ERROR: 'E_UNKNOWN_SOCKET_ERROR'}
3621+ERROR_STRING = {
3622+ 0: 'S_OK',
3623+ E_GENERAL: 'E_GENERAL',
3624+ E_NOT_CONNECTED: 'E_NOT_CONNECTED',
3625+ E_FAN: 'E_FAN',
3626+ E_LAMP: 'E_LAMP',
3627+ E_TEMP: 'E_TEMP',
3628+ E_COVER: 'E_COVER',
3629+ E_FILTER: 'E_FILTER',
3630+ E_AUTHENTICATION: 'E_AUTHENTICATION',
3631+ E_NO_AUTHENTICATION: 'E_NO_AUTHENTICATION',
3632+ E_UNDEFINED: 'E_UNDEFINED',
3633+ E_PARAMETER: 'E_PARAMETER',
3634+ E_UNAVAILABLE: 'E_UNAVAILABLE',
3635+ E_PROJECTOR: 'E_PROJECTOR',
3636+ E_INVALID_DATA: 'E_INVALID_DATA',
3637+ E_WARN: 'E_WARN',
3638+ E_ERROR: 'E_ERROR',
3639+ E_CLASS: 'E_CLASS',
3640+ E_PREFIX: 'E_PREFIX', # Last projector error
3641+ E_CONNECTION_REFUSED: 'E_CONNECTION_REFUSED', # First QtSocket error
3642+ E_REMOTE_HOST_CLOSED_CONNECTION: 'E_REMOTE_HOST_CLOSED_CONNECTION',
3643+ E_HOST_NOT_FOUND: 'E_HOST_NOT_FOUND',
3644+ E_SOCKET_ACCESS: 'E_SOCKET_ACCESS',
3645+ E_SOCKET_RESOURCE: 'E_SOCKET_RESOURCE',
3646+ E_SOCKET_TIMEOUT: 'E_SOCKET_TIMEOUT',
3647+ E_DATAGRAM_TOO_LARGE: 'E_DATAGRAM_TOO_LARGE',
3648+ E_NETWORK: 'E_NETWORK',
3649+ E_ADDRESS_IN_USE: 'E_ADDRESS_IN_USE',
3650+ E_SOCKET_ADDRESS_NOT_AVAILABLE: 'E_SOCKET_ADDRESS_NOT_AVAILABLE',
3651+ E_UNSUPPORTED_SOCKET_OPERATION: 'E_UNSUPPORTED_SOCKET_OPERATION',
3652+ E_PROXY_AUTHENTICATION_REQUIRED: 'E_PROXY_AUTHENTICATION_REQUIRED',
3653+ E_SLS_HANDSHAKE_FAILED: 'E_SLS_HANDSHAKE_FAILED',
3654+ E_UNFINISHED_SOCKET_OPERATION: 'E_UNFINISHED_SOCKET_OPERATION',
3655+ E_PROXY_CONNECTION_REFUSED: 'E_PROXY_CONNECTION_REFUSED',
3656+ E_PROXY_CONNECTION_CLOSED: 'E_PROXY_CONNECTION_CLOSED',
3657+ E_PROXY_CONNECTION_TIMEOUT: 'E_PROXY_CONNECTION_TIMEOUT',
3658+ E_PROXY_NOT_FOUND: 'E_PROXY_NOT_FOUND',
3659+ E_PROXY_PROTOCOL: 'E_PROXY_PROTOCOL',
3660+ E_UNKNOWN_SOCKET_ERROR: 'E_UNKNOWN_SOCKET_ERROR'
3661+}
3662
3663-STATUS_STRING = {S_NOT_CONNECTED: 'S_NOT_CONNECTED',
3664- S_CONNECTING: 'S_CONNECTING',
3665- S_CONNECTED: 'S_CONNECTED',
3666- S_STATUS: 'S_STATUS',
3667- S_OFF: 'S_OFF',
3668- S_INITIALIZE: 'S_INITIALIZE',
3669- S_STANDBY: 'S_STANDBY',
3670- S_WARMUP: 'S_WARMUP',
3671- S_ON: 'S_ON',
3672- S_COOLDOWN: 'S_COOLDOWN',
3673- S_INFO: 'S_INFO',
3674- S_NETWORK_SENDING: 'S_NETWORK_SENDING',
3675- S_NETWORK_RECEIVED: 'S_NETWORK_RECEIVED'}
3676+STATUS_STRING = {
3677+ S_NOT_CONNECTED: 'S_NOT_CONNECTED',
3678+ S_CONNECTING: 'S_CONNECTING',
3679+ S_CONNECTED: 'S_CONNECTED',
3680+ S_STATUS: 'S_STATUS',
3681+ S_OFF: 'S_OFF',
3682+ S_INITIALIZE: 'S_INITIALIZE',
3683+ S_STANDBY: 'S_STANDBY',
3684+ S_WARMUP: 'S_WARMUP',
3685+ S_ON: 'S_ON',
3686+ S_COOLDOWN: 'S_COOLDOWN',
3687+ S_INFO: 'S_INFO',
3688+ S_NETWORK_SENDING: 'S_NETWORK_SENDING',
3689+ S_NETWORK_RECEIVED: 'S_NETWORK_RECEIVED'
3690+}
3691
3692 # Map error/status codes to message strings
3693-ERROR_MSG = {E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK
3694- E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'),
3695- E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'),
3696- E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'),
3697- E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'),
3698- E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'),
3699- E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'),
3700- E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'),
3701- E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'),
3702- E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'),
3703- E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'),
3704- E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'),
3705- E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'),
3706- E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invalid packet received'),
3707- E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'),
3708- E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'),
3709- E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'),
3710- E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'),
3711- E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
3712- 'The connection was refused by the peer (or timed out)'),
3713- E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
3714- 'The remote host closed the connection'),
3715- E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'),
3716- E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants',
3717- 'The socket operation failed because the application '
3718- 'lacked the required privileges'),
3719- E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants',
3720- 'The local system ran out of resources (e.g., too many sockets)'),
3721- E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants',
3722- 'The socket operation timed out'),
3723- E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants',
3724- 'The datagram was larger than the operating system\'s limit'),
3725- E_NETWORK: translate('OpenLP.ProjectorConstants',
3726- 'An error occurred with the network (Possibly someone pulled the plug?)'),
3727- E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants',
3728- 'The address specified with socket.bind() '
3729- 'is already in use and was set to be exclusive'),
3730- E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
3731- 'The address specified to socket.bind() '
3732- 'does not belong to the host'),
3733- E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
3734- 'The requested socket operation is not supported by the local '
3735- 'operating system (e.g., lack of IPv6 support)'),
3736- E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
3737- 'The socket is using a proxy, '
3738- 'and the proxy requires authentication'),
3739- E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants',
3740- 'The SSL/TLS handshake failed'),
3741- E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
3742- 'The last operation attempted has not finished yet '
3743- '(still in progress in the background)'),
3744- E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
3745- 'Could not contact the proxy server because the connection '
3746- 'to that server was denied'),
3747- E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants',
3748- 'The connection to the proxy server was closed unexpectedly '
3749- '(before the connection to the final peer was established)'),
3750- E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants',
3751- 'The connection to the proxy server timed out or the proxy '
3752- 'server stopped responding in the authentication phase.'),
3753- E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants',
3754- 'The proxy address set with setProxy() was not found'),
3755- E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants',
3756- 'The connection negotiation with the proxy server failed because the '
3757- 'response from the proxy server could not be understood'),
3758- E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'),
3759- S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'),
3760- S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'),
3761- S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'),
3762- S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'),
3763- S_OFF: translate('OpenLP.ProjectorConstants', 'Off'),
3764- S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'),
3765- S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'),
3766- S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'),
3767- S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'),
3768- S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'),
3769- S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'),
3770- S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'),
3771- S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')}
3772+ERROR_MSG = {
3773+ E_OK: translate('OpenLP.ProjectorConstants', 'OK'), # E_OK | S_OK
3774+ E_GENERAL: translate('OpenLP.ProjectorConstants', 'General projector error'),
3775+ E_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected error'),
3776+ E_LAMP: translate('OpenLP.ProjectorConstants', 'Lamp error'),
3777+ E_FAN: translate('OpenLP.ProjectorConstants', 'Fan error'),
3778+ E_TEMP: translate('OpenLP.ProjectorConstants', 'High temperature detected'),
3779+ E_COVER: translate('OpenLP.ProjectorConstants', 'Cover open detected'),
3780+ E_FILTER: translate('OpenLP.ProjectorConstants', 'Check filter'),
3781+ E_AUTHENTICATION: translate('OpenLP.ProjectorConstants', 'Authentication Error'),
3782+ E_UNDEFINED: translate('OpenLP.ProjectorConstants', 'Undefined Command'),
3783+ E_PARAMETER: translate('OpenLP.ProjectorConstants', 'Invalid Parameter'),
3784+ E_UNAVAILABLE: translate('OpenLP.ProjectorConstants', 'Projector Busy'),
3785+ E_PROJECTOR: translate('OpenLP.ProjectorConstants', 'Projector/Display Error'),
3786+ E_INVALID_DATA: translate('OpenLP.ProjectorConstants', 'Invalid packet received'),
3787+ E_WARN: translate('OpenLP.ProjectorConstants', 'Warning condition detected'),
3788+ E_ERROR: translate('OpenLP.ProjectorConstants', 'Error condition detected'),
3789+ E_CLASS: translate('OpenLP.ProjectorConstants', 'PJLink class not supported'),
3790+ E_PREFIX: translate('OpenLP.ProjectorConstants', 'Invalid prefix character'),
3791+ E_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
3792+ 'The connection was refused by the peer (or timed out)'),
3793+ E_REMOTE_HOST_CLOSED_CONNECTION: translate('OpenLP.ProjectorConstants',
3794+ 'The remote host closed the connection'),
3795+ E_HOST_NOT_FOUND: translate('OpenLP.ProjectorConstants', 'The host address was not found'),
3796+ E_SOCKET_ACCESS: translate('OpenLP.ProjectorConstants',
3797+ 'The socket operation failed because the application '
3798+ 'lacked the required privileges'),
3799+ E_SOCKET_RESOURCE: translate('OpenLP.ProjectorConstants',
3800+ 'The local system ran out of resources (e.g., too many sockets)'),
3801+ E_SOCKET_TIMEOUT: translate('OpenLP.ProjectorConstants',
3802+ 'The socket operation timed out'),
3803+ E_DATAGRAM_TOO_LARGE: translate('OpenLP.ProjectorConstants',
3804+ 'The datagram was larger than the operating system\'s limit'),
3805+ E_NETWORK: translate('OpenLP.ProjectorConstants',
3806+ 'An error occurred with the network (Possibly someone pulled the plug?)'),
3807+ E_ADDRESS_IN_USE: translate('OpenLP.ProjectorConstants',
3808+ 'The address specified with socket.bind() '
3809+ 'is already in use and was set to be exclusive'),
3810+ E_SOCKET_ADDRESS_NOT_AVAILABLE: translate('OpenLP.ProjectorConstants',
3811+ 'The address specified to socket.bind() '
3812+ 'does not belong to the host'),
3813+ E_UNSUPPORTED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
3814+ 'The requested socket operation is not supported by the local '
3815+ 'operating system (e.g., lack of IPv6 support)'),
3816+ E_PROXY_AUTHENTICATION_REQUIRED: translate('OpenLP.ProjectorConstants',
3817+ 'The socket is using a proxy, '
3818+ 'and the proxy requires authentication'),
3819+ E_SLS_HANDSHAKE_FAILED: translate('OpenLP.ProjectorConstants',
3820+ 'The SSL/TLS handshake failed'),
3821+ E_UNFINISHED_SOCKET_OPERATION: translate('OpenLP.ProjectorConstants',
3822+ 'The last operation attempted has not finished yet '
3823+ '(still in progress in the background)'),
3824+ E_PROXY_CONNECTION_REFUSED: translate('OpenLP.ProjectorConstants',
3825+ 'Could not contact the proxy server because the connection '
3826+ 'to that server was denied'),
3827+ E_PROXY_CONNECTION_CLOSED: translate('OpenLP.ProjectorConstants',
3828+ 'The connection to the proxy server was closed unexpectedly '
3829+ '(before the connection to the final peer was established)'),
3830+ E_PROXY_CONNECTION_TIMEOUT: translate('OpenLP.ProjectorConstants',
3831+ 'The connection to the proxy server timed out or the proxy '
3832+ 'server stopped responding in the authentication phase.'),
3833+ E_PROXY_NOT_FOUND: translate('OpenLP.ProjectorConstants',
3834+ 'The proxy address set with setProxy() was not found'),
3835+ E_PROXY_PROTOCOL: translate('OpenLP.ProjectorConstants',
3836+ 'The connection negotiation with the proxy server failed because the '
3837+ 'response from the proxy server could not be understood'),
3838+ E_UNKNOWN_SOCKET_ERROR: translate('OpenLP.ProjectorConstants', 'An unidentified error occurred'),
3839+ S_NOT_CONNECTED: translate('OpenLP.ProjectorConstants', 'Not connected'),
3840+ S_CONNECTING: translate('OpenLP.ProjectorConstants', 'Connecting'),
3841+ S_CONNECTED: translate('OpenLP.ProjectorConstants', 'Connected'),
3842+ S_STATUS: translate('OpenLP.ProjectorConstants', 'Getting status'),
3843+ S_OFF: translate('OpenLP.ProjectorConstants', 'Off'),
3844+ S_INITIALIZE: translate('OpenLP.ProjectorConstants', 'Initialize in progress'),
3845+ S_STANDBY: translate('OpenLP.ProjectorConstants', 'Power in standby'),
3846+ S_WARMUP: translate('OpenLP.ProjectorConstants', 'Warmup in progress'),
3847+ S_ON: translate('OpenLP.ProjectorConstants', 'Power is on'),
3848+ S_COOLDOWN: translate('OpenLP.ProjectorConstants', 'Cooldown in progress'),
3849+ S_INFO: translate('OpenLP.ProjectorConstants', 'Projector Information available'),
3850+ S_NETWORK_SENDING: translate('OpenLP.ProjectorConstants', 'Sending data'),
3851+ S_NETWORK_RECEIVED: translate('OpenLP.ProjectorConstants', 'Received data')
3852+}
3853
3854 # Map for ERST return codes to string
3855-PJLINK_ERST_STATUS = {'0': ERROR_STRING[E_OK],
3856- '1': ERROR_STRING[E_WARN],
3857- '2': ERROR_STRING[E_ERROR]}
3858+PJLINK_ERST_STATUS = {
3859+ '0': ERROR_STRING[E_OK],
3860+ '1': ERROR_STRING[E_WARN],
3861+ '2': ERROR_STRING[E_ERROR]
3862+}
3863
3864 # Map for POWR return codes to status code
3865+<<<<<<< TREE
3866 PJLINK_POWR_STATUS = {'0': S_STANDBY,
3867 '1': S_ON,
3868 '2': S_COOLDOWN,
3869@@ -355,3 +367,71 @@
3870 '58': translate('OpenLP.DB', 'Network 8'),
3871 '59': translate('OpenLP.DB', 'Network 9')
3872 }
3873+=======
3874+PJLINK_POWR_STATUS = {
3875+ '0': S_STANDBY,
3876+ '1': S_ON,
3877+ '2': S_COOLDOWN,
3878+ '3': S_WARMUP,
3879+ S_STANDBY: '0',
3880+ S_ON: '1',
3881+ S_COOLDOWN: '2',
3882+ S_WARMUP: '3'
3883+}
3884+
3885+PJLINK_DEFAULT_SOURCES = {
3886+ '1': translate('OpenLP.DB', 'RGB'),
3887+ '2': translate('OpenLP.DB', 'Video'),
3888+ '3': translate('OpenLP.DB', 'Digital'),
3889+ '4': translate('OpenLP.DB', 'Storage'),
3890+ '5': translate('OpenLP.DB', 'Network')
3891+}
3892+
3893+PJLINK_DEFAULT_CODES = {
3894+ '11': translate('OpenLP.DB', 'RGB 1'),
3895+ '12': translate('OpenLP.DB', 'RGB 2'),
3896+ '13': translate('OpenLP.DB', 'RGB 3'),
3897+ '14': translate('OpenLP.DB', 'RGB 4'),
3898+ '15': translate('OpenLP.DB', 'RGB 5'),
3899+ '16': translate('OpenLP.DB', 'RGB 6'),
3900+ '17': translate('OpenLP.DB', 'RGB 7'),
3901+ '18': translate('OpenLP.DB', 'RGB 8'),
3902+ '19': translate('OpenLP.DB', 'RGB 9'),
3903+ '21': translate('OpenLP.DB', 'Video 1'),
3904+ '22': translate('OpenLP.DB', 'Video 2'),
3905+ '23': translate('OpenLP.DB', 'Video 3'),
3906+ '24': translate('OpenLP.DB', 'Video 4'),
3907+ '25': translate('OpenLP.DB', 'Video 5'),
3908+ '26': translate('OpenLP.DB', 'Video 6'),
3909+ '27': translate('OpenLP.DB', 'Video 7'),
3910+ '28': translate('OpenLP.DB', 'Video 8'),
3911+ '29': translate('OpenLP.DB', 'Video 9'),
3912+ '31': translate('OpenLP.DB', 'Digital 1'),
3913+ '32': translate('OpenLP.DB', 'Digital 2'),
3914+ '33': translate('OpenLP.DB', 'Digital 3'),
3915+ '34': translate('OpenLP.DB', 'Digital 4'),
3916+ '35': translate('OpenLP.DB', 'Digital 5'),
3917+ '36': translate('OpenLP.DB', 'Digital 6'),
3918+ '37': translate('OpenLP.DB', 'Digital 7'),
3919+ '38': translate('OpenLP.DB', 'Digital 8'),
3920+ '39': translate('OpenLP.DB', 'Digital 9'),
3921+ '41': translate('OpenLP.DB', 'Storage 1'),
3922+ '42': translate('OpenLP.DB', 'Storage 2'),
3923+ '43': translate('OpenLP.DB', 'Storage 3'),
3924+ '44': translate('OpenLP.DB', 'Storage 4'),
3925+ '45': translate('OpenLP.DB', 'Storage 5'),
3926+ '46': translate('OpenLP.DB', 'Storage 6'),
3927+ '47': translate('OpenLP.DB', 'Storage 7'),
3928+ '48': translate('OpenLP.DB', 'Storage 8'),
3929+ '49': translate('OpenLP.DB', 'Storage 9'),
3930+ '51': translate('OpenLP.DB', 'Network 1'),
3931+ '52': translate('OpenLP.DB', 'Network 2'),
3932+ '53': translate('OpenLP.DB', 'Network 3'),
3933+ '54': translate('OpenLP.DB', 'Network 4'),
3934+ '55': translate('OpenLP.DB', 'Network 5'),
3935+ '56': translate('OpenLP.DB', 'Network 6'),
3936+ '57': translate('OpenLP.DB', 'Network 7'),
3937+ '58': translate('OpenLP.DB', 'Network 8'),
3938+ '59': translate('OpenLP.DB', 'Network 9')
3939+}
3940+>>>>>>> MERGE-SOURCE
3941
3942=== modified file 'openlp/core/lib/projector/db.py'
3943--- openlp/core/lib/projector/db.py 2016-12-31 11:05:48 +0000
3944+++ openlp/core/lib/projector/db.py 2017-01-08 22:05:36 +0000
3945@@ -40,13 +40,12 @@
3946
3947 from sqlalchemy import Column, ForeignKey, Integer, MetaData, String, and_
3948 from sqlalchemy.ext.declarative import declarative_base, declared_attr
3949-from sqlalchemy.orm import backref, relationship
3950+from sqlalchemy.orm import relationship
3951
3952 from openlp.core.lib.db import Manager, init_db, init_url
3953 from openlp.core.lib.projector.constants import PJLINK_DEFAULT_CODES
3954
3955-metadata = MetaData()
3956-Base = declarative_base(metadata)
3957+Base = declarative_base(MetaData())
3958
3959
3960 class CommonBase(object):
3961@@ -54,8 +53,8 @@
3962 Base class to automate table name and ID column.
3963 """
3964 @declared_attr
3965- def __tablename__(cls):
3966- return cls.__name__.lower()
3967+ def __tablename__(self):
3968+ return self.__name__.lower()
3969
3970 id = Column(Integer, primary_key=True)
3971
3972@@ -74,7 +73,7 @@
3973 """
3974 Returns a basic representation of a Manufacturer table entry.
3975 """
3976- return '<Manufacturer(name="%s")>' % self.name
3977+ return '<Manufacturer(name="{name}")>'.format(name=self.name)
3978
3979 name = Column(String(30))
3980 models = relationship('Model',
3981@@ -101,7 +100,7 @@
3982 """
3983 Returns a basic representation of a Model table entry.
3984 """
3985- return '<Model(name=%s)>' % self.name
3986+ return '<Model(name={name})>'.format(name=self.name)
3987
3988 manufacturer_id = Column(Integer, ForeignKey('manufacturer.id'))
3989 name = Column(String(20))
3990@@ -131,8 +130,9 @@
3991 """
3992 Return basic representation of Source table entry.
3993 """
3994- return '<Source(pjlink_name="%s", pjlink_code="%s", text="%s")>' % \
3995- (self.pjlink_name, self.pjlink_code, self.text)
3996+ return '<Source(pjlink_name="{name}", pjlink_code="{code}", text="{text}")>'.format(name=self.pjlink_name,
3997+ code=self.pjlink_code,
3998+ text=self.text)
3999 model_id = Column(Integer, ForeignKey('model.id'))
4000 pjlink_name = Column(String(15))
4001 pjlink_code = Column(String(2))
4002@@ -162,11 +162,22 @@
4003 """
4004 Return basic representation of Source table entry.
4005 """
4006- return '< Projector(id="%s", ip="%s", port="%s", pin="%s", name="%s", location="%s",' \
4007- 'notes="%s", pjlink_name="%s", manufacturer="%s", model="%s", other="%s",' \
4008- 'sources="%s", source_list="%s") >' % (self.id, self.ip, self.port, self.pin, self.name, self.location,
4009- self.notes, self.pjlink_name, self.manufacturer, self.model,
4010- self.other, self.sources, self.source_list)
4011+ return '< Projector(id="{data}", ip="{ip}", port="{port}", pin="{pin}", name="{name}", ' \
4012+ 'location="{location}", notes="{notes}", pjlink_name="{pjlink_name}", ' \
4013+ 'manufacturer="{manufacturer}", model="{model}", other="{other}", ' \
4014+ 'sources="{sources}", source_list="{source_list}") >'.format(data=self.id,
4015+ ip=self.ip,
4016+ port=self.port,
4017+ pin=self.pin,
4018+ name=self.name,
4019+ location=self.location,
4020+ notes=self.notes,
4021+ pjlink_name=self.pjlink_name,
4022+ manufacturer=self.manufacturer,
4023+ model=self.model,
4024+ other=self.other,
4025+ sources=self.sources,
4026+ source_list=self.source_list)
4027 ip = Column(String(100))
4028 port = Column(String(8))
4029 pin = Column(String(20))
4030@@ -203,10 +214,11 @@
4031 """
4032 Return basic representation of Source table entry.
4033 """
4034- return '<ProjectorSource(id="%s", code="%s", text="%s", projector_id="%s")>' % (self.id,
4035- self.code,
4036- self.text,
4037- self.projector_id)
4038+ return '<ProjectorSource(id="{data}", code="{code}", text="{text}", ' \
4039+ 'projector_id="{projector_id}")>'.format(data=self.id,
4040+ code=self.code,
4041+ text=self.text,
4042+ projector_id=self.projector_id)
4043 code = Column(String(3))
4044 text = Column(String(20))
4045 projector_id = Column(Integer, ForeignKey('projector.id'))
4046@@ -217,10 +229,10 @@
4047 Class to access the projector database.
4048 """
4049 def __init__(self, *args, **kwargs):
4050- log.debug('ProjectorDB().__init__(args="%s", kwargs="%s")' % (args, kwargs))
4051+ log.debug('ProjectorDB().__init__(args="{arg}", kwargs="{kwarg}")'.format(arg=args, kwarg=kwargs))
4052 super().__init__(plugin_name='projector', init_schema=self.init_schema)
4053- log.debug('ProjectorDB() Initialized using db url %s' % self.db_url)
4054- log.debug('Session: %s', self.session)
4055+ log.debug('ProjectorDB() Initialized using db url {db}'.format(db=self.db_url))
4056+ log.debug('Session: {session}'.format(session=self.session))
4057
4058 def init_schema(self, *args, **kwargs):
4059 """
4060@@ -240,13 +252,14 @@
4061 :param dbid: DB record id
4062 :returns: Projector() instance
4063 """
4064- log.debug('get_projector_by_id(id="%s")' % dbid)
4065+ log.debug('get_projector_by_id(id="{data}")'.format(data=dbid))
4066 projector = self.get_object_filtered(Projector, Projector.id == dbid)
4067 if projector is None:
4068 # Not found
4069- log.warn('get_projector_by_id() did not find %s' % id)
4070+ log.warning('get_projector_by_id() did not find {data}'.format(data=id))
4071 return None
4072- log.debug('get_projectorby_id() returning 1 entry for "%s" id="%s"' % (dbid, projector.id))
4073+ log.debug('get_projectorby_id() returning 1 entry for "{entry}" id="{data}"'.format(entry=dbid,
4074+ data=projector.id))
4075 return projector
4076
4077 def get_projector_all(self):
4078@@ -262,7 +275,7 @@
4079 return return_list
4080 for new_projector in new_list:
4081 return_list.append(new_projector)
4082- log.debug('get_all() returning %s item(s)' % len(return_list))
4083+ log.debug('get_all() returning {items} item(s)'.format(items=len(return_list)))
4084 return return_list
4085
4086 def get_projector_by_ip(self, ip):
4087@@ -276,9 +289,10 @@
4088 projector = self.get_object_filtered(Projector, Projector.ip == ip)
4089 if projector is None:
4090 # Not found
4091- log.warn('get_projector_by_ip() did not find %s' % ip)
4092+ log.warning('get_projector_by_ip() did not find {ip}'.format(ip=ip))
4093 return None
4094- log.debug('get_projectorby_ip() returning 1 entry for "%s" id="%s"' % (ip, projector.id))
4095+ log.debug('get_projectorby_ip() returning 1 entry for "{ip}" id="{data}"'.format(ip=ip,
4096+ data=projector.id))
4097 return projector
4098
4099 def get_projector_by_name(self, name):
4100@@ -288,13 +302,14 @@
4101 :param name: Name of projector
4102 :returns: Projector() instance
4103 """
4104- log.debug('get_projector_by_name(name="%s")' % name)
4105+ log.debug('get_projector_by_name(name="{name}")'.format(name=name))
4106 projector = self.get_object_filtered(Projector, Projector.name == name)
4107 if projector is None:
4108 # Not found
4109- log.warn('get_projector_by_name() did not find "%s"' % name)
4110+ log.warning('get_projector_by_name() did not find "{name}"'.format(name=name))
4111 return None
4112- log.debug('get_projector_by_name() returning one entry for "%s" id="%s"' % (name, projector.id))
4113+ log.debug('get_projector_by_name() returning one entry for "{name}" id="{data}"'.format(name=name,
4114+ data=projector.id))
4115 return projector
4116
4117 def add_projector(self, projector):
4118@@ -308,13 +323,13 @@
4119 """
4120 old_projector = self.get_object_filtered(Projector, Projector.ip == projector.ip)
4121 if old_projector is not None:
4122- log.warn('add_new() skipping entry ip="%s" (Already saved)' % old_projector.ip)
4123+ log.warning('add_new() skipping entry ip="{ip}" (Already saved)'.format(ip=old_projector.ip))
4124 return False
4125 log.debug('add_new() saving new entry')
4126- log.debug('ip="%s", name="%s", location="%s"' % (projector.ip,
4127- projector.name,
4128- projector.location))
4129- log.debug('notes="%s"' % projector.notes)
4130+ log.debug('ip="{ip}", name="{name}", location="{location}"'.format(ip=projector.ip,
4131+ name=projector.name,
4132+ location=projector.location))
4133+ log.debug('notes="{notes}"'.format(notes=projector.notes))
4134 return self.save_object(projector)
4135
4136 def update_projector(self, projector=None):
4137@@ -333,7 +348,7 @@
4138 if old_projector is None:
4139 log.error('Edit called on projector instance not in database - cancelled')
4140 return False
4141- log.debug('(%s) Updating projector with dbid=%s' % (projector.ip, projector.id))
4142+ log.debug('({ip}) Updating projector with dbid={dbid}'.format(ip=projector.ip, dbid=projector.id))
4143 old_projector.ip = projector.ip
4144 old_projector.name = projector.name
4145 old_projector.location = projector.location
4146@@ -357,9 +372,9 @@
4147 """
4148 deleted = self.delete_object(Projector, projector.id)
4149 if deleted:
4150- log.debug('delete_by_id() Removed entry id="%s"' % projector.id)
4151+ log.debug('delete_by_id() Removed entry id="{data}"'.format(data=projector.id))
4152 else:
4153- log.error('delete_by_id() Entry id="%s" not deleted for some reason' % projector.id)
4154+ log.error('delete_by_id() Entry id="{data}" not deleted for some reason'.format(data=projector.id))
4155 return deleted
4156
4157 def get_source_list(self, projector):
4158@@ -392,12 +407,12 @@
4159 :param source: ProjectorSource id
4160 :returns: ProjetorSource instance or None
4161 """
4162- source_entry = self.get_object_filtered(ProjetorSource, ProjectorSource.id == source)
4163+ source_entry = self.get_object_filtered(ProjectorSource, ProjectorSource.id == source)
4164 if source_entry is None:
4165 # Not found
4166- log.warn('get_source_by_id() did not find "%s"' % source)
4167+ log.warning('get_source_by_id() did not find "{source}"'.format(source=source))
4168 return None
4169- log.debug('get_source_by_id() returning one entry for "%s""' % (source))
4170+ log.debug('get_source_by_id() returning one entry for "{source}""'.format(source=source))
4171 return source_entry
4172
4173 def get_source_by_code(self, code, projector_id):
4174@@ -411,11 +426,14 @@
4175 source_entry = self.get_object_filtered(ProjectorSource,
4176 and_(ProjectorSource.code == code,
4177 ProjectorSource.projector_id == projector_id))
4178+
4179 if source_entry is None:
4180 # Not found
4181- log.warn('get_source_by_id() did not find code="%s" projector_id="%s"' % (code, projector_id))
4182+ log.warning('get_source_by_id() not found')
4183+ log.warning('code="{code}" projector_id="{data}"'.format(code=code, data=projector_id))
4184 return None
4185- log.debug('get_source_by_id() returning one entry for code="%s" projector_id="%s"' % (code, projector_id))
4186+ log.debug('get_source_by_id() returning one entry')
4187+ log.debug('code="{code}" projector_id="{data}"'.format(code=code, data=projector_id))
4188 return source_entry
4189
4190 def add_source(self, source):
4191@@ -424,6 +442,6 @@
4192
4193 :param source: ProjectorSource() instance to add
4194 """
4195- log.debug('Saving ProjectorSource(projector_id="%s" code="%s" text="%s")' % (source.projector_id,
4196- source.code, source.text))
4197+ log.debug('Saving ProjectorSource(projector_id="{data}" '
4198+ 'code="{code}" text="{text}")'.format(data=source.projector_id, code=source.code, text=source.text))
4199 return self.save_object(source)
4200
4201=== modified file 'openlp/core/lib/projector/pjlink1.py'
4202--- openlp/core/lib/projector/pjlink1.py 2016-12-31 11:05:48 +0000
4203+++ openlp/core/lib/projector/pjlink1.py 2017-01-08 22:05:36 +0000
4204@@ -46,15 +46,24 @@
4205
4206 from codecs import decode
4207
4208-from PyQt5.QtCore import pyqtSignal, pyqtSlot
4209-from PyQt5.QtNetwork import QAbstractSocket, QTcpSocket
4210+from PyQt5 import QtCore, QtNetwork
4211
4212+<<<<<<< TREE
4213 from openlp.core.common import translate, md5_hash
4214 from openlp.core.lib.projector.constants import *
4215+=======
4216+from openlp.core.common import translate, qmd5_hash
4217+from openlp.core.lib.projector.constants import CONNECTION_ERRORS, CR, ERROR_MSG, ERROR_STRING, \
4218+ E_AUTHENTICATION, E_CONNECTION_REFUSED, E_GENERAL, E_INVALID_DATA, E_NETWORK, E_NOT_CONNECTED, \
4219+ E_PARAMETER, E_PROJECTOR, E_SOCKET_TIMEOUT, E_UNAVAILABLE, E_UNDEFINED, PJLINK_ERRORS, \
4220+ PJLINK_ERST_STATUS, PJLINK_MAX_PACKET, PJLINK_PORT, PJLINK_POWR_STATUS, PJLINK_VALID_CMD, \
4221+ STATUS_STRING, S_CONNECTED, S_CONNECTING, S_NETWORK_RECEIVED, S_NETWORK_SENDING, S_NOT_CONNECTED, \
4222+ S_OFF, S_OK, S_ON, S_STATUS
4223+>>>>>>> MERGE-SOURCE
4224
4225 # Shortcuts
4226-SocketError = QAbstractSocket.SocketError
4227-SocketSTate = QAbstractSocket.SocketState
4228+SocketError = QtNetwork.QAbstractSocket.SocketError
4229+SocketSTate = QtNetwork.QAbstractSocket.SocketState
4230
4231 PJLINK_PREFIX = '%'
4232 PJLINK_CLASS = '1'
4233@@ -62,11 +71,12 @@
4234 PJLINK_SUFFIX = CR
4235
4236
4237-class PJLink1(QTcpSocket):
4238+class PJLink1(QtNetwork.QTcpSocket):
4239 """
4240 Socket service for connecting to a PJLink-capable projector.
4241 """
4242 # Signals sent by this module
4243+<<<<<<< TREE
4244 changeStatus = pyqtSignal(str, int, str)
4245 projectorNetwork = pyqtSignal(str, int) # Projector network activity
4246 projectorStatus = pyqtSignal(int) # Status update
4247@@ -74,6 +84,15 @@
4248 projectorNoAuthentication = pyqtSignal(str) # PIN set and no authentication needed
4249 projectorReceivedData = pyqtSignal() # Notify when received data finished processing
4250 projectorUpdateIcons = pyqtSignal() # Update the status icons on toolbar
4251+=======
4252+ changeStatus = QtCore.pyqtSignal(str, int, str)
4253+ projectorNetwork = QtCore.pyqtSignal(int) # Projector network activity
4254+ projectorStatus = QtCore.pyqtSignal(int) # Status update
4255+ projectorAuthentication = QtCore.pyqtSignal(str) # Authentication error
4256+ projectorNoAuthentication = QtCore.pyqtSignal(str) # PIN set and no authentication needed
4257+ projectorReceivedData = QtCore.pyqtSignal() # Notify when received data finished processing
4258+ projectorUpdateIcons = QtCore.pyqtSignal() # Update the status icons on toolbar
4259+>>>>>>> MERGE-SOURCE
4260
4261 def __init__(self, name=None, ip=None, port=PJLINK_PORT, pin=None, *args, **kwargs):
4262 """
4263@@ -116,8 +135,8 @@
4264 self.error_status = S_OK
4265 # Socket information
4266 # Add enough space to input buffer for extraneous \n \r
4267- self.maxSize = PJLINK_MAX_PACKET + 2
4268- self.setReadBufferSize(self.maxSize)
4269+ self.max_size = PJLINK_MAX_PACKET + 2
4270+ self.setReadBufferSize(self.max_size)
4271 # PJLink information
4272 self.pjlink_class = '1' # Default class
4273 self.reset_information()
4274@@ -129,19 +148,20 @@
4275 # Socket timer for some possible brain-dead projectors or network cable pulled
4276 self.socket_timer = None
4277 # Map command to function
4278- self.PJLINK1_FUNC = {'AVMT': self.process_avmt,
4279- 'CLSS': self.process_clss,
4280- 'ERST': self.process_erst,
4281- 'INFO': self.process_info,
4282- 'INF1': self.process_inf1,
4283- 'INF2': self.process_inf2,
4284- 'INPT': self.process_inpt,
4285- 'INST': self.process_inst,
4286- 'LAMP': self.process_lamp,
4287- 'NAME': self.process_name,
4288- 'PJLINK': self.check_login,
4289- 'POWR': self.process_powr
4290- }
4291+ self.pjlink1_functions = {
4292+ 'AVMT': self.process_avmt,
4293+ 'CLSS': self.process_clss,
4294+ 'ERST': self.process_erst,
4295+ 'INFO': self.process_info,
4296+ 'INF1': self.process_inf1,
4297+ 'INF2': self.process_inf2,
4298+ 'INPT': self.process_inpt,
4299+ 'INST': self.process_inst,
4300+ 'LAMP': self.process_lamp,
4301+ 'NAME': self.process_name,
4302+ 'PJLINK': self.check_login,
4303+ 'POWR': self.process_powr
4304+ }
4305
4306 def reset_information(self):
4307 """
4308@@ -291,7 +311,7 @@
4309 message=status_message if msg is None else msg))
4310 self.changeStatus.emit(self.ip, status, message)
4311
4312- @pyqtSlot()
4313+ @QtCore.pyqtSlot()
4314 def check_login(self, data=None):
4315 """
4316 Processes the initial connection and authentication (if needed).
4317@@ -309,8 +329,8 @@
4318 log.error('({ip}) Socket timeout waiting for login'.format(ip=self.ip))
4319 self.change_status(E_SOCKET_TIMEOUT)
4320 return
4321- read = self.readLine(self.maxSize)
4322- dontcare = self.readLine(self.maxSize) # Clean out the trailing \r\n
4323+ read = self.readLine(self.max_size)
4324+ dontcare = self.readLine(self.max_size) # Clean out the trailing \r\n
4325 if read is None:
4326 log.warning('({ip}) read is None - socket error?'.format(ip=self.ip))
4327 return
4328@@ -320,8 +340,13 @@
4329 data = decode(read, 'ascii')
4330 # Possibility of extraneous data on input when reading.
4331 # Clean out extraneous characters in buffer.
4332+<<<<<<< TREE
4333 dontcare = self.readLine(self.maxSize)
4334 log.debug('({ip}) check_login() read "{data}"'.format(ip=self.ip, data=data.strip()))
4335+=======
4336+ dontcare = self.readLine(self.max_size)
4337+ log.debug('({ip}) check_login() read "{data}"'.format(ip=self.ip, data=data.strip()))
4338+>>>>>>> MERGE-SOURCE
4339 # At this point, we should only have the initial login prompt with
4340 # possible authentication
4341 # PJLink initial login will be:
4342@@ -354,6 +379,7 @@
4343 return
4344 elif data_check[1] == '1':
4345 # Authenticated login with salt
4346+<<<<<<< TREE
4347 if self.pin is None:
4348 log.warning('({ip}) Authenticated connection but no pin set'.format(ip=self.name))
4349 self.disconnect_from_host()
4350@@ -365,20 +391,34 @@
4351 log.debug('({ip}) Setting hash with salt="{data}"'.format(ip=self.ip, data=data_check[2]))
4352 log.debug('({ip}) pin="{data}"'.format(ip=self.ip, data=self.pin))
4353 salt = md5_hash(salt=data_check[2].encode('ascii'), data=self.pin.encode('ascii'))
4354+=======
4355+ if self.pin is None:
4356+ log.warning('({ip}) Authenticated connection but no pin set'.format(ip=self.name))
4357+ self.disconnect_from_host()
4358+ self.change_status(E_AUTHENTICATION)
4359+ log.debug('({ip}) Emitting projectorAuthentication() signal'.format(ip=self.name))
4360+ self.projectorAuthentication.emit(self.name)
4361+ return
4362+ else:
4363+ log.debug('({ip}) Setting hash with salt="{data}"'.format(ip=self.ip, data=data_check[2]))
4364+ log.debug('({ip}) pin="{data}"'.format(ip=self.ip, data=self.pin))
4365+ data_hash = str(qmd5_hash(salt=data_check[2].encode('utf-8'), data=self.pin.encode('utf-8')),
4366+ encoding='ascii')
4367+>>>>>>> MERGE-SOURCE
4368 else:
4369- salt = None
4370- # We're connected at this point, so go ahead and do regular I/O
4371+ data_hash = None
4372+ # We're connected at this point, so go ahead and setup regular I/O
4373 self.readyRead.connect(self.get_data)
4374 self.projectorReceivedData.connect(self._send_command)
4375 # Initial data we should know about
4376- self.send_command(cmd='CLSS', salt=salt)
4377+ self.send_command(cmd='CLSS', salt=data_hash)
4378 self.waitForReadyRead()
4379 if (not self.no_poll) and (self.state() == self.ConnectedState):
4380 log.debug('({ip}) Starting timer'.format(ip=self.ip))
4381 self.timer.setInterval(2000) # Set 2 seconds for initial information
4382 self.timer.start()
4383
4384- @pyqtSlot()
4385+ @QtCore.pyqtSlot()
4386 def get_data(self):
4387 """
4388 Socket interface to retrieve data.
4389@@ -388,7 +428,7 @@
4390 log.debug('({ip}) get_data(): Not connected - returning'.format(ip=self.ip))
4391 self.send_busy = False
4392 return
4393- read = self.readLine(self.maxSize)
4394+ read = self.readLine(self.max_size)
4395 if read == -1:
4396 # No data available
4397 log.debug('({ip}) get_data(): No data available (-1)'.format(ip=self.ip))
4398@@ -435,7 +475,11 @@
4399 return
4400 return self.process_command(cmd, data)
4401
4402+<<<<<<< TREE
4403 @pyqtSlot(QAbstractSocket.SocketError)
4404+=======
4405+ @QtCore.pyqtSlot(QtNetwork.QAbstractSocket.SocketError)
4406+>>>>>>> MERGE-SOURCE
4407 def get_error(self, err):
4408 """
4409 Process error from SocketError signal.
4410@@ -475,6 +519,7 @@
4411 log.warning('({ip}) send_command(): Not connected - returning'.format(ip=self.ip))
4412 self.send_queue = []
4413 return
4414+<<<<<<< TREE
4415 self.projectorNetwork.emit(self.ip, S_NETWORK_SENDING)
4416 log.debug('(%s) send_command(): Building cmd="%s" opts="%s" %s' % (self.ip,
4417 cmd,
4418@@ -484,6 +529,19 @@
4419 out = '%s%s %s%s' % (PJLINK_HEADER, cmd, opts, CR)
4420 else:
4421 out = '%s%s%s %s%s' % (salt, PJLINK_HEADER, cmd, opts, CR)
4422+=======
4423+ self.projectorNetwork.emit(S_NETWORK_SENDING)
4424+ log.debug('({ip}) send_command(): Building cmd="{command}" opts="{data}"{salt}'.format(ip=self.ip,
4425+ command=cmd,
4426+ data=opts,
4427+ salt='' if salt is None
4428+ else ' with hash'))
4429+ out = '{salt}{header}{command} {options}{suffix}'.format(salt="" if salt is None else salt,
4430+ header=PJLINK_HEADER,
4431+ command=cmd,
4432+ options=opts,
4433+ suffix=CR)
4434+>>>>>>> MERGE-SOURCE
4435 if out in self.send_queue:
4436 # Already there, so don't add
4437 log.debug('({ip}) send_command(out="{data}") Already in queue - skipping'.format(ip=self.ip,
4438@@ -501,7 +559,7 @@
4439 log.debug('({ip}) send_command() calling _send_string()'.format(ip=self.ip))
4440 self._send_command()
4441
4442- @pyqtSlot()
4443+ @QtCore.pyqtSlot()
4444 def _send_command(self, data=None):
4445 """
4446 Socket interface to send data. If data=None, then check queue.
4447@@ -533,6 +591,7 @@
4448 log.debug('({ip}) _send_string(): Sending "{data}"'.format(ip=self.ip, data=out.strip()))
4449 log.debug('({ip}) _send_string(): Queue = {data}'.format(ip=self.ip, data=self.send_queue))
4450 self.socket_timer.start()
4451+<<<<<<< TREE
4452 try:
4453 self.projectorNetwork.emit(self.ip, S_NETWORK_SENDING)
4454 sent = self.write(out.encode('ascii'))
4455@@ -546,6 +605,15 @@
4456 self.changeStatus(E_NETWORK,
4457 translate('OpenLP.PJLink1', '{code} : {string}').format(code=e.error(),
4458 string=e.errorString()))
4459+=======
4460+ self.projectorNetwork.emit(S_NETWORK_SENDING)
4461+ sent = self.write(out.encode('ascii'))
4462+ self.waitForBytesWritten(2000) # 2 seconds should be enough
4463+ if sent == -1:
4464+ # Network error?
4465+ self.change_status(E_NETWORK,
4466+ translate('OpenLP.PJLink1', 'Error while sending data to projector'))
4467+>>>>>>> MERGE-SOURCE
4468
4469 def process_command(self, cmd, data):
4470 """
4471@@ -589,8 +657,8 @@
4472 self.projectorReceivedData.emit()
4473 return
4474
4475- if cmd in self.PJLINK1_FUNC:
4476- self.PJLINK1_FUNC[cmd](data)
4477+ if cmd in self.pjlink1_functions:
4478+ self.pjlink1_functions[cmd](data)
4479 else:
4480 log.warning('({ip}) Invalid command {data}'.format(ip=self.ip, data=cmd))
4481 self.send_busy = False
4482@@ -815,9 +883,9 @@
4483 log.warning('({ip}) connect_to_host(): Already connected - returning'.format(ip=self.ip))
4484 return
4485 self.change_status(S_CONNECTING)
4486- self.connectToHost(self.ip, self.port if type(self.port) is int else int(self.port))
4487+ self.connectToHost(self.ip, self.port if isinstance(self.port, int) else int(self.port))
4488
4489- @pyqtSlot()
4490+ @QtCore.pyqtSlot()
4491 def disconnect_from_host(self, abort=False):
4492 """
4493 Close socket and cleanup.
4494
4495=== modified file 'openlp/core/lib/renderer.py'
4496--- openlp/core/lib/renderer.py 2016-12-31 11:05:48 +0000
4497+++ openlp/core/lib/renderer.py 2017-01-08 22:05:36 +0000
4498@@ -22,6 +22,7 @@
4499
4500 import re
4501
4502+from string import Template
4503 from PyQt5 import QtGui, QtCore, QtWebKitWidgets
4504
4505 from openlp.core.common import Registry, RegistryProperties, OpenLPMixin, RegistryMixin, Settings
4506@@ -107,7 +108,7 @@
4507
4508 :param theme_name: The theme name
4509 """
4510- self.log_debug("_set_theme with theme %s" % theme_name)
4511+ self.log_debug("_set_theme with theme {theme}".format(theme=theme_name))
4512 if theme_name not in self._theme_dimensions:
4513 theme_data = self.theme_manager.get_theme_data(theme_name)
4514 main_rect = self.get_main_rectangle(theme_data)
4515@@ -183,7 +184,7 @@
4516
4517 :param item_theme_name: The item theme's name.
4518 """
4519- self.log_debug("set_item_theme with theme %s" % item_theme_name)
4520+ self.log_debug("set_item_theme with theme {theme}".format(theme=item_theme_name))
4521 self._set_theme(item_theme_name)
4522 self.item_theme_name = item_theme_name
4523
4524@@ -317,7 +318,7 @@
4525 self.width = screen_size.width()
4526 self.height = screen_size.height()
4527 self.screen_ratio = self.height / self.width
4528- self.log_debug('_calculate default %s, %f' % (screen_size, self.screen_ratio))
4529+ self.log_debug('_calculate default {size}, {ratio:f}'.format(size=screen_size, ratio=self.screen_ratio))
4530 # 90% is start of footer
4531 self.footer_start = int(self.height * 0.90)
4532
4533@@ -354,7 +355,7 @@
4534 :param rect_main: The main text block.
4535 :param rect_footer: The footer text block.
4536 """
4537- self.log_debug('_set_text_rectangle %s , %s' % (rect_main, rect_footer))
4538+ self.log_debug('_set_text_rectangle {main} , {footer}'.format(main=rect_main, footer=rect_footer))
4539 self._rect = rect_main
4540 self._rect_footer = rect_footer
4541 self.page_width = self._rect.width()
4542@@ -370,7 +371,7 @@
4543 self.web.resize(self.page_width, self.page_height)
4544 self.web_frame = self.web.page().mainFrame()
4545 # Adjust width and height to account for shadow. outline done in css.
4546- html = """<!DOCTYPE html><html><head><script>
4547+ html = Template("""<!DOCTYPE html><html><head><script>
4548 function show_text(newtext) {
4549 var main = document.getElementById('main');
4550 main.innerHTML = newtext;
4551@@ -379,12 +380,16 @@
4552 // returned value).
4553 return main.offsetHeight;
4554 }
4555- </script><style>*{margin: 0; padding: 0; border: 0;}
4556- #main {position: absolute; top: 0px; %s %s}</style></head><body>
4557- <div id="main"></div></body></html>""" % \
4558- (build_lyrics_format_css(theme_data, self.page_width, self.page_height),
4559- build_lyrics_outline_css(theme_data))
4560- self.web.setHtml(html)
4561+ </script>
4562+ <style>
4563+ *{margin: 0; padding: 0; border: 0;}
4564+ #main {position: absolute; top: 0px; ${format_css} ${outline_css}}
4565+ </style></head>
4566+ <body><div id="main"></div></body></html>""")
4567+ self.web.setHtml(html.substitute(format_css=build_lyrics_format_css(theme_data,
4568+ self.page_width,
4569+ self.page_height),
4570+ outline_css=build_lyrics_outline_css(theme_data)))
4571 self.empty_height = self.web_frame.contentsSize().height()
4572
4573 def _paginate_slide(self, lines, line_end):
4574@@ -518,7 +523,8 @@
4575
4576 :param text: The text to check. It may contain HTML tags.
4577 """
4578- self.web_frame.evaluateJavaScript('show_text("%s")' % text.replace('\\', '\\\\').replace('\"', '\\\"'))
4579+ self.web_frame.evaluateJavaScript('show_text'
4580+ '("{text}")'.format(text=text.replace('\\', '\\\\').replace('\"', '\\\"')))
4581 return self.web_frame.contentsSize().height() <= self.empty_height
4582
4583
4584@@ -529,7 +535,7 @@
4585 :param line: Line to be split
4586 """
4587 # this parse we are to be wordy
4588- return re.split('\s+', line)
4589+ return re.split(r'\s+', line)
4590
4591
4592 def get_start_tags(raw_text):
4593
4594=== modified file 'openlp/core/lib/screen.py'
4595--- openlp/core/lib/screen.py 2016-12-31 11:05:48 +0000
4596+++ openlp/core/lib/screen.py 2017-01-08 22:05:36 +0000
4597@@ -78,7 +78,7 @@
4598 ``number``
4599 The number of the screen, which size has changed.
4600 """
4601- log.info('screen_resolution_changed %d' % number)
4602+ log.info('screen_resolution_changed {number:d}'.format(number=number))
4603 for screen in self.screen_list:
4604 if number == screen['number']:
4605 new_screen = {
4606@@ -105,7 +105,7 @@
4607 """
4608 # Do not log at start up.
4609 if changed_screen != -1:
4610- log.info('screen_count_changed %d' % self.desktop.screenCount())
4611+ log.info('screen_count_changed {count:d}'.format(count=self.desktop.screenCount()))
4612 # Remove unplugged screens.
4613 for screen in copy.deepcopy(self.screen_list):
4614 if screen['number'] == self.desktop.screenCount():
4615@@ -132,9 +132,11 @@
4616 """
4617 screen_list = []
4618 for screen in self.screen_list:
4619- screen_name = '%s %d' % (translate('OpenLP.ScreenList', 'Screen'), screen['number'] + 1)
4620+ screen_name = '{name} {number:d}'.format(name=translate('OpenLP.ScreenList', 'Screen'),
4621+ number=screen['number'] + 1)
4622 if screen['primary']:
4623- screen_name = '%s (%s)' % (screen_name, translate('OpenLP.ScreenList', 'primary'))
4624+ screen_name = '{name} ({primary})'.format(name=screen_name,
4625+ primary=translate('OpenLP.ScreenList', 'primary'))
4626 screen_list.append(screen_name)
4627 return screen_list
4628
4629@@ -152,7 +154,7 @@
4630 'size': PyQt5.QtCore.QRect(0, 0, 1024, 768)
4631 }
4632 """
4633- log.info('Screen %d found with resolution %s' % (screen['number'], screen['size']))
4634+ log.info('Screen {number:d} found with resolution {size}'.format(number=screen['number'], size=screen['size']))
4635 if screen['primary']:
4636 self.current = screen
4637 self.override = copy.deepcopy(self.current)
4638@@ -165,7 +167,7 @@
4639
4640 :param number: The screen number (int).
4641 """
4642- log.info('remove_screen %d' % number)
4643+ log.info('remove_screen {number:d}'.format(number=number))
4644 for screen in self.screen_list:
4645 if screen['number'] == number:
4646 self.screen_list.remove(screen)
4647@@ -189,7 +191,7 @@
4648
4649 :param number: The screen number (int).
4650 """
4651- log.debug('set_current_display %s' % number)
4652+ log.debug('set_current_display {number}'.format(number=number))
4653 if number + 1 > self.display_count:
4654 self.current = self.screen_list[0]
4655 else:
4656
4657=== modified file 'openlp/core/lib/searchedit.py'
4658--- openlp/core/lib/searchedit.py 2016-12-31 11:05:48 +0000
4659+++ openlp/core/lib/searchedit.py 2017-01-08 22:05:36 +0000
4660@@ -26,6 +26,7 @@
4661
4662 from openlp.core.lib import build_icon
4663 from openlp.core.lib.ui import create_widget_action
4664+from openlp.core.common import Settings
4665
4666 log = logging.getLogger(__name__)
4667
4668@@ -37,11 +38,12 @@
4669 searchTypeChanged = QtCore.pyqtSignal(QtCore.QVariant)
4670 cleared = QtCore.pyqtSignal()
4671
4672- def __init__(self, parent):
4673+ def __init__(self, parent, settings_section):
4674 """
4675 Constructor.
4676 """
4677- super(SearchEdit, self).__init__(parent)
4678+ super().__init__(parent)
4679+ self.settings_section = settings_section
4680 self._current_search_type = -1
4681 self.clear_button = QtWidgets.QToolButton(self)
4682 self.clear_button.setIcon(build_icon(':/system/clear_shortcut.png'))
4683@@ -62,9 +64,10 @@
4684 right_padding = self.clear_button.width() + frame_width
4685 if hasattr(self, 'menu_button'):
4686 left_padding = self.menu_button.width()
4687- stylesheet = 'QLineEdit { padding-left: %spx; padding-right: %spx; } ' % (left_padding, right_padding)
4688+ stylesheet = 'QLineEdit {{ padding-left:{left}px; padding-right: {right}px; }} '.format(left=left_padding,
4689+ right=right_padding)
4690 else:
4691- stylesheet = 'QLineEdit { padding-right: %spx; } ' % right_padding
4692+ stylesheet = 'QLineEdit {{ padding-right: {right}px; }} '.format(right=right_padding)
4693 self.setStyleSheet(stylesheet)
4694 msz = self.minimumSizeHint()
4695 self.setMinimumSize(max(msz.width(), self.clear_button.width() + (frame_width * 2) + 2),
4696@@ -99,14 +102,10 @@
4697 menu = self.menu_button.menu()
4698 for action in menu.actions():
4699 if identifier == action.data():
4700- # setPlaceholderText has been implemented in Qt 4.7 and in at least PyQt 4.9 (I am not sure, if it was
4701- # implemented in PyQt 4.8).
4702- try:
4703- self.setPlaceholderText(action.placeholder_text)
4704- except AttributeError:
4705- pass
4706+ self.setPlaceholderText(action.placeholder_text)
4707 self.menu_button.setDefaultAction(action)
4708 self._current_search_type = identifier
4709+ Settings().setValue('{section}/last search type'.format(section=self.settings_section), identifier)
4710 self.searchTypeChanged.emit(identifier)
4711 return True
4712
4713@@ -129,14 +128,10 @@
4714 (2, ":/songs/authors.png", "Authors", "Search Authors...")
4715 """
4716 menu = QtWidgets.QMenu(self)
4717- first = None
4718 for identifier, icon, title, placeholder in items:
4719 action = create_widget_action(
4720 menu, text=title, icon=icon, data=identifier, triggers=self._on_menu_action_triggered)
4721 action.placeholder_text = placeholder
4722- if first is None:
4723- first = action
4724- self._current_search_type = identifier
4725 if not hasattr(self, 'menu_button'):
4726 self.menu_button = QtWidgets.QToolButton(self)
4727 self.menu_button.setIcon(build_icon(':/system/clear_shortcut.png'))
4728@@ -145,7 +140,8 @@
4729 self.menu_button.setStyleSheet('QToolButton { border: none; padding: 0px 10px 0px 0px; }')
4730 self.menu_button.resize(QtCore.QSize(28, 18))
4731 self.menu_button.setMenu(menu)
4732- self.menu_button.setDefaultAction(first)
4733+ self.set_current_search_type(
4734+ Settings().value('{section}/last search type'.format(section=self.settings_section)))
4735 self.menu_button.show()
4736 self._update_style_sheet()
4737
4738
4739=== modified file 'openlp/core/lib/serviceitem.py'
4740--- openlp/core/lib/serviceitem.py 2016-12-31 11:05:48 +0000
4741+++ openlp/core/lib/serviceitem.py 2017-01-08 22:05:36 +0000
4742@@ -34,7 +34,7 @@
4743 from PyQt5 import QtGui
4744
4745 from openlp.core.common import RegistryProperties, Settings, translate, AppLocation, md5_hash
4746-from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags, create_thumb
4747+from openlp.core.lib import ImageSource, build_icon, clean_tags, expand_tags
4748
4749 log = logging.getLogger(__name__)
4750
4751@@ -247,7 +247,7 @@
4752 self.renderer.set_item_theme(self.theme)
4753 self.theme_data, self.main, self.footer = self.renderer.pre_render()
4754 if self.service_item_type == ServiceItemType.Text:
4755- log.debug('Formatting slides: %s' % self.title)
4756+ log.debug('Formatting slides: {title}'.format(title=self.title))
4757 # Save rendered pages to this dict. In the case that a slide is used twice we can use the pages saved to
4758 # the dict instead of rendering them again.
4759 previous_pages = {}
4760@@ -270,7 +270,7 @@
4761 elif self.service_item_type == ServiceItemType.Image or self.service_item_type == ServiceItemType.Command:
4762 pass
4763 else:
4764- log.error('Invalid value renderer: %s' % self.service_item_type)
4765+ log.error('Invalid value renderer: {item}'.format(item=self.service_item_type))
4766 self.title = clean_tags(self.title)
4767 # The footer should never be None, but to be compatible with a few
4768 # nightly builds between 1.9.4 and 1.9.5, we have to correct this to
4769@@ -325,7 +325,8 @@
4770 self.service_item_type = ServiceItemType.Command
4771 # If the item should have a display title but this frame doesn't have one, we make one up
4772 if self.is_capable(ItemCapabilities.HasDisplayTitle) and not display_title:
4773- display_title = translate('OpenLP.ServiceItem', '[slide %d]') % (len(self._raw_frames) + 1)
4774+ display_title = translate('OpenLP.ServiceItem',
4775+ '[slide {frame:d}]').format(frame=len(self._raw_frames) + 1)
4776 # Update image path to match servicemanager location if file was loaded from service
4777 if image and not self.has_original_files and self.name == 'presentations':
4778 file_location = os.path.join(path, file_name)
4779@@ -334,6 +335,8 @@
4780 file_location_hash, ntpath.basename(image))
4781 self._raw_frames.append({'title': file_name, 'image': image, 'path': path,
4782 'display_title': display_title, 'notes': notes})
4783+ if self.is_capable(ItemCapabilities.HasThumbnails):
4784+ self.image_manager.add_image(image, ImageSource.CommandPlugins, '#000000')
4785 self._new_item()
4786
4787 def get_service_repr(self, lite_save):
4788@@ -390,7 +393,7 @@
4789 :param path: Defaults to *None*. This is the service manager path for things which have their files saved
4790 with them or None when the saved service is lite and the original file paths need to be preserved.
4791 """
4792- log.debug('set_from_service called with path %s' % path)
4793+ log.debug('set_from_service called with path {path}'.format(path=path))
4794 header = service_item['serviceitem']['header']
4795 self.title = header['title']
4796 self.name = header['name']
4797@@ -606,11 +609,13 @@
4798 start = None
4799 end = None
4800 if self.start_time != 0:
4801- start = translate('OpenLP.ServiceItem', '<strong>Start</strong>: %s') % \
4802- str(datetime.timedelta(seconds=self.start_time))
4803+ time = str(datetime.timedelta(seconds=self.start_time))
4804+ start = translate('OpenLP.ServiceItem',
4805+ '<strong>Start</strong>: {start}').format(start=time)
4806 if self.media_length != 0:
4807- end = translate('OpenLP.ServiceItem', '<strong>Length</strong>: %s') % \
4808- str(datetime.timedelta(seconds=self.media_length))
4809+ length = str(datetime.timedelta(seconds=self.media_length // 1000))
4810+ end = translate('OpenLP.ServiceItem', '<strong>Length</strong>: {length}').format(length=length)
4811+
4812 if not start and not end:
4813 return ''
4814 elif start and not end:
4815@@ -618,7 +623,7 @@
4816 elif not start and end:
4817 return end
4818 else:
4819- return '%s <br>%s' % (start, end)
4820+ return '{start} <br>{end}'.format(start=start, end=end)
4821
4822 def update_theme(self, theme):
4823 """
4824
4825=== modified file 'openlp/core/lib/settingstab.py'
4826--- openlp/core/lib/settingstab.py 2016-12-31 11:05:48 +0000
4827+++ openlp/core/lib/settingstab.py 2017-01-08 22:05:36 +0000
4828@@ -135,4 +135,4 @@
4829 """
4830 Tab has just been made visible to the user
4831 """
4832- self.tab_visited = True
4833+ pass
4834
4835=== renamed file 'openlp/core/lib/spelltextedit.py' => 'openlp/core/lib/spelltextedit.py.THIS'
4836=== modified file 'openlp/core/lib/theme.py'
4837--- openlp/core/lib/theme.py 2016-12-31 11:05:48 +0000
4838+++ openlp/core/lib/theme.py 2017-01-08 22:05:36 +0000
4839@@ -23,7 +23,6 @@
4840 Provide the theme XML and handling functions for OpenLP v2 themes.
4841 """
4842 import os
4843-import re
4844 import logging
4845 import json
4846
4847@@ -44,6 +43,7 @@
4848 Gradient = 1
4849 Image = 2
4850 Transparent = 3
4851+ Video = 4
4852
4853 @staticmethod
4854 def to_string(background_type):
4855@@ -58,6 +58,8 @@
4856 return 'image'
4857 elif background_type == BackgroundType.Transparent:
4858 return 'transparent'
4859+ elif background_type == BackgroundType.Video:
4860+ return 'video'
4861
4862 @staticmethod
4863 def from_string(type_string):
4864@@ -72,6 +74,8 @@
4865 return BackgroundType.Image
4866 elif type_string == 'transparent':
4867 return BackgroundType.Transparent
4868+ elif type_string == 'video':
4869+ return BackgroundType.Video
4870
4871
4872 class BackgroundGradientType(object):
4873@@ -160,6 +164,7 @@
4874 jsn = get_text_file_string(json_file)
4875 jsn = json.loads(jsn)
4876 self.expand_json(jsn)
4877+ self.background_filename = None
4878
4879 def expand_json(self, var, prev=None):
4880 """
4881@@ -184,7 +189,7 @@
4882
4883 :param path: The path name to be added.
4884 """
4885- if self.background_type == 'image':
4886+ if self.background_type == 'image' or self.background_type == 'video':
4887 if self.background_filename and path:
4888 self.theme_name = self.theme_name.strip()
4889 self.background_filename = self.background_filename.strip()
4890@@ -255,6 +260,21 @@
4891 # Create endColor element
4892 self.child_element(background, 'borderColor', str(border_color))
4893
4894+ def add_background_video(self, filename, border_color):
4895+ """
4896+ Add a video background.
4897+
4898+ :param filename: The file name of the video.
4899+ :param border_color:
4900+ """
4901+ background = self.theme_xml.createElement('background')
4902+ background.setAttribute('type', 'video')
4903+ self.theme.appendChild(background)
4904+ # Create Filename element
4905+ self.child_element(background, 'filename', filename)
4906+ # Create endColor element
4907+ self.child_element(background, 'borderColor', str(border_color))
4908+
4909 def add_font(self, name, color, size, override, fonttype='main', bold='False', italics='False',
4910 line_adjustment=0, xpos=0, ypos=0, width=0, height=0, outline='False', outline_color='#ffffff',
4911 outline_pixel=2, shadow='False', shadow_color='#ffffff', shadow_pixel=5):
4912@@ -407,7 +427,7 @@
4913 try:
4914 theme_xml = objectify.fromstring(xml)
4915 except etree.XMLSyntaxError:
4916- log.exception('Invalid xml %s', xml)
4917+ log.exception('Invalid xml {text}'.format(text=xml))
4918 return
4919 xml_iter = theme_xml.getiterator()
4920 for element in xml_iter:
4921@@ -454,15 +474,16 @@
4922 if element.startswith('shadow') or element.startswith('outline'):
4923 master = 'font_main'
4924 # fix bold font
4925+ ret_value = None
4926 if element == 'weight':
4927 element = 'bold'
4928 if value == 'Normal':
4929- value = False
4930+ ret_value = False
4931 else:
4932- value = True
4933+ ret_value = True
4934 if element == 'proportion':
4935 element = 'size'
4936- return False, master, element, value
4937+ return False, master, element, ret_value if ret_value is not None else value
4938
4939 def _create_attr(self, master, element, value):
4940 """
4941@@ -493,7 +514,8 @@
4942 theme_strings = []
4943 for key in dir(self):
4944 if key[0:1] != '_':
4945- theme_strings.append('%30s: %s' % (key, getattr(self, key)))
4946+ # TODO: Due to bound methods returned, I don't know how to write a proper test
4947+ theme_strings.append('{key:>30}: {value}'.format(key=key, value=getattr(self, key)))
4948 return '\n'.join(theme_strings)
4949
4950 def _build_xml_from_attrs(self):
4951@@ -512,6 +534,9 @@
4952 elif self.background_type == BackgroundType.to_string(BackgroundType.Image):
4953 filename = os.path.split(self.background_filename)[1]
4954 self.add_background_image(filename, self.background_border_color)
4955+ elif self.background_type == BackgroundType.to_string(BackgroundType.Video):
4956+ filename = os.path.split(self.background_filename)[1]
4957+ self.add_background_video(filename, self.background_border_color)
4958 elif self.background_type == BackgroundType.to_string(BackgroundType.Transparent):
4959 self.add_background_transparent()
4960 self.add_font(
4961
4962=== renamed file 'openlp/core/lib/toolbar.py' => 'openlp/core/lib/toolbar.py.THIS'
4963=== renamed file 'openlp/core/lib/treewidgetwithdnd.py' => 'openlp/core/lib/treewidgetwithdnd.py.THIS'
4964=== modified file 'openlp/core/lib/ui.py'
4965--- openlp/core/lib/ui.py 2016-12-31 11:05:48 +0000
4966+++ openlp/core/lib/ui.py 2017-01-08 22:05:36 +0000
4967@@ -27,8 +27,8 @@
4968 from PyQt5 import QtCore, QtGui, QtWidgets
4969
4970 from openlp.core.common import Registry, UiStrings, translate, is_macosx
4971+from openlp.core.common.actions import ActionList
4972 from openlp.core.lib import build_icon
4973-from openlp.core.utils.actions import ActionList
4974
4975
4976 log = logging.getLogger(__name__)
4977@@ -165,7 +165,7 @@
4978 kwargs.setdefault('icon', ':/services/service_down.png')
4979 kwargs.setdefault('tooltip', translate('OpenLP.Ui', 'Move selection down one position.'))
4980 else:
4981- log.warning('The role "%s" is not defined in create_push_button().', role)
4982+ log.warning('The role "{role}" is not defined in create_push_button().'.format(role=role))
4983 if kwargs.pop('btn_class', '') == 'toolbutton':
4984 button = QtWidgets.QToolButton(parent)
4985 else:
4986@@ -183,7 +183,7 @@
4987 button.clicked.connect(kwargs.pop('click'))
4988 for key in list(kwargs.keys()):
4989 if key not in ['text', 'icon', 'tooltip', 'click']:
4990- log.warning('Parameter %s was not consumed in create_button().', key)
4991+ log.warning('Parameter {key} was not consumed in create_button().'.format(key=key))
4992 return button
4993
4994
4995@@ -270,7 +270,7 @@
4996 action.triggered.connect(kwargs.pop('triggers'))
4997 for key in list(kwargs.keys()):
4998 if key not in ['text', 'icon', 'tooltip', 'statustip', 'checked', 'can_shortcuts', 'category', 'triggers']:
4999- log.warning('Parameter %s was not consumed in create_action().' % key)
5000+ log.warning('Parameter {key} was not consumed in create_action().'.format(key=key))
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: