From 4213dad51c25746fd5b04d4bd5f171d484a970c0 Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Tue, 7 Jun 2011 21:37:07 -0400 Subject: [PATCH 01/35] Confirm delete of Custom item --- openlp/plugins/custom/lib/customtab.py | 33 +++++++++++++++++++++++--- openlp/plugins/custom/lib/mediaitem.py | 14 +++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/custom/lib/customtab.py b/openlp/plugins/custom/lib/customtab.py index fb83fab81..7aabe7dda 100644 --- a/openlp/plugins/custom/lib/customtab.py +++ b/openlp/plugins/custom/lib/customtab.py @@ -47,17 +47,32 @@ class CustomTab(SettingsTab): self.displayFooterCheckBox.setObjectName(u'displayFooterCheckBox') self.customModeLayout.addRow(self.displayFooterCheckBox) self.leftLayout.addWidget(self.customModeGroupBox) - self.leftLayout.addStretch() - self.rightLayout.addStretch() QtCore.QObject.connect(self.displayFooterCheckBox, QtCore.SIGNAL(u'stateChanged(int)'), self.onDisplayFooterCheckBoxChanged) - + self.customUIGroupBox = QtGui.QGroupBox(self.leftColumn) + self.customUIGroupBox.setObjectName(u'customUIGroupBox') + self.customUILayout = QtGui.QFormLayout(self.customUIGroupBox) + self.customUILayout.setObjectName(u'customUILayout') + self.confirmDeleteCheckBox = QtGui.QCheckBox(self.customUIGroupBox) + self.confirmDeleteCheckBox.setObjectName(u'confirmDeleteCheckBox') + self.customUILayout.addRow(self.confirmDeleteCheckBox) + self.leftLayout.addWidget(self.customUIGroupBox) + QtCore.QObject.connect(self.confirmDeleteCheckBox, + QtCore.SIGNAL(u'stateChanged(int)'), + self.onConfirmDeleteCheckBoxChanged) + self.leftLayout.addStretch() + self.rightLayout.addStretch() + def retranslateUi(self): self.customModeGroupBox.setTitle(translate('CustomPlugin.CustomTab', 'Custom Display')) self.displayFooterCheckBox.setText( translate('CustomPlugin.CustomTab', 'Display footer')) + self.customUIGroupBox.setTitle(translate('CustomPlugin.CustomTab', + 'UI Settings')) + self.confirmDeleteCheckBox.setText( + translate('CustomPlugin.CustomTab', 'Confirm delete')) def onDisplayFooterCheckBoxChanged(self, check_state): self.displayFooter = False @@ -65,12 +80,24 @@ class CustomTab(SettingsTab): if check_state == QtCore.Qt.Checked: self.displayFooter = True + def onConfirmDeleteCheckBoxChanged(self, check_state): + self.confirmDelete = False + # we have a set value convert to True/False + if check_state == QtCore.Qt.Checked: + self.confirmDelete = True + def load(self): self.displayFooter = QtCore.QSettings().value( self.settingsSection + u'/display footer', QtCore.QVariant(True)).toBool() self.displayFooterCheckBox.setChecked(self.displayFooter) + self.confirmDelete = QtCore.QSettings().value( + self.settingsSection + u'/confirm delete', + QtCore.QVariant(True)).toBool() + self.confirmDeleteCheckBox.setChecked(self.confirmDelete) def save(self): QtCore.QSettings().setValue(self.settingsSection + u'/display footer', QtCore.QVariant(self.displayFooter)) + QtCore.QSettings().setValue(self.settingsSection + u'/confirm delete', + QtCore.QVariant(self.confirmDelete)) diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 94ee6d94e..fec660360 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -200,6 +200,20 @@ class CustomMediaItem(MediaManagerItem): Remove a custom item from the list and database """ if check_item_selected(self.listView, UiStrings().SelectDelete): + self.confirmDelete = QtCore.QSettings().value( + self.settingsSection + u'/confirm delete', + QtCore.QVariant(u'False')).toBool() + items = self.listView.selectedIndexes() + if self.confirmDelete: + if QtGui.QMessageBox.question(self, + translate('CustomPlugin.MediaItem', 'Delete Custom(s)?'), + translate('CustomPlugin.MediaItem', + 'Are you sure you want to delete the %n selected custom(s)?', '', + QtCore.QCoreApplication.CodecForTr, len(items)), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok | + QtGui.QMessageBox.Cancel), + QtGui.QMessageBox.Ok) == QtGui.QMessageBox.Cancel: + return row_list = [item.row() for item in self.listView.selectedIndexes()] row_list.sort(reverse=True) id_list = [(item.data(QtCore.Qt.UserRole)).toInt()[0] From 30b56d6fee78b2d205033f779249d4d6377a424b Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Sat, 11 Jun 2011 08:48:11 -0400 Subject: [PATCH 02/35] Moved signals to end of SetupUI --- openlp/plugins/custom/lib/customtab.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/custom/lib/customtab.py b/openlp/plugins/custom/lib/customtab.py index 7aabe7dda..12acabd0b 100644 --- a/openlp/plugins/custom/lib/customtab.py +++ b/openlp/plugins/custom/lib/customtab.py @@ -47,9 +47,6 @@ class CustomTab(SettingsTab): self.displayFooterCheckBox.setObjectName(u'displayFooterCheckBox') self.customModeLayout.addRow(self.displayFooterCheckBox) self.leftLayout.addWidget(self.customModeGroupBox) - QtCore.QObject.connect(self.displayFooterCheckBox, - QtCore.SIGNAL(u'stateChanged(int)'), - self.onDisplayFooterCheckBoxChanged) self.customUIGroupBox = QtGui.QGroupBox(self.leftColumn) self.customUIGroupBox.setObjectName(u'customUIGroupBox') self.customUILayout = QtGui.QFormLayout(self.customUIGroupBox) @@ -58,12 +55,15 @@ class CustomTab(SettingsTab): self.confirmDeleteCheckBox.setObjectName(u'confirmDeleteCheckBox') self.customUILayout.addRow(self.confirmDeleteCheckBox) self.leftLayout.addWidget(self.customUIGroupBox) + self.leftLayout.addStretch() + self.rightLayout.addStretch() + QtCore.QObject.connect(self.displayFooterCheckBox, + QtCore.SIGNAL(u'stateChanged(int)'), + self.onDisplayFooterCheckBoxChanged) QtCore.QObject.connect(self.confirmDeleteCheckBox, QtCore.SIGNAL(u'stateChanged(int)'), self.onConfirmDeleteCheckBoxChanged) - self.leftLayout.addStretch() - self.rightLayout.addStretch() - + def retranslateUi(self): self.customModeGroupBox.setTitle(translate('CustomPlugin.CustomTab', 'Custom Display')) From 1550239f5e37d75cbfbe0226a9292bd067af72dd Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 12 Jun 2011 15:19:32 +0100 Subject: [PATCH 03/35] Add Lock Toolbar feature - Beta 3 --- openlp/core/lib/ui.py | 5 ++-- openlp/core/ui/mainwindow.py | 54 ++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index c784a7b2b..69a4e51d1 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -323,8 +323,9 @@ def shortcut_action(parent, name, shortcuts, function, icon=None, checked=None, if checked is not None: action.setCheckable(True) action.setChecked(checked) - action.setShortcuts(shortcuts) - action.setShortcutContext(context) + if shortcuts: + action.setShortcuts(shortcuts) + action.setShortcutContext(context) action_list = ActionList.get_instance() action_list.add_action(action, category) QtCore.QObject.connect(action, QtCore.SIGNAL(u'triggered(bool)'), function) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 0dd4872fe..b7d63ae58 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -92,6 +92,8 @@ class Ui_MainWindow(object): self.previewController.panel.setVisible(previewVisible) liveVisible = QtCore.QSettings().value(u'user interface/live panel', QtCore.QVariant(True)).toBool() + panelLocked = QtCore.QSettings().value(u'user interface/lock panel', + QtCore.QVariant(False)).toBool() self.liveController.panel.setVisible(liveVisible) # Create menu self.menuBar = QtGui.QMenuBar(mainWindow) @@ -212,7 +214,11 @@ class Ui_MainWindow(object): self.viewLivePanel = shortcut_action(mainWindow, u'viewLivePanel', [QtGui.QKeySequence(u'F12')], self.setLivePanelVisibility, checked=liveVisible, category=UiStrings().View) - action_list.add_category(UiStrings().ViewMode, CategoryOrder.standardMenu) + self.lockPanel = shortcut_action(mainWindow, u'lockPanel', + None, self.setLockPanel, + checked=panelLocked, category=None) + action_list.add_category(UiStrings().ViewMode, + CategoryOrder.standardMenu) self.modeDefaultItem = checkable_action( mainWindow, u'modeDefaultItem', category=UiStrings().ViewMode) self.modeSetupItem = checkable_action( @@ -232,7 +238,8 @@ class Ui_MainWindow(object): category=UiStrings().Tools) self.updateThemeImages = base_action(mainWindow, u'updateThemeImages', category=UiStrings().Tools) - action_list.add_category(UiStrings().Settings, CategoryOrder.standardMenu) + action_list.add_category(UiStrings().Settings, + CategoryOrder.standardMenu) self.settingsPluginListItem = shortcut_action(mainWindow, u'settingsPluginListItem', [QtGui.QKeySequence(u'Alt+F7')], self.onPluginItemClicked, u':/system/settings_plugin_list.png', @@ -287,7 +294,7 @@ class Ui_MainWindow(object): add_actions(self.viewMenu, (self.viewModeMenu.menuAction(), None, self.viewMediaManagerItem, self.viewServiceManagerItem, self.viewThemeManagerItem, None, self.viewPreviewPanel, - self.viewLivePanel)) + self.viewLivePanel, None, self.lockPanel)) # i18n add Language Actions add_actions(self.settingsLanguageMenu, (self.autoLanguageItem, None)) add_actions(self.settingsLanguageMenu, self.languageGroup.actions()) @@ -316,6 +323,7 @@ class Ui_MainWindow(object): self.importLanguageItem.setVisible(False) self.exportLanguageItem.setVisible(False) self.helpDocumentationItem.setVisible(False) + self.setLockPanel(panelLocked) def retranslateUi(self, mainWindow): """ @@ -405,6 +413,10 @@ class Ui_MainWindow(object): translate('OpenLP.MainWindow', '&Live Panel')) self.viewLivePanel.setToolTip( translate('OpenLP.MainWindow', 'Toggle Live Panel')) + self.lockPanel.setText( + translate('OpenLP.MainWindow', 'L&ock Panels')) + self.lockPanel.setToolTip( + translate('OpenLP.MainWindow', 'Prevent Panels changing')) self.viewLivePanel.setStatusTip(translate('OpenLP.MainWindow', 'Toggle the visibility of the live panel.')) self.settingsPluginListItem.setText(translate('OpenLP.MainWindow', @@ -644,7 +656,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): QtCore.QVariant(False)).toBool(): self.serviceManagerContents.loadLastFile() view_mode = QtCore.QSettings().value(u'%s/view mode' % \ - self.generalSettingsSection, u'default') + self.generalSettingsSection, u'default').toString() if view_mode == u'default': self.modeDefaultItem.setChecked(True) elif view_mode == u'setup': @@ -927,7 +939,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): self.mediaManagerDock.setVisible(not self.mediaManagerDock.isVisible()) def toggleServiceManager(self): - self.serviceManagerDock.setVisible(not self.serviceManagerDock.isVisible()) + self.serviceManagerDock.setVisible( + not self.serviceManagerDock.isVisible()) def toggleThemeManager(self): self.themeManagerDock.setVisible(not self.themeManagerDock.isVisible()) @@ -947,6 +960,37 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): QtCore.QVariant(visible)) self.viewPreviewPanel.setChecked(visible) + def setLockPanel(self, lock): + """ + Sets the ability to stiop the toolbars being changed. + """ + if lock: + self.themeManagerDock.setFeatures( + QtGui.QDockWidget.NoDockWidgetFeatures) + self.serviceManagerDock.setFeatures( + QtGui.QDockWidget.NoDockWidgetFeatures) + self.mediaManagerDock.setFeatures( + QtGui.QDockWidget.NoDockWidgetFeatures) + self.viewMediaManagerItem.setEnabled(False) + self.viewServiceManagerItem.setEnabled(False) + self.viewThemeManagerItem.setEnabled(False) + self.viewPreviewPanel.setEnabled(False) + self.viewLivePanel.setEnabled(False) + else: + self.themeManagerDock.setFeatures( + QtGui.QDockWidget.AllDockWidgetFeatures) + self.serviceManagerDock.setFeatures( + QtGui.QDockWidget.AllDockWidgetFeatures) + self.mediaManagerDock.setFeatures( + QtGui.QDockWidget.AllDockWidgetFeatures) + self.viewMediaManagerItem.setEnabled(True) + self.viewServiceManagerItem.setEnabled(True) + self.viewThemeManagerItem.setEnabled(True) + self.viewPreviewPanel.setEnabled(True) + self.viewLivePanel.setEnabled(True) + QtCore.QSettings().setValue(u'user interface/lock panel', + QtCore.QVariant(lock)) + def setLivePanelVisibility(self, visible): """ Sets the visibility of the live panel including saving the setting and From 2939151ff1e78841b361fe1090617ea07b13f6d5 Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 21 Jun 2011 07:40:53 +0200 Subject: [PATCH 04/35] EasyWorship importer: added conversion of Tags - basically working, but some issues remain --- openlp/plugins/songs/lib/ewimport.py | 728 ++++++++++++++------------- 1 file changed, 380 insertions(+), 348 deletions(-) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index 09f84fbe2..2431743d6 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -1,348 +1,380 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2011 Raoul Snyman # -# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, # -# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias # -# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -The :mod:`ewimport` module provides the functionality for importing -EasyWorship song databases into the current installation database. -""" - -import os -import struct - -from openlp.core.lib import translate -from openlp.core.ui.wizard import WizardStrings -from openlp.plugins.songs.lib import VerseType -from openlp.plugins.songs.lib import retrieve_windows_encoding -from songimport import SongImport - -def strip_rtf(blob, encoding): - depth = 0 - control = False - clear_text = [] - control_word = [] - for c in blob: - if control: - # for delimiters, set control to False - if c == '{': - if len(control_word) > 0: - depth += 1 - control = False - elif c == '}': - if len(control_word) > 0: - depth -= 1 - control = False - elif c == '\\': - new_control = (len(control_word) > 0) - control = False - elif c.isspace(): - control = False - else: - control_word.append(c) - if len(control_word) == 3 and control_word[0] == '\'': - control = False - if not control: - if len(control_word) == 0: - if c == '{' or c == '}' or c == '\\': - clear_text.append(c) - else: - control_str = ''.join(control_word) - if control_str == 'par' or control_str == 'line': - clear_text.append(u'\n') - elif control_str == 'tab': - clear_text.append(u'\t') - # Prefer the encoding specified by the RTF data to that - # specified by the Paradox table header - # West European encoding - elif control_str == 'fcharset0': - encoding = u'cp1252' - # Greek encoding - elif control_str == 'fcharset161': - encoding = u'cp1253' - # Turkish encoding - elif control_str == 'fcharset162': - encoding = u'cp1254' - # Vietnamese encoding - elif control_str == 'fcharset163': - encoding = u'cp1258' - # Hebrew encoding - elif control_str == 'fcharset177': - encoding = u'cp1255' - # Arabic encoding - elif control_str == 'fcharset178': - encoding = u'cp1256' - # Baltic encoding - elif control_str == 'fcharset186': - encoding = u'cp1257' - # Cyrillic encoding - elif control_str == 'fcharset204': - encoding = u'cp1251' - # Thai encoding - elif control_str == 'fcharset222': - encoding = u'cp874' - # Central+East European encoding - elif control_str == 'fcharset238': - encoding = u'cp1250' - elif control_str[0] == '\'': - s = chr(int(control_str[1:3], 16)) - clear_text.append(s.decode(encoding)) - del control_word[:] - if c == '\\' and new_control: - control = True - elif c == '{': - depth += 1 - elif c == '}': - depth -= 1 - elif depth > 2: - continue - elif c == '\n' or c == '\r': - continue - elif c == '\\': - control = True - else: - clear_text.append(c) - return u''.join(clear_text) - -class FieldDescEntry: - def __init__(self, name, type, size): - self.name = name - self.type = type - self.size = size - - -class EasyWorshipSongImport(SongImport): - """ - The :class:`EasyWorshipSongImport` class provides OpenLP with the - ability to import EasyWorship song files. - """ - def __init__(self, manager, **kwargs): - SongImport.__init__(self, manager, **kwargs) - - def do_import(self): - # Open the DB and MB files if they exist - import_source_mb = self.import_source.replace('.DB', '.MB') - if not os.path.isfile(self.import_source): - return - if not os.path.isfile(import_source_mb): - return - db_size = os.path.getsize(self.import_source) - if db_size < 0x800: - return - db_file = open(self.import_source, 'rb') - self.memo_file = open(import_source_mb, 'rb') - # Don't accept files that are clearly not paradox files - record_size, header_size, block_size, first_block, num_fields \ - = struct.unpack(' 4: - db_file.close() - self.memo_file.close() - return - # Take a stab at how text is encoded - self.encoding = u'cp1252' - db_file.seek(106) - code_page, = struct.unpack(''] - for field_desc in field_descs: - if field_desc.type == 1: - # string - fsl.append('%ds' % field_desc.size) - elif field_desc.type == 3: - # 16-bit int - fsl.append('H') - elif field_desc.type == 4: - # 32-bit int - fsl.append('I') - elif field_desc.type == 9: - # Logical - fsl.append('B') - elif field_desc.type == 0x0c: - # Memo - fsl.append('%ds' % field_desc.size) - elif field_desc.type == 0x0d: - # Blob - fsl.append('%ds' % field_desc.size) - elif field_desc.type == 0x15: - # Timestamp - fsl.append('Q') - else: - fsl.append('%ds' % field_desc.size) - self.record_struct = struct.Struct(''.join(fsl)) - self.field_descs = field_descs - - def get_field(self, field_desc_index): - field = self.fields[field_desc_index] - field_desc = self.field_descs[field_desc_index] - # Return None in case of 'blank' entries - if isinstance(field, str): - if len(field.rstrip('\0')) == 0: - return None - elif field == 0: - return None - # Format the field depending on the field type - if field_desc.type == 1: - # string - return field.rstrip('\0').decode(self.encoding) - elif field_desc.type == 3: - # 16-bit int - return field ^ 0x8000 - elif field_desc.type == 4: - # 32-bit int - return field ^ 0x80000000 - elif field_desc.type == 9: - # Logical - return (field ^ 0x80 == 1) - elif field_desc.type == 0x0c or field_desc.type == 0x0d: - # Memo or Blob - block_start, blob_size = \ - struct.unpack_from(' 63: - return u'' - self.memo_file.seek(11 + (5 * sub_block), os.SEEK_CUR) - sub_block_start, = struct.unpack('B', self.memo_file.read(1)) - self.memo_file.seek(block_start + (sub_block_start * 16)) - else: - return u'' - return self.memo_file.read(blob_size) - else: - return 0 +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2011 Raoul Snyman # +# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, # +# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias # +# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`ewimport` module provides the functionality for importing +EasyWorship song databases into the current installation database. +""" + +import os +import struct +import re + +from openlp.core.lib import translate +from openlp.core.ui.wizard import WizardStrings +from openlp.plugins.songs.lib import VerseType +from openlp.plugins.songs.lib import retrieve_windows_encoding +from songimport import SongImport + +def strip_rtf(blob, encoding): + depth = 0 + control = False + clear_text = [] + control_word = [] + for c in blob: + if control: + # for delimiters, set control to False + if c == '{': + if len(control_word) > 0: + depth += 1 + control = False + elif c == '}': + if len(control_word) > 0: + depth -= 1 + control = False + elif c == '\\': + new_control = (len(control_word) > 0) + control = False + elif c.isspace(): + control = False + else: + control_word.append(c) + if len(control_word) == 3 and control_word[0] == '\'': + control = False + if not control: + if len(control_word) == 0: + if c == '{' or c == '}' or c == '\\': + clear_text.append(c) + else: + control_str = ''.join(control_word) + if control_str == 'par' or control_str == 'line': + clear_text.append(u'\n') + elif control_str == 'tab': + clear_text.append(u'\t') + # Prefer the encoding specified by the RTF data to that + # specified by the Paradox table header + # West European encoding + elif control_str == 'fcharset0': + encoding = u'cp1252' + # Greek encoding + elif control_str == 'fcharset161': + encoding = u'cp1253' + # Turkish encoding + elif control_str == 'fcharset162': + encoding = u'cp1254' + # Vietnamese encoding + elif control_str == 'fcharset163': + encoding = u'cp1258' + # Hebrew encoding + elif control_str == 'fcharset177': + encoding = u'cp1255' + # Arabic encoding + elif control_str == 'fcharset178': + encoding = u'cp1256' + # Baltic encoding + elif control_str == 'fcharset186': + encoding = u'cp1257' + # Cyrillic encoding + elif control_str == 'fcharset204': + encoding = u'cp1251' + # Thai encoding + elif control_str == 'fcharset222': + encoding = u'cp874' + # Central+East European encoding + elif control_str == 'fcharset238': + encoding = u'cp1250' + elif control_str[0] == '\'': + s = chr(int(control_str[1:3], 16)) + clear_text.append(s.decode(encoding)) + del control_word[:] + if c == '\\' and new_control: + control = True + elif c == '{': + depth += 1 + elif c == '}': + depth -= 1 + elif depth > 2: + continue + elif c == '\n' or c == '\r': + continue + elif c == '\\': + control = True + else: + clear_text.append(c) + return u''.join(clear_text) + +class FieldDescEntry: + def __init__(self, name, type, size): + self.name = name + self.type = type + self.size = size + + +class EasyWorshipSongImport(SongImport): + """ + The :class:`EasyWorshipSongImport` class provides OpenLP with the + ability to import EasyWorship song files. + """ + def __init__(self, manager, **kwargs): + SongImport.__init__(self, manager, **kwargs) + + def do_import(self): + # Open the DB and MB files if they exist + import_source_mb = self.import_source.replace('.DB', '.MB') + if not os.path.isfile(self.import_source): + return + if not os.path.isfile(import_source_mb): + return + db_size = os.path.getsize(self.import_source) + if db_size < 0x800: + return + db_file = open(self.import_source, 'rb') + self.memo_file = open(import_source_mb, 'rb') + # Don't accept files that are clearly not paradox files + record_size, header_size, block_size, first_block, num_fields \ + = struct.unpack(' 4: + db_file.close() + self.memo_file.close() + return + # Take a stab at how text is encoded + self.encoding = u'cp1252' + db_file.seek(106) + code_page, = struct.unpack(' len(type): # tag is followed by number and/or note + p = re.compile(r'[0-9]+') + m = re.search(p, ew_tag) + if m: + number = m.group() + verse_type +=number + + p = re.compile(r'\(.*\)') + m = re.search(p, ew_tag) + if m: + self.comments += ew_tag+'\n' + break + + self.add_verse( + verse_split[-1].strip() if first_line_is_tag else verse.strip(), # TODO: hacky: -1 + verse_type) + if len(self.comments) > 5: + self.comments += unicode(translate('SongsPlugin.EasyWorshipSongImport', + '\n[above are Song Tags with notes imported from EasyWorship]')) + if self.stop_import_flag: + break + if not self.finish(): + self.log_error(self.import_source) + db_file.close() + self.memo_file.close() + + def find_field(self, field_name): + return [i for i, x in enumerate(self.field_descs) + if x.name == field_name][0] + + def set_record_struct(self, field_descs): + # Begin with empty field struct list + fsl = ['>'] + for field_desc in field_descs: + if field_desc.type == 1: + # string + fsl.append('%ds' % field_desc.size) + elif field_desc.type == 3: + # 16-bit int + fsl.append('H') + elif field_desc.type == 4: + # 32-bit int + fsl.append('I') + elif field_desc.type == 9: + # Logical + fsl.append('B') + elif field_desc.type == 0x0c: + # Memo + fsl.append('%ds' % field_desc.size) + elif field_desc.type == 0x0d: + # Blob + fsl.append('%ds' % field_desc.size) + elif field_desc.type == 0x15: + # Timestamp + fsl.append('Q') + else: + fsl.append('%ds' % field_desc.size) + self.record_struct = struct.Struct(''.join(fsl)) + self.field_descs = field_descs + + def get_field(self, field_desc_index): + field = self.fields[field_desc_index] + field_desc = self.field_descs[field_desc_index] + # Return None in case of 'blank' entries + if isinstance(field, str): + if len(field.rstrip('\0')) == 0: + return None + elif field == 0: + return None + # Format the field depending on the field type + if field_desc.type == 1: + # string + return field.rstrip('\0').decode(self.encoding) + elif field_desc.type == 3: + # 16-bit int + return field ^ 0x8000 + elif field_desc.type == 4: + # 32-bit int + return field ^ 0x80000000 + elif field_desc.type == 9: + # Logical + return (field ^ 0x80 == 1) + elif field_desc.type == 0x0c or field_desc.type == 0x0d: + # Memo or Blob + block_start, blob_size = \ + struct.unpack_from(' 63: + return u'' + self.memo_file.seek(11 + (5 * sub_block), os.SEEK_CUR) + sub_block_start, = struct.unpack('B', self.memo_file.read(1)) + self.memo_file.seek(block_start + (sub_block_start * 16)) + else: + return u'' + return self.memo_file.read(blob_size) + else: + return 0 From 31dd4945bae57b2aeaa2f0310290d40824413b6d Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 21 Jun 2011 07:55:11 +0200 Subject: [PATCH 05/35] fixed line endings --- openlp/plugins/songs/lib/ewimport.py | 759 +++++++++++++-------------- 1 file changed, 379 insertions(+), 380 deletions(-) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index 2431743d6..fb82ab347 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -1,380 +1,379 @@ -# -*- coding: utf-8 -*- -# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 - -############################################################################### -# OpenLP - Open Source Lyrics Projection # -# --------------------------------------------------------------------------- # -# Copyright (c) 2008-2011 Raoul Snyman # -# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan # -# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, # -# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias # -# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # -# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund # -# --------------------------------------------------------------------------- # -# This program is free software; you can redistribute it and/or modify it # -# under the terms of the GNU General Public License as published by the Free # -# Software Foundation; version 2 of the License. # -# # -# This program is distributed in the hope that it will be useful, but WITHOUT # -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # -# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # -# more details. # -# # -# You should have received a copy of the GNU General Public License along # -# with this program; if not, write to the Free Software Foundation, Inc., 59 # -# Temple Place, Suite 330, Boston, MA 02111-1307 USA # -############################################################################### -""" -The :mod:`ewimport` module provides the functionality for importing -EasyWorship song databases into the current installation database. -""" - -import os -import struct -import re - -from openlp.core.lib import translate -from openlp.core.ui.wizard import WizardStrings -from openlp.plugins.songs.lib import VerseType -from openlp.plugins.songs.lib import retrieve_windows_encoding -from songimport import SongImport - -def strip_rtf(blob, encoding): - depth = 0 - control = False - clear_text = [] - control_word = [] - for c in blob: - if control: - # for delimiters, set control to False - if c == '{': - if len(control_word) > 0: - depth += 1 - control = False - elif c == '}': - if len(control_word) > 0: - depth -= 1 - control = False - elif c == '\\': - new_control = (len(control_word) > 0) - control = False - elif c.isspace(): - control = False - else: - control_word.append(c) - if len(control_word) == 3 and control_word[0] == '\'': - control = False - if not control: - if len(control_word) == 0: - if c == '{' or c == '}' or c == '\\': - clear_text.append(c) - else: - control_str = ''.join(control_word) - if control_str == 'par' or control_str == 'line': - clear_text.append(u'\n') - elif control_str == 'tab': - clear_text.append(u'\t') - # Prefer the encoding specified by the RTF data to that - # specified by the Paradox table header - # West European encoding - elif control_str == 'fcharset0': - encoding = u'cp1252' - # Greek encoding - elif control_str == 'fcharset161': - encoding = u'cp1253' - # Turkish encoding - elif control_str == 'fcharset162': - encoding = u'cp1254' - # Vietnamese encoding - elif control_str == 'fcharset163': - encoding = u'cp1258' - # Hebrew encoding - elif control_str == 'fcharset177': - encoding = u'cp1255' - # Arabic encoding - elif control_str == 'fcharset178': - encoding = u'cp1256' - # Baltic encoding - elif control_str == 'fcharset186': - encoding = u'cp1257' - # Cyrillic encoding - elif control_str == 'fcharset204': - encoding = u'cp1251' - # Thai encoding - elif control_str == 'fcharset222': - encoding = u'cp874' - # Central+East European encoding - elif control_str == 'fcharset238': - encoding = u'cp1250' - elif control_str[0] == '\'': - s = chr(int(control_str[1:3], 16)) - clear_text.append(s.decode(encoding)) - del control_word[:] - if c == '\\' and new_control: - control = True - elif c == '{': - depth += 1 - elif c == '}': - depth -= 1 - elif depth > 2: - continue - elif c == '\n' or c == '\r': - continue - elif c == '\\': - control = True - else: - clear_text.append(c) - return u''.join(clear_text) - -class FieldDescEntry: - def __init__(self, name, type, size): - self.name = name - self.type = type - self.size = size - - -class EasyWorshipSongImport(SongImport): - """ - The :class:`EasyWorshipSongImport` class provides OpenLP with the - ability to import EasyWorship song files. - """ - def __init__(self, manager, **kwargs): - SongImport.__init__(self, manager, **kwargs) - - def do_import(self): - # Open the DB and MB files if they exist - import_source_mb = self.import_source.replace('.DB', '.MB') - if not os.path.isfile(self.import_source): - return - if not os.path.isfile(import_source_mb): - return - db_size = os.path.getsize(self.import_source) - if db_size < 0x800: - return - db_file = open(self.import_source, 'rb') - self.memo_file = open(import_source_mb, 'rb') - # Don't accept files that are clearly not paradox files - record_size, header_size, block_size, first_block, num_fields \ - = struct.unpack(' 4: - db_file.close() - self.memo_file.close() - return - # Take a stab at how text is encoded - self.encoding = u'cp1252' - db_file.seek(106) - code_page, = struct.unpack(' len(type): # tag is followed by number and/or note - p = re.compile(r'[0-9]+') - m = re.search(p, ew_tag) - if m: - number = m.group() - verse_type +=number - - p = re.compile(r'\(.*\)') - m = re.search(p, ew_tag) - if m: - self.comments += ew_tag+'\n' - break - - self.add_verse( - verse_split[-1].strip() if first_line_is_tag else verse.strip(), # TODO: hacky: -1 - verse_type) - if len(self.comments) > 5: - self.comments += unicode(translate('SongsPlugin.EasyWorshipSongImport', - '\n[above are Song Tags with notes imported from EasyWorship]')) - if self.stop_import_flag: - break - if not self.finish(): - self.log_error(self.import_source) - db_file.close() - self.memo_file.close() - - def find_field(self, field_name): - return [i for i, x in enumerate(self.field_descs) - if x.name == field_name][0] - - def set_record_struct(self, field_descs): - # Begin with empty field struct list - fsl = ['>'] - for field_desc in field_descs: - if field_desc.type == 1: - # string - fsl.append('%ds' % field_desc.size) - elif field_desc.type == 3: - # 16-bit int - fsl.append('H') - elif field_desc.type == 4: - # 32-bit int - fsl.append('I') - elif field_desc.type == 9: - # Logical - fsl.append('B') - elif field_desc.type == 0x0c: - # Memo - fsl.append('%ds' % field_desc.size) - elif field_desc.type == 0x0d: - # Blob - fsl.append('%ds' % field_desc.size) - elif field_desc.type == 0x15: - # Timestamp - fsl.append('Q') - else: - fsl.append('%ds' % field_desc.size) - self.record_struct = struct.Struct(''.join(fsl)) - self.field_descs = field_descs - - def get_field(self, field_desc_index): - field = self.fields[field_desc_index] - field_desc = self.field_descs[field_desc_index] - # Return None in case of 'blank' entries - if isinstance(field, str): - if len(field.rstrip('\0')) == 0: - return None - elif field == 0: - return None - # Format the field depending on the field type - if field_desc.type == 1: - # string - return field.rstrip('\0').decode(self.encoding) - elif field_desc.type == 3: - # 16-bit int - return field ^ 0x8000 - elif field_desc.type == 4: - # 32-bit int - return field ^ 0x80000000 - elif field_desc.type == 9: - # Logical - return (field ^ 0x80 == 1) - elif field_desc.type == 0x0c or field_desc.type == 0x0d: - # Memo or Blob - block_start, blob_size = \ - struct.unpack_from(' 63: - return u'' - self.memo_file.seek(11 + (5 * sub_block), os.SEEK_CUR) - sub_block_start, = struct.unpack('B', self.memo_file.read(1)) - self.memo_file.seek(block_start + (sub_block_start * 16)) - else: - return u'' - return self.memo_file.read(blob_size) - else: - return 0 +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# OpenLP - Open Source Lyrics Projection # +# --------------------------------------------------------------------------- # +# Copyright (c) 2008-2011 Raoul Snyman # +# Portions copyright (c) 2008-2011 Tim Bentley, Gerald Britton, Jonathan # +# Corwin, Michael Gorven, Scott Guerrieri, Matthias Hub, Meinert Jordan, # +# Armin Köhler, Joshua Miller, Stevan Pettit, Andreas Preikschat, Mattias # +# Põldaru, Christian Richter, Philip Ridout, Simon Scudder, Jeffrey Smith, # +# Maikel Stuivenberg, Martin Thompson, Jon Tibble, Frode Woldsund # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### +""" +The :mod:`ewimport` module provides the functionality for importing +EasyWorship song databases into the current installation database. +""" + +import os +import struct +import re + +from openlp.core.lib import translate +from openlp.core.ui.wizard import WizardStrings +from openlp.plugins.songs.lib import VerseType +from openlp.plugins.songs.lib import retrieve_windows_encoding +from songimport import SongImport + +def strip_rtf(blob, encoding): + depth = 0 + control = False + clear_text = [] + control_word = [] + for c in blob: + if control: + # for delimiters, set control to False + if c == '{': + if len(control_word) > 0: + depth += 1 + control = False + elif c == '}': + if len(control_word) > 0: + depth -= 1 + control = False + elif c == '\\': + new_control = (len(control_word) > 0) + control = False + elif c.isspace(): + control = False + else: + control_word.append(c) + if len(control_word) == 3 and control_word[0] == '\'': + control = False + if not control: + if len(control_word) == 0: + if c == '{' or c == '}' or c == '\\': + clear_text.append(c) + else: + control_str = ''.join(control_word) + if control_str == 'par' or control_str == 'line': + clear_text.append(u'\n') + elif control_str == 'tab': + clear_text.append(u'\t') + # Prefer the encoding specified by the RTF data to that + # specified by the Paradox table header + # West European encoding + elif control_str == 'fcharset0': + encoding = u'cp1252' + # Greek encoding + elif control_str == 'fcharset161': + encoding = u'cp1253' + # Turkish encoding + elif control_str == 'fcharset162': + encoding = u'cp1254' + # Vietnamese encoding + elif control_str == 'fcharset163': + encoding = u'cp1258' + # Hebrew encoding + elif control_str == 'fcharset177': + encoding = u'cp1255' + # Arabic encoding + elif control_str == 'fcharset178': + encoding = u'cp1256' + # Baltic encoding + elif control_str == 'fcharset186': + encoding = u'cp1257' + # Cyrillic encoding + elif control_str == 'fcharset204': + encoding = u'cp1251' + # Thai encoding + elif control_str == 'fcharset222': + encoding = u'cp874' + # Central+East European encoding + elif control_str == 'fcharset238': + encoding = u'cp1250' + elif control_str[0] == '\'': + s = chr(int(control_str[1:3], 16)) + clear_text.append(s.decode(encoding)) + del control_word[:] + if c == '\\' and new_control: + control = True + elif c == '{': + depth += 1 + elif c == '}': + depth -= 1 + elif depth > 2: + continue + elif c == '\n' or c == '\r': + continue + elif c == '\\': + control = True + else: + clear_text.append(c) + return u''.join(clear_text) + +class FieldDescEntry: + def __init__(self, name, type, size): + self.name = name + self.type = type + self.size = size + + +class EasyWorshipSongImport(SongImport): + """ + The :class:`EasyWorshipSongImport` class provides OpenLP with the + ability to import EasyWorship song files. + """ + def __init__(self, manager, **kwargs): + SongImport.__init__(self, manager, **kwargs) + + def do_import(self): + # Open the DB and MB files if they exist + import_source_mb = self.import_source.replace('.DB', '.MB') + if not os.path.isfile(self.import_source): + return + if not os.path.isfile(import_source_mb): + return + db_size = os.path.getsize(self.import_source) + if db_size < 0x800: + return + db_file = open(self.import_source, 'rb') + self.memo_file = open(import_source_mb, 'rb') + # Don't accept files that are clearly not paradox files + record_size, header_size, block_size, first_block, num_fields \ + = struct.unpack(' 4: + db_file.close() + self.memo_file.close() + return + # Take a stab at how text is encoded + self.encoding = u'cp1252' + db_file.seek(106) + code_page, = struct.unpack(' len(type): # tag is followed by number and/or note + p = re.compile(r'[0-9]+') + m = re.search(p, ew_tag) + if m: + number = m.group() + verse_type +=number + + p = re.compile(r'\(.*\)') + m = re.search(p, ew_tag) + if m: + self.comments += ew_tag+'\n' + break + self.add_verse( + verse_split[-1].strip() if first_line_is_tag else verse.strip(), # TODO: hacky: -1 + verse_type) + if len(self.comments) > 5: + self.comments += unicode(translate('SongsPlugin.EasyWorshipSongImport', + '\n[above are Song Tags with notes imported from EasyWorship]')) + if self.stop_import_flag: + break + if not self.finish(): + self.log_error(self.import_source) + db_file.close() + self.memo_file.close() + + def find_field(self, field_name): + return [i for i, x in enumerate(self.field_descs) + if x.name == field_name][0] + + def set_record_struct(self, field_descs): + # Begin with empty field struct list + fsl = ['>'] + for field_desc in field_descs: + if field_desc.type == 1: + # string + fsl.append('%ds' % field_desc.size) + elif field_desc.type == 3: + # 16-bit int + fsl.append('H') + elif field_desc.type == 4: + # 32-bit int + fsl.append('I') + elif field_desc.type == 9: + # Logical + fsl.append('B') + elif field_desc.type == 0x0c: + # Memo + fsl.append('%ds' % field_desc.size) + elif field_desc.type == 0x0d: + # Blob + fsl.append('%ds' % field_desc.size) + elif field_desc.type == 0x15: + # Timestamp + fsl.append('Q') + else: + fsl.append('%ds' % field_desc.size) + self.record_struct = struct.Struct(''.join(fsl)) + self.field_descs = field_descs + + def get_field(self, field_desc_index): + field = self.fields[field_desc_index] + field_desc = self.field_descs[field_desc_index] + # Return None in case of 'blank' entries + if isinstance(field, str): + if len(field.rstrip('\0')) == 0: + return None + elif field == 0: + return None + # Format the field depending on the field type + if field_desc.type == 1: + # string + return field.rstrip('\0').decode(self.encoding) + elif field_desc.type == 3: + # 16-bit int + return field ^ 0x8000 + elif field_desc.type == 4: + # 32-bit int + return field ^ 0x80000000 + elif field_desc.type == 9: + # Logical + return (field ^ 0x80 == 1) + elif field_desc.type == 0x0c or field_desc.type == 0x0d: + # Memo or Blob + block_start, blob_size = \ + struct.unpack_from(' 63: + return u'' + self.memo_file.seek(11 + (5 * sub_block), os.SEEK_CUR) + sub_block_start, = struct.unpack('B', self.memo_file.read(1)) + self.memo_file.seek(block_start + (sub_block_start * 16)) + else: + return u'' + return self.memo_file.read(blob_size) + else: + return 0 From 217510281c902c26cfa68829dd63d55e80215fcc Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Sun, 26 Jun 2011 07:49:10 +0100 Subject: [PATCH 06/35] Fix comments --- openlp/core/ui/mainwindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 11467cded..12b59763d 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -424,8 +424,8 @@ class Ui_MainWindow(object): translate('OpenLP.MainWindow', 'Toggle Live Panel')) self.lockPanel.setText( translate('OpenLP.MainWindow', 'L&ock Panels')) - self.lockPanel.setToolTip( - translate('OpenLP.MainWindow', 'Prevent Panels changing')) + self.lockPanel.setStatusTip( + translate('OpenLP.MainWindow', 'Prevent the Panels being moved.')) self.viewLivePanel.setStatusTip(translate('OpenLP.MainWindow', 'Toggle the visibility of the live panel.')) self.settingsPluginListItem.setText(translate('OpenLP.MainWindow', @@ -971,7 +971,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): def setLockPanel(self, lock): """ - Sets the ability to stiop the toolbars being changed. + Sets the ability to stop the toolbars being changed. """ if lock: self.themeManagerDock.setFeatures( From f7f20f9722cab57ed9fedef58bbcfa3294613989 Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Mon, 27 Jun 2011 00:05:25 -0400 Subject: [PATCH 07/35] Add link to Windows help file from main window --- openlp/core/ui/mainwindow.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 0210da52d..4148b40eb 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -269,7 +269,20 @@ class Ui_MainWindow(object): self.helpAboutItem = shortcut_action(mainWindow, u'helpAboutItem', [QtGui.QKeySequence(u'Ctrl+F1')], self.onHelpAboutItemClicked, u':/system/system_about.png', category=UiStrings().Help) - self.helpOnlineHelpItem = shortcut_action( + self.localHelpFile = os.path.join( + AppLocation.get_directory(AppLocation.AppDir), 'Openlp.chm') + self.haveHelpFile = os.path.isfile(self.localHelpFile) + if self.haveHelpFile: + self.helpLocalHelpItem = shortcut_action( + mainWindow, u'helpLocalHelpItem', [QtGui.QKeySequence(u'F1')], + self.onHelpLocalHelpClicked, u':/system/system_about.png', + category=UiStrings().Help) + self.helpOnlineHelpItem = shortcut_action( + mainWindow, u'helpOnlineHelpItem', [QtGui.QKeySequence(u'Alt+F1')], + self.onHelpOnlineHelpClicked, u':/system/system_online_help.png', + category=UiStrings().Help) + else: + self.helpOnlineHelpItem = shortcut_action( mainWindow, u'helpOnlineHelpItem', [QtGui.QKeySequence(u'F1')], self.onHelpOnlineHelpClicked, u':/system/system_online_help.png', category=UiStrings().Help) @@ -307,9 +320,14 @@ class Ui_MainWindow(object): add_actions(self.toolsMenu, (self.toolsAddToolItem, None)) add_actions(self.toolsMenu, (self.toolsOpenDataFolder, None)) add_actions(self.toolsMenu, [self.updateThemeImages]) - add_actions(self.helpMenu, (self.helpDocumentationItem, + add_actions(self.helpMenu, (self.helpDocumentationItem, None)) + if self.haveHelpFile: + add_actions(self.helpMenu, (self.helpLocalHelpItem, self.helpOnlineHelpItem, None, self.helpWebSiteItem, self.helpAboutItem)) + else: + add_actions(self.helpMenu, (self.helpOnlineHelpItem, None, + self.helpWebSiteItem, self.helpAboutItem)) add_actions(self.menuBar, (self.fileMenu.menuAction(), self.viewMenu.menuAction(), self.toolsMenu.menuAction(), self.settingsMenu.menuAction(), self.helpMenu.menuAction())) @@ -425,6 +443,9 @@ class Ui_MainWindow(object): self.helpAboutItem.setText(translate('OpenLP.MainWindow', '&About')) self.helpAboutItem.setStatusTip( translate('OpenLP.MainWindow', 'More information about OpenLP')) + if self.haveHelpFile: + self.helpLocalHelpItem.setText( + translate('OpenLP.MainWindow', '&Help')) self.helpOnlineHelpItem.setText( translate('OpenLP.MainWindow', '&Online Help')) self.helpWebSiteItem.setText( @@ -723,6 +744,12 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): import webbrowser webbrowser.open_new(u'http://openlp.org/') + def onHelpLocalHelpClicked(self): + """ + Load the local OpenLP help file + """ + os.startfile(self.localHelpFile) + def onHelpOnlineHelpClicked(self): """ Load the online OpenLP manual From 2d60f3842ac7107521afe4543a4c6853e65d4406 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Mon, 27 Jun 2011 16:56:48 +0100 Subject: [PATCH 08/35] review comment --- openlp/core/ui/mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 191b26998..e73ea5b80 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -425,7 +425,7 @@ class Ui_MainWindow(object): self.lockPanel.setText( translate('OpenLP.MainWindow', 'L&ock Panels')) self.lockPanel.setStatusTip( - translate('OpenLP.MainWindow', 'Prevent the Panels being moved.')) + translate('OpenLP.MainWindow', 'Prevent the panels being moved.')) self.viewLivePanel.setStatusTip(translate('OpenLP.MainWindow', 'Toggle the visibility of the live panel.')) self.settingsPluginListItem.setText(translate('OpenLP.MainWindow', From 231c47db531ac054d8320a3035e0108225f1cfb0 Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Tue, 28 Jun 2011 00:52:46 -0400 Subject: [PATCH 09/35] Remove settings for confirm delete --- openlp/plugins/custom/lib/customtab.py | 29 +------------------------- openlp/plugins/custom/lib/mediaitem.py | 22 ++++++++----------- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/openlp/plugins/custom/lib/customtab.py b/openlp/plugins/custom/lib/customtab.py index 12acabd0b..b16746f8b 100644 --- a/openlp/plugins/custom/lib/customtab.py +++ b/openlp/plugins/custom/lib/customtab.py @@ -47,57 +47,30 @@ class CustomTab(SettingsTab): self.displayFooterCheckBox.setObjectName(u'displayFooterCheckBox') self.customModeLayout.addRow(self.displayFooterCheckBox) self.leftLayout.addWidget(self.customModeGroupBox) - self.customUIGroupBox = QtGui.QGroupBox(self.leftColumn) - self.customUIGroupBox.setObjectName(u'customUIGroupBox') - self.customUILayout = QtGui.QFormLayout(self.customUIGroupBox) - self.customUILayout.setObjectName(u'customUILayout') - self.confirmDeleteCheckBox = QtGui.QCheckBox(self.customUIGroupBox) - self.confirmDeleteCheckBox.setObjectName(u'confirmDeleteCheckBox') - self.customUILayout.addRow(self.confirmDeleteCheckBox) - self.leftLayout.addWidget(self.customUIGroupBox) self.leftLayout.addStretch() self.rightLayout.addStretch() QtCore.QObject.connect(self.displayFooterCheckBox, QtCore.SIGNAL(u'stateChanged(int)'), self.onDisplayFooterCheckBoxChanged) - QtCore.QObject.connect(self.confirmDeleteCheckBox, - QtCore.SIGNAL(u'stateChanged(int)'), - self.onConfirmDeleteCheckBoxChanged) def retranslateUi(self): self.customModeGroupBox.setTitle(translate('CustomPlugin.CustomTab', 'Custom Display')) self.displayFooterCheckBox.setText( translate('CustomPlugin.CustomTab', 'Display footer')) - self.customUIGroupBox.setTitle(translate('CustomPlugin.CustomTab', - 'UI Settings')) - self.confirmDeleteCheckBox.setText( - translate('CustomPlugin.CustomTab', 'Confirm delete')) def onDisplayFooterCheckBoxChanged(self, check_state): self.displayFooter = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: - self.displayFooter = True - - def onConfirmDeleteCheckBoxChanged(self, check_state): - self.confirmDelete = False - # we have a set value convert to True/False - if check_state == QtCore.Qt.Checked: - self.confirmDelete = True + self.displayFooter = True def load(self): self.displayFooter = QtCore.QSettings().value( self.settingsSection + u'/display footer', QtCore.QVariant(True)).toBool() self.displayFooterCheckBox.setChecked(self.displayFooter) - self.confirmDelete = QtCore.QSettings().value( - self.settingsSection + u'/confirm delete', - QtCore.QVariant(True)).toBool() - self.confirmDeleteCheckBox.setChecked(self.confirmDelete) def save(self): QtCore.QSettings().setValue(self.settingsSection + u'/display footer', QtCore.QVariant(self.displayFooter)) - QtCore.QSettings().setValue(self.settingsSection + u'/confirm delete', - QtCore.QVariant(self.confirmDelete)) diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index fec660360..41accef27 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -200,20 +200,16 @@ class CustomMediaItem(MediaManagerItem): Remove a custom item from the list and database """ if check_item_selected(self.listView, UiStrings().SelectDelete): - self.confirmDelete = QtCore.QSettings().value( - self.settingsSection + u'/confirm delete', - QtCore.QVariant(u'False')).toBool() items = self.listView.selectedIndexes() - if self.confirmDelete: - if QtGui.QMessageBox.question(self, - translate('CustomPlugin.MediaItem', 'Delete Custom(s)?'), - translate('CustomPlugin.MediaItem', - 'Are you sure you want to delete the %n selected custom(s)?', '', - QtCore.QCoreApplication.CodecForTr, len(items)), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok | - QtGui.QMessageBox.Cancel), - QtGui.QMessageBox.Ok) == QtGui.QMessageBox.Cancel: - return + if QtGui.QMessageBox.question(self, + translate('CustomPlugin.MediaItem', 'Delete Custom(s)?'), + translate('CustomPlugin.MediaItem', + 'Are you sure you want to delete the %n selected custom(s)?', '', + QtCore.QCoreApplication.CodecForTr, len(items)), + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok | + QtGui.QMessageBox.Cancel), + QtGui.QMessageBox.Ok) == QtGui.QMessageBox.Cancel: + return row_list = [item.row() for item in self.listView.selectedIndexes()] row_list.sort(reverse=True) id_list = [(item.data(QtCore.Qt.UserRole)).toInt()[0] From c3631659251d8b76febed523f7fd553cf375e5fc Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Tue, 28 Jun 2011 00:59:26 -0400 Subject: [PATCH 10/35] Removed some whitespace --- openlp/plugins/custom/lib/customtab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openlp/plugins/custom/lib/customtab.py b/openlp/plugins/custom/lib/customtab.py index b16746f8b..fb83fab81 100644 --- a/openlp/plugins/custom/lib/customtab.py +++ b/openlp/plugins/custom/lib/customtab.py @@ -63,7 +63,7 @@ class CustomTab(SettingsTab): self.displayFooter = False # we have a set value convert to True/False if check_state == QtCore.Qt.Checked: - self.displayFooter = True + self.displayFooter = True def load(self): self.displayFooter = QtCore.QSettings().value( From f14fa780812839e53e531643e854f58bee97e443 Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Tue, 28 Jun 2011 09:29:36 -0400 Subject: [PATCH 11/35] Modified delete message and dialog buttons --- openlp/plugins/custom/lib/mediaitem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 41accef27..8babfc245 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -204,11 +204,12 @@ class CustomMediaItem(MediaManagerItem): if QtGui.QMessageBox.question(self, translate('CustomPlugin.MediaItem', 'Delete Custom(s)?'), translate('CustomPlugin.MediaItem', - 'Are you sure you want to delete the %n selected custom(s)?', '', + 'Are you sure you want to delete the %n selected custom' \ + ' slides(s)?', '', QtCore.QCoreApplication.CodecForTr, len(items)), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok | - QtGui.QMessageBox.Cancel), - QtGui.QMessageBox.Ok) == QtGui.QMessageBox.Cancel: + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | + QtGui.QMessageBox.No), + QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No: return row_list = [item.row() for item in self.listView.selectedIndexes()] row_list.sort(reverse=True) From 75550b505bdb2bc88b4b3e8d12e129c0e6a46ba6 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 28 Jun 2011 17:35:50 +0100 Subject: [PATCH 12/35] Save Controller Toolbars Fixes: https://launchpad.net/bugs/765239 --- openlp/core/ui/mainwindow.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index e73ea5b80..ca84de411 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -1030,6 +1030,10 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): self.restoreGeometry( settings.value(u'main window geometry').toByteArray()) self.restoreState(settings.value(u'main window state').toByteArray()) + self.liveController.splitter.restoreState( + settings.value(u'live splitter geometry').toByteArray()) + self.previewController.splitter.restoreState( + settings.value(u'preview splitter geometry').toByteArray()) settings.endGroup() def saveSettings(self): @@ -1050,6 +1054,10 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): QtCore.QVariant(self.saveState())) settings.setValue(u'main window geometry', QtCore.QVariant(self.saveGeometry())) + settings.setValue(u'live splitter geometry', + QtCore.QVariant(self.liveController.splitter.saveState())) + settings.setValue(u'preview splitter geometry', + QtCore.QVariant(self.liveController.splitter.saveState())) settings.endGroup() def updateFileMenu(self): From a167f6ef86f4d21799d7dcd7b5a8abb04101e1bd Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Tue, 28 Jun 2011 17:40:17 +0100 Subject: [PATCH 13/35] Make progressbar smaller --- openlp/core/ui/mainwindow.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index ca84de411..53cd5a792 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -65,6 +65,12 @@ MEDIA_MANAGER_STYLE = """ } """ +PROGRESSBAR = """ + QProgressBar{ + height: 10px; + } +""" + class Ui_MainWindow(object): def setupUi(self, mainWindow): """ @@ -130,6 +136,7 @@ class Ui_MainWindow(object): self.statusBar.addPermanentWidget(self.loadProgressBar) self.loadProgressBar.hide() self.loadProgressBar.setValue(0) + self.loadProgressBar.setStyleSheet(PROGRESSBAR) self.defaultThemeLabel = QtGui.QLabel(self.statusBar) self.defaultThemeLabel.setObjectName(u'defaultThemeLabel') self.statusBar.addPermanentWidget(self.defaultThemeLabel) From 334bf4ab444559b5063a4baf54f2dd56bb4047fb Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Tue, 28 Jun 2011 15:05:05 -0400 Subject: [PATCH 14/35] Changed dialog heading. Changed Songs delete confirm to match Custom Slides --- openlp/plugins/custom/lib/mediaitem.py | 6 +++--- openlp/plugins/songs/lib/mediaitem.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 8babfc245..23094a98e 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -202,10 +202,10 @@ class CustomMediaItem(MediaManagerItem): if check_item_selected(self.listView, UiStrings().SelectDelete): items = self.listView.selectedIndexes() if QtGui.QMessageBox.question(self, - translate('CustomPlugin.MediaItem', 'Delete Custom(s)?'), + translate('CustomPlugin.MediaItem', 'Confirm Delete'), translate('CustomPlugin.MediaItem', - 'Are you sure you want to delete the %n selected custom' \ - ' slides(s)?', '', + 'Are you sure you want to delete the %n selected custom' + ' slides(s)?', '', QtCore.QCoreApplication.CodecForTr, len(items)), QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | QtGui.QMessageBox.No), diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index 61bdc32c0..badb5cde9 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -353,13 +353,13 @@ class SongMediaItem(MediaManagerItem): if check_item_selected(self.listView, UiStrings().SelectDelete): items = self.listView.selectedIndexes() if QtGui.QMessageBox.question(self, - translate('SongsPlugin.MediaItem', 'Delete Song(s)?'), + translate('SongsPlugin.MediaItem', 'Confirm Delete'), translate('SongsPlugin.MediaItem', 'Are you sure you want to delete the %n selected song(s)?', '', QtCore.QCoreApplication.CodecForTr, len(items)), - QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok | - QtGui.QMessageBox.Cancel), - QtGui.QMessageBox.Ok) == QtGui.QMessageBox.Cancel: + QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Yes | + QtGui.QMessageBox.No), + QtGui.QMessageBox.Yes) == QtGui.QMessageBox.No: return for item in items: item_id = (item.data(QtCore.Qt.UserRole)).toInt()[0] From 486a427794fa9e3c6a8f56dbc04d244190dac284 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Tue, 28 Jun 2011 21:24:29 +0200 Subject: [PATCH 15/35] Minor UI tweaks. --- openlp/core/ui/serviceitemeditdialog.py | 2 ++ openlp/core/ui/servicenoteform.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openlp/core/ui/serviceitemeditdialog.py b/openlp/core/ui/serviceitemeditdialog.py index a00feafc7..d821430b2 100644 --- a/openlp/core/ui/serviceitemeditdialog.py +++ b/openlp/core/ui/serviceitemeditdialog.py @@ -35,6 +35,8 @@ class Ui_ServiceItemEditDialog(object): def setupUi(self, serviceItemEditDialog): serviceItemEditDialog.setObjectName(u'serviceItemEditDialog') self.dialogLayout = QtGui.QGridLayout(serviceItemEditDialog) + self.dialogLayout.setContentsMargins(8, 8, 8, 8) + self.dialogLayout.setSpacing(8) self.dialogLayout.setObjectName(u'dialogLayout') self.listWidget = QtGui.QListWidget(serviceItemEditDialog) self.listWidget.setAlternatingRowColors(True) diff --git a/openlp/core/ui/servicenoteform.py b/openlp/core/ui/servicenoteform.py index d361c567e..3bc55e242 100644 --- a/openlp/core/ui/servicenoteform.py +++ b/openlp/core/ui/servicenoteform.py @@ -49,6 +49,8 @@ class ServiceNoteForm(QtGui.QDialog): def setupUi(self): self.setObjectName(u'serviceNoteEdit') self.dialogLayout = QtGui.QVBoxLayout(self) + self.dialogLayout.setContentsMargins(8, 8, 8, 8) + self.dialogLayout.setSpacing(8) self.dialogLayout.setObjectName(u'verticalLayout') self.textEdit = QtGui.QTextEdit(self) self.textEdit.setObjectName(u'textEdit') From da45d09e48df447d40c8e89a6fb3a2f587e44ca1 Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Tue, 28 Jun 2011 16:35:28 -0400 Subject: [PATCH 16/35] Moved delete dialog heading translate to ui.py --- openlp/core/lib/ui.py | 1 + openlp/plugins/custom/lib/mediaitem.py | 2 +- openlp/plugins/songs/lib/mediaitem.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openlp/core/lib/ui.py b/openlp/core/lib/ui.py index 5055bb619..558c8ba52 100644 --- a/openlp/core/lib/ui.py +++ b/openlp/core/lib/ui.py @@ -64,6 +64,7 @@ class UiStrings(object): self.Cancel = translate('OpenLP.Ui', 'Cancel') self.CCLINumberLabel = translate('OpenLP.Ui', 'CCLI number:') self.CreateService = translate('OpenLP.Ui', 'Create a new service.') + self.ConfirmDelete = translate('OpenLP.Ui', 'Confirm Delete') self.Continuous = translate('OpenLP.Ui', 'Continuous') self.Default = unicode(translate('OpenLP.Ui', 'Default')) self.Delete = translate('OpenLP.Ui', '&Delete') diff --git a/openlp/plugins/custom/lib/mediaitem.py b/openlp/plugins/custom/lib/mediaitem.py index 23094a98e..60f25ee0c 100644 --- a/openlp/plugins/custom/lib/mediaitem.py +++ b/openlp/plugins/custom/lib/mediaitem.py @@ -202,7 +202,7 @@ class CustomMediaItem(MediaManagerItem): if check_item_selected(self.listView, UiStrings().SelectDelete): items = self.listView.selectedIndexes() if QtGui.QMessageBox.question(self, - translate('CustomPlugin.MediaItem', 'Confirm Delete'), + UiStrings().ConfirmDelete, translate('CustomPlugin.MediaItem', 'Are you sure you want to delete the %n selected custom' ' slides(s)?', '', diff --git a/openlp/plugins/songs/lib/mediaitem.py b/openlp/plugins/songs/lib/mediaitem.py index badb5cde9..e0634d5cc 100644 --- a/openlp/plugins/songs/lib/mediaitem.py +++ b/openlp/plugins/songs/lib/mediaitem.py @@ -353,7 +353,7 @@ class SongMediaItem(MediaManagerItem): if check_item_selected(self.listView, UiStrings().SelectDelete): items = self.listView.selectedIndexes() if QtGui.QMessageBox.question(self, - translate('SongsPlugin.MediaItem', 'Confirm Delete'), + UiStrings().ConfirmDelete, translate('SongsPlugin.MediaItem', 'Are you sure you want to delete the %n selected song(s)?', '', QtCore.QCoreApplication.CodecForTr, len(items)), From 30df723358536ab462a050fbe51f3871039e88ca Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 29 Jun 2011 07:12:32 +0100 Subject: [PATCH 17/35] Fixes --- openlp/core/ui/mainwindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 53cd5a792..67839b52f 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -65,7 +65,7 @@ MEDIA_MANAGER_STYLE = """ } """ -PROGRESSBAR = """ +PROGRESSBAR_STYLE = """ QProgressBar{ height: 10px; } @@ -136,7 +136,7 @@ class Ui_MainWindow(object): self.statusBar.addPermanentWidget(self.loadProgressBar) self.loadProgressBar.hide() self.loadProgressBar.setValue(0) - self.loadProgressBar.setStyleSheet(PROGRESSBAR) + self.loadProgressBar.setStyleSheet(PROGRESSBAR_STYLE) self.defaultThemeLabel = QtGui.QLabel(self.statusBar) self.defaultThemeLabel.setObjectName(u'defaultThemeLabel') self.statusBar.addPermanentWidget(self.defaultThemeLabel) @@ -1064,7 +1064,7 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): settings.setValue(u'live splitter geometry', QtCore.QVariant(self.liveController.splitter.saveState())) settings.setValue(u'preview splitter geometry', - QtCore.QVariant(self.liveController.splitter.saveState())) + QtCore.QVariant(self.previewController.splitter.saveState())) settings.endGroup() def updateFileMenu(self): From 8bb96b97bfad8ac49e61ed961aedf95510b60c78 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 29 Jun 2011 10:04:14 +0200 Subject: [PATCH 18/35] - When creating/editing a custom slide give the title edit the focus (and not only when creating a new) - fixed long lines - simplification --- openlp/plugins/custom/forms/editcustomform.py | 6 ++---- openlp/plugins/songs/forms/editsongform.py | 6 ++++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openlp/plugins/custom/forms/editcustomform.py b/openlp/plugins/custom/forms/editcustomform.py index 32a2a3146..f4ac97fab 100644 --- a/openlp/plugins/custom/forms/editcustomform.py +++ b/openlp/plugins/custom/forms/editcustomform.py @@ -93,7 +93,6 @@ class EditCustomForm(QtGui.QDialog, Ui_CustomEditDialog): self.titleEdit.setText(u'') self.creditEdit.setText(u'') self.themeComboBox.setCurrentIndex(0) - self.titleEdit.setFocus(QtCore.Qt.OtherFocusReason) else: self.customSlide = self.manager.get_object(CustomSlide, id) self.titleEdit.setText(self.customSlide.title) @@ -104,10 +103,9 @@ class EditCustomForm(QtGui.QDialog, Ui_CustomEditDialog): self.slideListView.addItem(slide[1]) theme = self.customSlide.theme_name find_and_set_in_combo_box(self.themeComboBox, theme) + self.titleEdit.setFocus(QtCore.Qt.OtherFocusReason) # If not preview hide the preview button. - self.previewButton.setVisible(False) - if preview: - self.previewButton.setVisible(True) + self.previewButton.setVisible(preview) def reject(self): Receiver.send_message(u'custom_edit_clear') diff --git a/openlp/plugins/songs/forms/editsongform.py b/openlp/plugins/songs/forms/editsongform.py index a07ceb6c9..4eab75703 100644 --- a/openlp/plugins/songs/forms/editsongform.py +++ b/openlp/plugins/songs/forms/editsongform.py @@ -209,9 +209,11 @@ class EditSongForm(QtGui.QDialog, Ui_EditSongDialog): self.alternativeEdit.setText(u'') if self.song.song_book_id != 0: book_name = self.manager.get_object(Book, self.song.song_book_id) - find_and_set_in_combo_box(self.songBookComboBox, unicode(book_name.name)) + find_and_set_in_combo_box( + self.songBookComboBox, unicode(book_name.name)) if self.song.theme_name: - find_and_set_in_combo_box(self.themeComboBox, unicode(self.song.theme_name)) + find_and_set_in_combo_box( + self.themeComboBox, unicode(self.song.theme_name)) if self.song.copyright: self.copyrightEdit.setText(self.song.copyright) else: From 6e3343c6060606acef6f0dac92fd72199f1c2342 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 29 Jun 2011 11:08:53 +0200 Subject: [PATCH 19/35] fixed theme copy --- openlp/core/ui/thememanager.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index d84fe7e1f..ff852c591 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -298,11 +298,10 @@ class ThemeManager(QtGui.QWidget): Copies an existing theme to a new name """ item = self.themeListWidget.currentItem() - oldThemeName = unicode( - translate('OpenLP.ThemeManager', 'Copy of %s', - 'Copy of ')) % unicode( - item.data(QtCore.Qt.UserRole).toString()) - self.fileRenameForm.fileNameEdit.setText(oldThemeName) + oldThemeName = unicode(item.data(QtCore.Qt.UserRole).toString()) + self.fileRenameForm.fileNameEdit.setText( + unicode(translate('OpenLP.ThemeManager', + 'Copy of %s','Copy of ')) % oldThemeName) if self.fileRenameForm.exec_(True): newThemeName = unicode(self.fileRenameForm.fileNameEdit.text()) if self.checkIfThemeExists(newThemeName): From 6980299697f56531ac18b26c7cb75877f2f9c218 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 29 Jun 2011 11:23:42 +0200 Subject: [PATCH 20/35] use help function --- openlp/core/ui/servicemanager.py | 2 +- openlp/core/ui/thememanager.py | 59 ++++++++++++++------------------ 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 8fc796ea4..d35b0cb33 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -688,7 +688,7 @@ class ServiceManager(QtGui.QWidget): QtGui.QAction, serviceItem[u'service_item'].theme) if themeAction is not None: themeAction.setChecked(True) - action = self.menu.exec_(self.serviceManagerList.mapToGlobal(point)) + self.menu.exec_(self.serviceManagerList.mapToGlobal(point)) def onServiceItemNoteForm(self): item = self.findServiceItem()[0] diff --git a/openlp/core/ui/thememanager.py b/openlp/core/ui/thememanager.py index d84fe7e1f..4582e55c0 100644 --- a/openlp/core/ui/thememanager.py +++ b/openlp/core/ui/thememanager.py @@ -39,7 +39,8 @@ from openlp.core.lib import OpenLPToolbar, get_text_file_string, build_icon, \ check_directory_exists from openlp.core.lib.theme import ThemeXML, BackgroundType, VerticalType, \ BackgroundGradientType -from openlp.core.lib.ui import UiStrings, critical_error_message_box +from openlp.core.lib.ui import UiStrings, critical_error_message_box, \ + context_menu_action, context_menu_separator from openlp.core.theme import Theme from openlp.core.ui import FileRenameForm, ThemeForm from openlp.core.utils import AppLocation, delete_file, file_is_unicode, \ @@ -104,25 +105,29 @@ class ThemeManager(QtGui.QWidget): self.contextMenu) # build the context menu self.menu = QtGui.QMenu() - self.editAction = self.menu.addAction( - translate('OpenLP.ThemeManager', '&Edit Theme')) - self.editAction.setIcon(build_icon(u':/themes/theme_edit.png')) - self.copyAction = self.menu.addAction( - translate('OpenLP.ThemeManager', '&Copy Theme')) - self.copyAction.setIcon(build_icon(u':/themes/theme_edit.png')) - self.renameAction = self.menu.addAction( - translate('OpenLP.ThemeManager', '&Rename Theme')) - self.renameAction.setIcon(build_icon(u':/themes/theme_edit.png')) - self.deleteAction = self.menu.addAction( - translate('OpenLP.ThemeManager', '&Delete Theme')) - self.deleteAction.setIcon(build_icon(u':/general/general_delete.png')) - self.separator = self.menu.addSeparator() - self.globalAction = self.menu.addAction( - translate('OpenLP.ThemeManager', 'Set As &Global Default')) - self.globalAction.setIcon(build_icon(u':/general/general_export.png')) - self.exportAction = self.menu.addAction( - translate('OpenLP.ThemeManager', '&Export Theme')) - self.exportAction.setIcon(build_icon(u':/general/general_export.png')) + self.editAction = context_menu_action( + self.menu, u':/themes/theme_edit.png', + translate('OpenLP.ThemeManager', '&Edit Theme'), self.onEditTheme) + self.copyAction = context_menu_action( + self.menu, u':/themes/theme_edit.png', + translate('OpenLP.ThemeManager', '&Copy Theme'), self.onCopyTheme) + self.renameAction = context_menu_action( + self.menu, u':/themes/theme_edit.png', + translate('OpenLP.ThemeManager', '&Rename Theme'), + self.onRenameTheme) + self.deleteAction = context_menu_action( + self.menu, u':/general/general_delete.png', + translate('OpenLP.ThemeManager', '&Delete Theme'), + self.onDeleteTheme) + context_menu_separator(self.menu) + self.globalAction = context_menu_action( + self.menu, u':/general/general_export.png', + translate('OpenLP.ThemeManager', 'Set As &Global Default'), + self.changeGlobalFromScreen) + self.exportAction = context_menu_action( + self.menu, u':/general/general_export.png', + translate('OpenLP.ThemeManager', '&Export Theme'), + self.onExportTheme) # Signals QtCore.QObject.connect(self.themeListWidget, QtCore.SIGNAL(u'doubleClicked(QModelIndex)'), @@ -198,19 +203,7 @@ class ThemeManager(QtGui.QWidget): self.deleteAction.setVisible(True) self.renameAction.setVisible(True) self.globalAction.setVisible(True) - action = self.menu.exec_(self.themeListWidget.mapToGlobal(point)) - if action == self.editAction: - self.onEditTheme() - if action == self.copyAction: - self.onCopyTheme() - if action == self.renameAction: - self.onRenameTheme() - if action == self.deleteAction: - self.onDeleteTheme() - if action == self.globalAction: - self.changeGlobalFromScreen() - if action == self.exportAction: - self.onExportTheme() + self.menu.exec_(self.themeListWidget.mapToGlobal(point)) def changeGlobalFromTab(self, themeName): """ From c2fb73dde6feec58a8c00ce1ad87702425686162 Mon Sep 17 00:00:00 2001 From: Andreas Preikschat Date: Wed, 29 Jun 2011 16:28:10 +0200 Subject: [PATCH 21/35] do not regenerate all service items when collapsing/expanding all items --- openlp/core/ui/servicemanager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/servicemanager.py b/openlp/core/ui/servicemanager.py index 8fc796ea4..6424dfb2c 100644 --- a/openlp/core/ui/servicemanager.py +++ b/openlp/core/ui/servicemanager.py @@ -832,7 +832,7 @@ class ServiceManager(QtGui.QWidget): """ for item in self.serviceItems: item[u'expanded'] = False - self.regenerateServiceItems() + self.serviceManagerList.collapseAll() def collapsed(self, item): """ @@ -848,7 +848,7 @@ class ServiceManager(QtGui.QWidget): """ for item in self.serviceItems: item[u'expanded'] = True - self.regenerateServiceItems() + self.serviceManagerList.expandAll() def expanded(self, item): """ @@ -856,7 +856,7 @@ class ServiceManager(QtGui.QWidget): correct state. """ pos = item.data(0, QtCore.Qt.UserRole).toInt()[0] - self.serviceItems[pos -1 ][u'expanded'] = True + self.serviceItems[pos - 1][u'expanded'] = True def onServiceTop(self): """ From 0612a939641b9dfec3067dccbb72bc021db8a773 Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Wed, 29 Jun 2011 10:42:14 -0400 Subject: [PATCH 22/35] Changed helpfile check to Windows condition check. Modified build script to create OpenLP.chm --- openlp/core/ui/mainwindow.py | 11 +++++------ scripts/windows-builder.py | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 4148b40eb..2114e17a9 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -269,10 +269,9 @@ class Ui_MainWindow(object): self.helpAboutItem = shortcut_action(mainWindow, u'helpAboutItem', [QtGui.QKeySequence(u'Ctrl+F1')], self.onHelpAboutItemClicked, u':/system/system_about.png', category=UiStrings().Help) - self.localHelpFile = os.path.join( - AppLocation.get_directory(AppLocation.AppDir), 'Openlp.chm') - self.haveHelpFile = os.path.isfile(self.localHelpFile) - if self.haveHelpFile: + if sys.platform[:3] == u'win': + self.localHelpFile = os.path.join( + AppLocation.get_directory(AppLocation.AppDir), 'OpenLP.chm') self.helpLocalHelpItem = shortcut_action( mainWindow, u'helpLocalHelpItem', [QtGui.QKeySequence(u'F1')], self.onHelpLocalHelpClicked, u':/system/system_about.png', @@ -321,7 +320,7 @@ class Ui_MainWindow(object): add_actions(self.toolsMenu, (self.toolsOpenDataFolder, None)) add_actions(self.toolsMenu, [self.updateThemeImages]) add_actions(self.helpMenu, (self.helpDocumentationItem, None)) - if self.haveHelpFile: + if sys.platform[:3] == u'win': add_actions(self.helpMenu, (self.helpLocalHelpItem, self.helpOnlineHelpItem, None, self.helpWebSiteItem, self.helpAboutItem)) @@ -443,7 +442,7 @@ class Ui_MainWindow(object): self.helpAboutItem.setText(translate('OpenLP.MainWindow', '&About')) self.helpAboutItem.setStatusTip( translate('OpenLP.MainWindow', 'More information about OpenLP')) - if self.haveHelpFile: + if sys.platform[:3] == u'win': self.helpLocalHelpItem.setText( translate('OpenLP.MainWindow', '&Help')) self.helpOnlineHelpItem.setText( diff --git a/scripts/windows-builder.py b/scripts/windows-builder.py index 854c927c9..4ab31f893 100644 --- a/scripts/windows-builder.py +++ b/scripts/windows-builder.py @@ -242,10 +242,10 @@ def copy_windows_files(): os.path.join(dist_path, u'LICENSE.txt')) copy(os.path.join(winres_path, u'psvince.dll'), os.path.join(dist_path, u'psvince.dll')) - if os.path.isfile(os.path.join(helpfile_path, u'Openlp.chm')): + if os.path.isfile(os.path.join(helpfile_path, u'OpenLP.chm')): print u' Windows help file found' - copy(os.path.join(helpfile_path, u'Openlp.chm'), - os.path.join(dist_path, u'Openlp.chm')) + copy(os.path.join(helpfile_path, u'OpenLP.chm'), + os.path.join(dist_path, u'OpenLP.chm')) else: print u' WARNING ---- Windows help file not found ---- WARNING' From 143e69728173e6596e06b3ea1c75030f98a85863 Mon Sep 17 00:00:00 2001 From: Tim Bentley Date: Wed, 29 Jun 2011 17:11:19 +0100 Subject: [PATCH 23/35] Add middle splitter --- openlp/core/ui/mainwindow.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 67839b52f..a34cb7b96 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -1041,6 +1041,9 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): settings.value(u'live splitter geometry').toByteArray()) self.previewController.splitter.restoreState( settings.value(u'preview splitter geometry').toByteArray()) + self.controlSplitter.restoreState( + settings.value(u'mainwindow splitter geometry').toByteArray()) + settings.endGroup() def saveSettings(self): @@ -1065,6 +1068,8 @@ class MainWindow(QtGui.QMainWindow, Ui_MainWindow): QtCore.QVariant(self.liveController.splitter.saveState())) settings.setValue(u'preview splitter geometry', QtCore.QVariant(self.previewController.splitter.saveState())) + settings.setValue(u'mainwindow splitter geometry', + QtCore.QVariant(self.controlSplitter.saveState())) settings.endGroup() def updateFileMenu(self): From 97a76369bf3b31d50ea87a5890df706985089813 Mon Sep 17 00:00:00 2001 From: Stevan Pettit Date: Wed, 29 Jun 2011 13:30:53 -0400 Subject: [PATCH 24/35] Changed Windows test to os.name --- openlp/core/ui/mainwindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openlp/core/ui/mainwindow.py b/openlp/core/ui/mainwindow.py index 2114e17a9..cde13f18a 100644 --- a/openlp/core/ui/mainwindow.py +++ b/openlp/core/ui/mainwindow.py @@ -269,7 +269,7 @@ class Ui_MainWindow(object): self.helpAboutItem = shortcut_action(mainWindow, u'helpAboutItem', [QtGui.QKeySequence(u'Ctrl+F1')], self.onHelpAboutItemClicked, u':/system/system_about.png', category=UiStrings().Help) - if sys.platform[:3] == u'win': + if os.name == u'nt': self.localHelpFile = os.path.join( AppLocation.get_directory(AppLocation.AppDir), 'OpenLP.chm') self.helpLocalHelpItem = shortcut_action( @@ -320,7 +320,7 @@ class Ui_MainWindow(object): add_actions(self.toolsMenu, (self.toolsOpenDataFolder, None)) add_actions(self.toolsMenu, [self.updateThemeImages]) add_actions(self.helpMenu, (self.helpDocumentationItem, None)) - if sys.platform[:3] == u'win': + if os.name == u'nt': add_actions(self.helpMenu, (self.helpLocalHelpItem, self.helpOnlineHelpItem, None, self.helpWebSiteItem, self.helpAboutItem)) @@ -442,7 +442,7 @@ class Ui_MainWindow(object): self.helpAboutItem.setText(translate('OpenLP.MainWindow', '&About')) self.helpAboutItem.setStatusTip( translate('OpenLP.MainWindow', 'More information about OpenLP')) - if sys.platform[:3] == u'win': + if os.name == u'nt': self.helpLocalHelpItem.setText( translate('OpenLP.MainWindow', '&Help')) self.helpOnlineHelpItem.setText( From bc808ade93762cf5d3a1a4501e0f3378a1cee4c3 Mon Sep 17 00:00:00 2001 From: Benny Date: Sat, 2 Jul 2011 00:45:27 +0200 Subject: [PATCH 25/35] EasyWorshipSongImport: use tag from previous slide for slides without tag, fix regex for notes --- openlp/plugins/songs/lib/ewimport.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index c207a07d2..95533ba94 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -261,6 +261,7 @@ class EasyWorshipSongImport(SongImport): # Format the lyrics words = strip_rtf(words, self.encoding) # TODO: convert rtf instead of stripping? p = re.compile(r'\n *?\n[\n ]*') # at least two newlines, with zero or more space characters between them + verse_type = VerseType.Tags[VerseType.Verse] # TODO!!!: use previous verse type.... for verse in p.split(words): #for verse in words.split(u'\n\n'): # ew tags: verse, chorus, pre-chorus, bridge, tag, intro, ending, slide @@ -268,13 +269,11 @@ class EasyWorshipSongImport(SongImport): if len(verse) == 0: continue verse_split = verse.split(u'\n', 1) - verse_type = VerseType.Tags[VerseType.Verse] first_line_is_tag = False for type in VerseType.Names+['tag', 'slide']: # doesnt cover tag, slide type = type.lower() ew_tag = verse_split[0].strip().lower() if ew_tag.startswith(type): - #print ew_tag verse_type = type[0] if type == 'tag' or type == 'slide': verse_type = VerseType.Tags[VerseType.Other] @@ -286,7 +285,7 @@ class EasyWorshipSongImport(SongImport): number = m.group() verse_type +=number - p = re.compile(r'\(.*\)') + p = re.compile(r'\(.*?\)') m = re.search(p, ew_tag) if m: self.comments += ew_tag+'\n' From ffab60d29e9cf2c8ec969a599cfaf3d5d7a5425f Mon Sep 17 00:00:00 2001 From: Martin Zibricky Date: Mon, 4 Jul 2011 11:32:13 +0200 Subject: [PATCH 27/35] Updat windows icon to contain higher resolution images (64x64, 128x128) --- resources/images/OpenLP.ico | Bin 15086 -> 99678 bytes resources/images/README.txt | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 resources/images/README.txt diff --git a/resources/images/OpenLP.ico b/resources/images/OpenLP.ico index b275542c3809b3c3fb91facd13bc8ce75df076e0..c4aa7c41e76f50f6000a58b400c427e72cb8343e 100644 GIT binary patch literal 99678 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 bzPmg~eS(UIt9P)yx_TA6`B?p}M*sXDRalY7 delta 1171 zcmb_bUr3Wt6u;lS`?mRy=H~pfx%s(TxJc0DrjyCk!V;lU6p9jZX{FG^f&yi-2_+a5 zj6;-A(u-k4Pb-j~^tB$s-m49I=pjO*zz6eo&fN-o=&cKv-}n2@Ilu4x&b{A<+vcy= z2N=_sU^W|}!LFDX3p2(Hqrg}2{Sv-wX(@6)V|`A>nlJ}LSW$S7z`9{0>K2!eM>7in zF*Oj&B%GPoZDJ%808NXCODCe)E^Uqrqm0A4h=};H2>cZNiuaoW@sUU~$*8rtPs*G{ z$~7TnT9cBofA2KLD2H;XW*cj%KLd9+Zt_JdZG#>?$hihqy;F?&bfjHGZ|c3`lu?4T zm1`R8=tCk>SBlgxeB~Ub&1Z$ouVxY+;lw2Yn7i7>#@nVFl`^@zH-QP9E4r_F!z?>V=z1K7}Ff&0Nv~|mfB$~ zw#`@r>{hwqsNQHwqYDT-4WF6^`NcVrRXiPb0rwxPe(b(iA0vTi}FCMS?LEB}612Khxb z9=k<@ylb(TAAl$B2dD-;Z(Ks|tix#ia!rLv!^ZwzXT0WYohQVFeCWsyHbg3moi4Sk zsA0Jw-uhT9oj#0{6-FrsPjFp~hxbE1_y`+6Af8SR!t#-Oyw-|?J0#X-;xO5Ehc~J@ zif2>9&>XtOJ*61!6yxoF*b2>Zy9H}|mEOy60*ChzDyv6fzWqEO@xoSY3Ql%>*DRTY zgZd)>slvsQl8z&WAn`cxcHuo^49^Mg9L}6DjRpx3SW2a<0z5%HKUJm*H;JQwLKMgm z$4=JvA_}I7Yclowp@4MHDWLv^|BGpo+oP|z2Je%Wa{99 Date: Mon, 4 Jul 2011 11:52:01 +0200 Subject: [PATCH 28/35] Added empty file to run unit tests --- testing/run.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 testing/run.py diff --git a/testing/run.py b/testing/run.py new file mode 100644 index 000000000..e69de29bb From 5b95657fbb027681c4b02490ddaafafda0e8a46d Mon Sep 17 00:00:00 2001 From: Martin Zibricky Date: Mon, 4 Jul 2011 13:27:20 +0200 Subject: [PATCH 29/35] Update windows build script to work with pyinstaller svn version (1.6dev) --- resources/windows/OpenLP.spec | 14 -------- scripts/windows-builder.py | 63 ++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 44 deletions(-) delete mode 100644 resources/windows/OpenLP.spec diff --git a/resources/windows/OpenLP.spec b/resources/windows/OpenLP.spec deleted file mode 100644 index 47a1952f3..000000000 --- a/resources/windows/OpenLP.spec +++ /dev/null @@ -1,14 +0,0 @@ -# -*- mode: python -*- -a = Analysis([ - os.path.join(HOMEPATH, 'support\\_mountzlib.py'), - os.path.join(HOMEPATH, 'support\\useUnicode.py'), - os.path.abspath('openlp.pyw')], - pathex=[os.path.abspath('.')]) -pyz = PYZ(a.pure) -exe = EXE(pyz, a.scripts, exclude_binaries=1, - name=os.path.abspath(os.path.join('build', 'pyi.win32', 'OpenLP', - 'OpenLP.exe')), - debug=False, strip=False, upx=True, console=False, - icon=os.path.abspath(os.path.join('resources', 'images', 'OpenLP.ico'))) -coll = COLLECT(exe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, - name=os.path.abspath(os.path.join('dist', 'OpenLP'))) diff --git a/scripts/windows-builder.py b/scripts/windows-builder.py index 4ab31f893..dc08a3417 100644 --- a/scripts/windows-builder.py +++ b/scripts/windows-builder.py @@ -32,8 +32,7 @@ Windows Build Script This script is used to build the Windows binary and the accompanying installer. For this script to work out of the box, it depends on a number of things: -Python 2.6 - This build script only works with Python 2.6. +Python 2.6/2.7 PyQt4 You should already have this installed, OpenLP doesn't work without it. The @@ -49,7 +48,7 @@ Inno Setup 5 UPX This is used to compress DLLs and EXEs so that they take up less space, but - still function exactly the same. To install UPS, download it from + still function exactly the same. To install UPX, download it from http://upx.sourceforge.net/, extract it into C:\%PROGRAMFILES%\UPX, and then add that directory to your PATH environment variable. @@ -61,7 +60,7 @@ HTML Help Workshop This is used to create the help file PyInstaller - PyInstaller should be a checkout of revision 844 of trunk, and in a + PyInstaller should be a checkout of revision 1470 of trunk, and in a directory called, "pyinstaller" on the same level as OpenLP's Bazaar shared repository directory. The revision is very important as there is currently a major regression in HEAD. @@ -73,13 +72,8 @@ PyInstaller http://svn.pyinstaller.org/trunk Then you need to copy the two hook-*.py files from the "pyinstaller" - subdirectory in OpenLP's "resources" directory into PyInstaller's "hooks" - directory. - - Once you've done that, open a command prompt (DOS shell), navigate to the - PyInstaller directory and run:: - - C:\Projects\pyinstaller>python Configure.py + subdirectory in OpenLP's "resources" directory into PyInstaller's + "PyInstaller/hooks" directory. Bazaar You need the command line "bzr" client installed. @@ -137,9 +131,11 @@ site_packages = os.path.join(os.path.split(python_exe)[0], u'Lib', # Files and executables pyi_build = os.path.abspath(os.path.join(branch_path, u'..', u'..', - u'pyinstaller', u'Build.py')) + u'pyinstaller', u'pyinstaller.py')) +openlp_main_script = os.path.abspath(os.path.join(branch_path, 'openlp.pyw')) lrelease_exe = os.path.join(site_packages, u'PyQt4', u'bin', u'lrelease.exe') i18n_utils = os.path.join(script_path, u'translation_utils.py') +win32_icon = os.path.join(branch_path, u'resources', u'images', 'OpenLP.ico') # Paths source_path = os.path.join(branch_path, u'openlp') @@ -148,9 +144,9 @@ manual_build_path = os.path.join(manual_path, u'build') helpfile_path = os.path.join(manual_build_path, u'htmlhelp') i18n_path = os.path.join(branch_path, u'resources', u'i18n') winres_path = os.path.join(branch_path, u'resources', u'windows') -build_path = os.path.join(branch_path, u'build', u'pyi.win32', u'OpenLP') -dist_path = os.path.join(branch_path, u'dist', u'OpenLP') -enchant_path = os.path.join(site_packages, u'enchant') +build_path = os.path.join(branch_path, u'build') +dist_path = os.path.join(build_path, u'dist', u'OpenLP') +#enchant_path = os.path.join(site_packages, u'enchant') pptviewlib_path = os.path.join(source_path, u'plugins', u'presentations', u'lib', u'pptviewlib') @@ -174,8 +170,15 @@ def update_code(): def run_pyinstaller(): print u'Running PyInstaller...' os.chdir(branch_path) - pyinstaller = Popen((python_exe, pyi_build, u'-y', u'-o', build_path, - os.path.join(winres_path, u'OpenLP.spec')), stdout=PIPE) + pyinstaller = Popen((python_exe, pyi_build, + u'--noconfirm', + u'--windowed', + u'-o', build_path, + u'-i', win32_icon, + u'-p', branch_path, + u'-n', 'OpenLP', + openlp_main_script), + stdout=PIPE) output, error = pyinstaller.communicate() code = pyinstaller.wait() if code != 0: @@ -208,18 +211,18 @@ def write_version_file(): f.write(versionstring) f.close() -def copy_enchant(): - print u'Copying enchant/pyenchant...' - source = enchant_path - dest = os.path.join(dist_path, u'enchant') - for root, dirs, files in os.walk(source): - for filename in files: - if not filename.endswith(u'.pyc') and not filename.endswith(u'.pyo'): - dest_path = os.path.join(dest, root[len(source) + 1:]) - if not os.path.exists(dest_path): - os.makedirs(dest_path) - copy(os.path.join(root, filename), - os.path.join(dest_path, filename)) +#def copy_enchant(): + #print u'Copying enchant/pyenchant...' + #source = enchant_path + #dest = os.path.join(dist_path, u'enchant') + #for root, dirs, files in os.walk(source): + #for filename in files: + #if not filename.endswith(u'.pyc') and not filename.endswith(u'.pyo'): + #dest_path = os.path.join(dest, root[len(source) + 1:]) + #if not os.path.exists(dest_path): + #os.makedirs(dest_path) + #copy(os.path.join(root, filename), + #os.path.join(dest_path, filename)) def copy_plugins(): print u'Copying plugins...' @@ -353,7 +356,7 @@ def main(): build_pptviewlib() run_pyinstaller() write_version_file() - copy_enchant() + #copy_enchant() copy_plugins() if os.path.exists(manual_path): run_sphinx() From a039a50005444b6f479319bbf0128e295d94d66f Mon Sep 17 00:00:00 2001 From: Martin Zibricky Date: Mon, 4 Jul 2011 14:04:50 +0200 Subject: [PATCH 30/35] Update formating and remove dead enchant code --- scripts/windows-builder.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/scripts/windows-builder.py b/scripts/windows-builder.py index dc08a3417..d6f4d42e3 100644 --- a/scripts/windows-builder.py +++ b/scripts/windows-builder.py @@ -146,7 +146,6 @@ i18n_path = os.path.join(branch_path, u'resources', u'i18n') winres_path = os.path.join(branch_path, u'resources', u'windows') build_path = os.path.join(branch_path, u'build') dist_path = os.path.join(build_path, u'dist', u'OpenLP') -#enchant_path = os.path.join(site_packages, u'enchant') pptviewlib_path = os.path.join(source_path, u'plugins', u'presentations', u'lib', u'pptviewlib') @@ -171,14 +170,14 @@ def run_pyinstaller(): print u'Running PyInstaller...' os.chdir(branch_path) pyinstaller = Popen((python_exe, pyi_build, - u'--noconfirm', - u'--windowed', - u'-o', build_path, - u'-i', win32_icon, - u'-p', branch_path, - u'-n', 'OpenLP', - openlp_main_script), - stdout=PIPE) + u'--noconfirm', + u'--windowed', + u'-o', build_path, + u'-i', win32_icon, + u'-p', branch_path, + u'-n', 'OpenLP', + openlp_main_script), + stdout=PIPE) output, error = pyinstaller.communicate() code = pyinstaller.wait() if code != 0: @@ -211,19 +210,6 @@ def write_version_file(): f.write(versionstring) f.close() -#def copy_enchant(): - #print u'Copying enchant/pyenchant...' - #source = enchant_path - #dest = os.path.join(dist_path, u'enchant') - #for root, dirs, files in os.walk(source): - #for filename in files: - #if not filename.endswith(u'.pyc') and not filename.endswith(u'.pyo'): - #dest_path = os.path.join(dest, root[len(source) + 1:]) - #if not os.path.exists(dest_path): - #os.makedirs(dest_path) - #copy(os.path.join(root, filename), - #os.path.join(dest_path, filename)) - def copy_plugins(): print u'Copying plugins...' source = os.path.join(source_path, u'plugins') @@ -356,7 +342,6 @@ def main(): build_pptviewlib() run_pyinstaller() write_version_file() - #copy_enchant() copy_plugins() if os.path.exists(manual_path): run_sphinx() From 36b9c2e386e9c69342cf1853240252dd5f45f77e Mon Sep 17 00:00:00 2001 From: Martin Zibricky Date: Mon, 4 Jul 2011 15:26:39 +0200 Subject: [PATCH 31/35] Remove folder for tests. --- testing/run.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 testing/run.py diff --git a/testing/run.py b/testing/run.py deleted file mode 100644 index e69de29bb..000000000 From 4bf45ad2defe7018062c65abdc1b586e7d241c2e Mon Sep 17 00:00:00 2001 From: Benny Date: Mon, 4 Jul 2011 22:51:43 +0200 Subject: [PATCH 32/35] ewimport: workaround for RTF stripping bug --- openlp/plugins/songs/lib/ewimport.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index 95533ba94..a50c97f47 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -44,6 +44,14 @@ def strip_rtf(blob, encoding): control = False clear_text = [] control_word = [] + + # workaround for \tx bug: remove one pair of curly braces if \tx is encountered + p = re.compile(r'\{\\tx[^}]*\}') + m = p.search(blob) + if m: + # start and end indices of match are curly braces - filter them out + blob = ''.join([blob[i] for i in xrange(len(blob)) if i != m.start() and i !=m.end()]) + for c in blob: if control: # for delimiters, set control to False From 3c0c9c5b781d80899f1b73543a085c779bfc9ac8 Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 5 Jul 2011 00:55:57 +0200 Subject: [PATCH 33/35] EasyWorship importer: some work to create more reasonable verse numbers if EW tags are missing or without numbers --- openlp/plugins/songs/lib/ewimport.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index a50c97f47..18b87f9c0 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -267,18 +267,18 @@ class EasyWorshipSongImport(SongImport): self.add_author(author_name.strip()) if words: # Format the lyrics - words = strip_rtf(words, self.encoding) # TODO: convert rtf instead of stripping? - p = re.compile(r'\n *?\n[\n ]*') # at least two newlines, with zero or more space characters between them - verse_type = VerseType.Tags[VerseType.Verse] # TODO!!!: use previous verse type.... + words = strip_rtf(words, self.encoding) # TODO: convert rtf to display tags? + # regex: at least two newlines, with zero or more space characters between them + p = re.compile(r'\n *?\n[\n ]*') + verse_type = VerseType.Tags[VerseType.Verse] for verse in p.split(words): - #for verse in words.split(u'\n\n'): - # ew tags: verse, chorus, pre-chorus, bridge, tag, intro, ending, slide verse = verse.strip() if len(verse) == 0: continue verse_split = verse.split(u'\n', 1) first_line_is_tag = False - for type in VerseType.Names+['tag', 'slide']: # doesnt cover tag, slide + # ew tags: verse, chorus, pre-chorus, bridge, tag, intro, ending, slide + for type in VerseType.Names+['tag', 'slide']: type = type.lower() ew_tag = verse_split[0].strip().lower() if ew_tag.startswith(type): @@ -286,20 +286,24 @@ class EasyWorshipSongImport(SongImport): if type == 'tag' or type == 'slide': verse_type = VerseType.Tags[VerseType.Other] first_line_is_tag = True + number_found = False if len(ew_tag) > len(type): # tag is followed by number and/or note p = re.compile(r'[0-9]+') m = re.search(p, ew_tag) if m: number = m.group() verse_type +=number + number_found = True p = re.compile(r'\(.*?\)') m = re.search(p, ew_tag) if m: self.comments += ew_tag+'\n' + if not number_found: + verse_type += '1' break self.add_verse( - verse_split[-1].strip() if first_line_is_tag else verse, # TODO: hacky: -1 + verse_split[-1].strip() if first_line_is_tag else verse, verse_type) if len(self.comments) > 5: self.comments += unicode(translate('SongsPlugin.EasyWorshipSongImport', From 6436b05240635a5b5c3fd4675e32fad3bf46c7d4 Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 5 Jul 2011 12:50:55 +0200 Subject: [PATCH 34/35] changes from review (cosmetic & regex performance) --- openlp/plugins/songs/lib/ewimport.py | 54 +++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index 18b87f9c0..732c6e4f0 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -39,18 +39,25 @@ from openlp.plugins.songs.lib import VerseType from openlp.plugins.songs.lib import retrieve_windows_encoding from songimport import SongImport +RTF_STRIPPING_REGEX = re.compile(r'\{\\tx[^}]*\}') +# regex: at least two newlines, can have spaces between them +SLIDE_BREAK_REGEX = re.compile(r'\n *?\n[\n ]*') +NUMBER_REGEX = re.compile(r'[0-9]+') +NOTE_REGEX = re.compile(r'\(.*?\)') + def strip_rtf(blob, encoding): depth = 0 control = False clear_text = [] control_word = [] - # workaround for \tx bug: remove one pair of curly braces if \tx is encountered - p = re.compile(r'\{\\tx[^}]*\}') - m = p.search(blob) - if m: + # workaround for \tx bug: remove one pair of curly braces + # if \tx is encountered + match = RTF_STRIPPING_REGEX.search(blob) + if match: # start and end indices of match are curly braces - filter them out - blob = ''.join([blob[i] for i in xrange(len(blob)) if i != m.start() and i !=m.end()]) + blob = ''.join([blob[i] for i in xrange(len(blob)) + if i != match.start() and i !=match.end()]) for c in blob: if control: @@ -267,17 +274,16 @@ class EasyWorshipSongImport(SongImport): self.add_author(author_name.strip()) if words: # Format the lyrics - words = strip_rtf(words, self.encoding) # TODO: convert rtf to display tags? - # regex: at least two newlines, with zero or more space characters between them - p = re.compile(r'\n *?\n[\n ]*') + words = strip_rtf(words, self.encoding) verse_type = VerseType.Tags[VerseType.Verse] - for verse in p.split(words): + for verse in SLIDE_BREAK_REGEX.split(words): verse = verse.strip() if len(verse) == 0: continue - verse_split = verse.split(u'\n', 1) + verse_split = verse.split(u'\n', 1) first_line_is_tag = False - # ew tags: verse, chorus, pre-chorus, bridge, tag, intro, ending, slide + # EW tags: verse, chorus, pre-chorus, bridge, tag, + # intro, ending, slide for type in VerseType.Names+['tag', 'slide']: type = type.lower() ew_tag = verse_split[0].strip().lower() @@ -287,27 +293,27 @@ class EasyWorshipSongImport(SongImport): verse_type = VerseType.Tags[VerseType.Other] first_line_is_tag = True number_found = False - if len(ew_tag) > len(type): # tag is followed by number and/or note - p = re.compile(r'[0-9]+') - m = re.search(p, ew_tag) - if m: - number = m.group() + # check if tag is followed by number and/or note + if len(ew_tag) > len(type): + match = NUMBER_REGEX.search(ew_tag) + if match: + number = match.group() verse_type +=number number_found = True - - p = re.compile(r'\(.*?\)') - m = re.search(p, ew_tag) - if m: - self.comments += ew_tag+'\n' + match = NOTE_REGEX.search(ew_tag) + if match: + self.comments += ew_tag + u'\n' if not number_found: - verse_type += '1' + verse_type += u'1' break self.add_verse( verse_split[-1].strip() if first_line_is_tag else verse, verse_type) if len(self.comments) > 5: - self.comments += unicode(translate('SongsPlugin.EasyWorshipSongImport', - '\n[above are Song Tags with notes imported from EasyWorship]')) + self.comments += unicode( + translate('SongsPlugin.EasyWorshipSongImport', + '\n[above are Song Tags with notes imported from \ + EasyWorship]')) if self.stop_import_flag: break if not self.finish(): From 1876d520ae4a23899210c6ca5efe3ee474a588ac Mon Sep 17 00:00:00 2001 From: Benny Date: Tue, 5 Jul 2011 14:00:34 +0200 Subject: [PATCH 35/35] review fixes --- openlp/plugins/songs/lib/ewimport.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openlp/plugins/songs/lib/ewimport.py b/openlp/plugins/songs/lib/ewimport.py index 732c6e4f0..448d629d5 100644 --- a/openlp/plugins/songs/lib/ewimport.py +++ b/openlp/plugins/songs/lib/ewimport.py @@ -278,18 +278,18 @@ class EasyWorshipSongImport(SongImport): verse_type = VerseType.Tags[VerseType.Verse] for verse in SLIDE_BREAK_REGEX.split(words): verse = verse.strip() - if len(verse) == 0: + if not verse: continue verse_split = verse.split(u'\n', 1) first_line_is_tag = False # EW tags: verse, chorus, pre-chorus, bridge, tag, # intro, ending, slide - for type in VerseType.Names+['tag', 'slide']: + for type in VerseType.Names+[u'tag', u'slide']: type = type.lower() ew_tag = verse_split[0].strip().lower() if ew_tag.startswith(type): verse_type = type[0] - if type == 'tag' or type == 'slide': + if type == u'tag' or type == u'slide': verse_type = VerseType.Tags[VerseType.Other] first_line_is_tag = True number_found = False @@ -298,7 +298,7 @@ class EasyWorshipSongImport(SongImport): match = NUMBER_REGEX.search(ew_tag) if match: number = match.group() - verse_type +=number + verse_type += number number_found = True match = NOTE_REGEX.search(ew_tag) if match: