Compare commits

..

1 Commits

Author SHA1 Message Date
bb334971a2
Add a configuration dialog, better output and PDF exporting
- Add configure dialog
- Dynamically create rendering options
- Load and save settings
- Pass on options to the renderer
- Add editor settings to the configure dialog
- Add exporting to PDF and printing
- Add options for PDF export to the configure dialog
2021-08-07 17:38:43 -07:00
3 changed files with 234 additions and 54 deletions

View File

@ -2,25 +2,40 @@
from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets
from chordpro.renderers.html import get_options, get_option_groups
from ukatali.util import coerce_bool, convert_units
def _coerce_bool(value):
"""Coerce a value to be a boolean"""
if isinstance(value, str):
return value[0].lower() in ['t', 'y', '1']
else:
return bool(value)
SUPPORTED_SIZES = {
QtGui.QPageSize.A4: 'A4',
QtGui.QPageSize.A5: 'A5',
QtGui.QPageSize.B5: 'B5',
QtGui.QPageSize.B6: 'B6',
QtGui.QPageSize.Letter: 'Letter',
QtGui.QPageSize.Legal: 'Legal',
QtGui.QPageSize.ExecutiveStandard: 'Executive'
}
SUPPORTED_UNITS = {
QtGui.QPageSize.Millimeter: 'mm',
QtGui.QPageSize.Point: 'pt',
QtGui.QPageSize.Inch: 'in'
}
SUPPORTED_ORIENTATIONS = {
'Portrait': QtGui.QPageLayout.Portrait,
'Landscape': QtGui.QPageLayout.Landscape
}
class ConfigureDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.settings = QtCore.QSettings()
self.option_widgets = {}
self.option_values = {}
self.editor_values = {}
self.export_values = {}
self.setup_ui()
self.setup_options()
self.editor_values = {}
def setup_ui(self):
self.setObjectName('ConfigureDialog')
@ -32,7 +47,7 @@ class ConfigureDialog(QtWidgets.QDialog):
self.configure_layout.addLayout(self.category_layout)
self.category_list_widget = QtWidgets.QListWidget(self)
self.category_list_widget.setFixedWidth(100)
self.category_list_widget.addItems(['', '', ''])
self.category_list_widget.addItems(['', '', '', ''])
self.category_list_widget.setObjectName('category_list_widget')
self.category_layout.addWidget(self.category_list_widget)
self.category_stack = QtWidgets.QStackedWidget(self)
@ -68,6 +83,56 @@ class ConfigureDialog(QtWidgets.QDialog):
self.render_options_stack = QtWidgets.QStackedWidget(self)
self.render_options_stack.setObjectName("render_options_stack")
self.render_layout.addWidget(self.render_options_stack)
# Export settings
self.export_page = QtWidgets.QWidget()
self.export_page.setObjectName('export_page')
self.category_stack.addWidget(self.export_page)
self.export_layout = QtWidgets.QFormLayout(self.export_page)
self.export_layout.setObjectName('export_layout')
self.export_size_combobox = QtWidgets.QComboBox(self.export_page)
for idx, (size_id, size_name) in enumerate(SUPPORTED_SIZES.items()):
self.export_size_combobox.addItem(size_name)
self.export_size_combobox.setItemData(idx, size_id)
self.export_size_combobox.setCurrentIndex(0)
self.export_size_combobox.setObjectName('export_size_combobox')
self.export_layout.addRow('Page size', self.export_size_combobox)
self.export_orientation_layout = QtWidgets.QHBoxLayout()
self.export_orientation_layout.setObjectName('export_orientation_layout')
self.export_orientation_portrait_radio = QtWidgets.QRadioButton(self.export_page)
self.export_orientation_portrait_radio.setChecked(True)
self.export_orientation_portrait_radio.setObjectName('export_orientation_portrait_radio')
self.export_orientation_layout.addWidget(self.export_orientation_portrait_radio)
self.export_orientation_landscape_radio = QtWidgets.QRadioButton(self.export_page)
self.export_orientation_landscape_radio.setObjectName('export_orientation_landscape_radio')
self.export_orientation_layout.addWidget(self.export_orientation_landscape_radio)
self.export_layout.addRow('Orientation', self.export_orientation_layout)
self.export_unit_combobox = QtWidgets.QComboBox(self.export_page)
for idx, (unit_id, unit_name) in enumerate(SUPPORTED_UNITS.items()):
self.export_unit_combobox.addItem(unit_name)
self.export_unit_combobox.setItemData(idx, unit_id)
self.export_unit_combobox.setCurrentIndex(0)
self.export_unit_combobox.setObjectName('export_unit_combobox')
self.export_layout.addRow('Unit', self.export_unit_combobox)
self.export_margin_top_spinbox = QtWidgets.QDoubleSpinBox(self.export_page)
self.export_margin_top_spinbox.setValue(20)
self.export_margin_top_spinbox.setSuffix(self.export_unit_combobox.currentText())
self.export_margin_top_spinbox.setObjectName('export_margin_top_spinbox')
self.export_layout.addRow('Top margin', self.export_margin_top_spinbox)
self.export_margin_left_spinbox = QtWidgets.QDoubleSpinBox(self.export_page)
self.export_margin_left_spinbox.setValue(20)
self.export_margin_left_spinbox.setSuffix(self.export_unit_combobox.currentText())
self.export_margin_left_spinbox.setObjectName('export_margin_left_spinbox')
self.export_layout.addRow('Left margin', self.export_margin_left_spinbox)
self.export_margin_right_spinbox = QtWidgets.QDoubleSpinBox(self.export_page)
self.export_margin_right_spinbox.setValue(20)
self.export_margin_right_spinbox.setSuffix(self.export_unit_combobox.currentText())
self.export_margin_right_spinbox.setObjectName('export_margin_right_spinbox')
self.export_layout.addRow('Right margin', self.export_margin_right_spinbox)
self.export_margin_bottom_spinbox = QtWidgets.QDoubleSpinBox(self.export_page)
self.export_margin_bottom_spinbox.setValue(20)
self.export_margin_bottom_spinbox.setSuffix(self.export_unit_combobox.currentText())
self.export_margin_bottom_spinbox.setObjectName('export_margin_bottom_spinbox')
self.export_layout.addRow('Bottom margin', self.export_margin_bottom_spinbox)
# Buttons
self.button_box = QtWidgets.QDialogButtonBox(self)
self.button_box.setOrientation(QtCore.Qt.Horizontal)
@ -81,8 +146,23 @@ class ConfigureDialog(QtWidgets.QDialog):
self.button_box.rejected.connect(self.reject)
self.category_list_widget.currentRowChanged.connect(self.category_stack.setCurrentIndex)
self.render_options_combobox.activated.connect(self.render_options_stack.setCurrentIndex)
self.editor_font_combobox.currentFontChanged.connect(self.on_editor_font_changed)
self.editor_size_spinbox.valueChanged.connect(self.on_editor_size_changed)
on_editor_font_changed = partial(self._set_editor_value, 'font')
self.editor_font_combobox.currentFontChanged.connect(on_editor_font_changed)
on_editor_size_changed = partial(self._set_editor_value, 'size')
self.editor_size_spinbox.valueChanged.connect(on_editor_size_changed)
on_export_size_changed = partial(self._set_export_value, 'page_size')
self.export_size_combobox.currentIndexChanged.connect(on_export_size_changed)
self.export_unit_combobox.currentIndexChanged.connect(self.on_export_unit_changed)
on_export_margin_left_changed = partial(self._set_export_value, 'margin_left')
self.export_margin_left_spinbox.valueChanged.connect(on_export_margin_left_changed)
on_export_margin_right_changed = partial(self._set_export_value, 'margin_right')
self.export_margin_right_spinbox.valueChanged.connect(on_export_margin_right_changed)
on_export_margin_top_changed = partial(self._set_export_value, 'margin_top')
self.export_margin_top_spinbox.valueChanged.connect(on_export_margin_top_changed)
on_export_margin_bottom_changed = partial(self._set_export_value, 'margin_bottom')
self.export_margin_bottom_spinbox.valueChanged.connect(on_export_margin_bottom_changed)
on_export_orientation_portait_clicked = partial(self._set_export_value, 'orientation')
self.export_orientation_portrait_radio.toggled.connect(on_export_orientation_portait_clicked)
def retranslate_ui(self):
_translate = QtCore.QCoreApplication.translate
@ -90,14 +170,29 @@ class ConfigureDialog(QtWidgets.QDialog):
self.category_list_widget.item(0).setText(_translate('ConfigureDialog', 'General'))
self.category_list_widget.item(1).setText(_translate('ConfigureDialog', 'Editor'))
self.category_list_widget.item(2).setText(_translate('ConfigureDialog', 'Render'))
self.category_list_widget.item(3).setText(_translate('ConfigureDialog', 'PDF'))
self.export_orientation_portrait_radio.setText(_translate('ConfigureDialog', 'Portrait'))
self.export_orientation_landscape_radio.setText(_translate('ConfigureDialog', 'Landscape'))
def _set_value(self, name, value):
def _set_option_value(self, name, value):
"""Set an option value"""
self.option_values[name] = value
def _set_editor_value(self, name, value):
"""Set an editor value"""
self.editor_values[name] = value
def _set_export_value(self, name, value):
"""Set an export value"""
if name == 'page_size':
value = self.export_size_combobox.itemData(value)
elif name == 'units':
value = self.export_unit_combobox.itemData(value)
elif name == 'orientation':
value = QtGui.QPageLayout.Portrait if value else QtGui.QPageLayout.Landscape
self.export_values[name] = value
def setup_options(self):
self.option_widgets = {}
self.option_values = {}
for group in get_option_groups():
pretty_group = group.replace('_', ' ').title()
self.render_options_combobox.addItem(pretty_group)
@ -107,7 +202,7 @@ class ConfigureDialog(QtWidgets.QDialog):
pretty_name = name.replace(group, '').replace('_', ' ').title()
self.option_values[name] = details['default']
widget = None
set_value = partial(self._set_value, name)
set_value = partial(self._set_option_value, name)
if details["type"] is int:
widget = QtWidgets.QSpinBox(page_widget)
if details['default']:
@ -115,7 +210,7 @@ class ConfigureDialog(QtWidgets.QDialog):
widget.valueChanged.connect(set_value)
elif details["type"] is bool:
widget = QtWidgets.QCheckBox(page_widget)
widget.setChecked(_coerce_bool(details['default']))
widget.setChecked(coerce_bool(details['default']))
widget.toggled.connect(set_value)
elif 'font' in name:
widget = QtWidgets.QFontComboBox(page_widget)
@ -142,13 +237,13 @@ class ConfigureDialog(QtWidgets.QDialog):
# Editor settings
self.settings.beginGroup('editor')
if self.settings.contains('font'):
value = self.settings.value('font')
value = QtGui.QFont(self.settings.value('font'))
self.editor_values['font'] = value
self.editor_font_combobox.setCurrentFont(QtGui.QFont(value))
self.editor_font_combobox.setCurrentFont(value)
if self.settings.contains('size'):
value = self.settings.value('size')
self.editor_values['size'] = int(value)
self.editor_size.setValue(int(value))
value = int(self.settings.value('size'))
self.editor_values['size'] = value
self.editor_size.setValue(value)
self.settings.endGroup()
# Renderer settings
self.settings.beginGroup('render')
@ -161,7 +256,7 @@ class ConfigureDialog(QtWidgets.QDialog):
value = int(value)
self.option_widgets[name].setValue(value)
elif details['type'] is bool:
value = _coerce_bool(value)
value = coerce_bool(value)
self.option_widgets[name].setChecked(value)
elif 'font' in name:
value = QtGui.QFont(value)
@ -170,6 +265,32 @@ class ConfigureDialog(QtWidgets.QDialog):
self.option_widgets[name].setText(value)
self.option_values[name] = value
self.settings.endGroup()
# PDF settings
self.settings.beginGroup('export')
if self.settings.contains('page_size'):
value = int(self.settings.value('page_size'))
self.export_values['page_size'] = value
self.export_size_combobox.setCurrentText(SUPPORTED_SIZES[value])
if self.settings.contains('unit'):
value = int(self.settings.value('unit'))
self.export_values['unit'] = value
self.export_unit_combobox.setCurrentText(SUPPORTED_UNITS[value])
if self.settings.contains('orientation'):
value = int(self.settings.value('orientation'))
self.export_values['orientation'] = value
self.export_orientation_portrait_radio.setChecked(value == QtGui.QPageLayout.Portrait)
margins = [
('margin_left', self.export_margin_left_spinbox),
('margin_right', self.export_margin_right_spinbox),
('margin_top', self.export_margin_top_spinbox),
('margin_bottom', self.export_margin_bottom_spinbox),
]
for name, spinbox in margins:
if self.settings.contains(name):
value = float(self.settings.value(name))
self.export_values[name] = value
spinbox.setValue(value)
self.settings.endGroup()
def save_settings(self):
"""Save the settings"""
@ -187,6 +308,11 @@ class ConfigureDialog(QtWidgets.QDialog):
value = value.family()
self.settings.setValue(name, value)
self.settings.endGroup()
# Export settings
self.settings.beginGroup('export')
for name, value in self.export_values.items():
self.settings.setValue(name, value)
self.settings.endGroup()
def exec(self):
"""Execute the dialog"""
@ -198,10 +324,15 @@ class ConfigureDialog(QtWidgets.QDialog):
self.save_settings()
return super().accept()
def on_editor_font_changed(self, font):
"""Set the new font if the font has changed"""
self.editor_values['font'] = font.family()
def on_editor_size_changed(self, size):
"""Set the new size if the size has changed"""
self.editor_values['size'] = size
def on_export_unit_changed(self, value):
self._set_export_value('unit', self.export_unit_combobox.itemData(value))
new_suffix = self.export_unit_combobox.itemText(value)
for spinbox in [self.export_margin_bottom_spinbox, self.export_margin_left_spinbox,
self.export_margin_right_spinbox, self.export_margin_top_spinbox]:
old_suffix = spinbox.suffix()
spinbox.setSuffix(new_suffix)
spinbox.setValue(convert_units(spinbox.value(), old_suffix, new_suffix))
if new_suffix == 'in':
spinbox.setSingleStep(0.1)
else:
spinbox.setSingleStep(1.0)

View File

@ -1,11 +1,14 @@
# -*- coding: utf-8 -*-
import os
from pathlib import Path
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets, Qsci, QtPrintSupport
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets, Qsci, QtPrintSupport
from chordpro import Song
from chordpro.renderers.html import render
from chordpro.renderers.html import render, get_options
from ukatali.configuredialog import ConfigureDialog
from ukatali.lexer import ChordProLexer
from ukatali.util import coerce_bool
CHORDPRO_FILTERS = 'ChordPro files (*.txt *.cho *.chordpro *.chopro);;All files (*)'
@ -14,6 +17,7 @@ class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.settings = QtCore.QSettings()
self.filename = None
self.configure_dialog = ConfigureDialog(self)
@ -233,8 +237,8 @@ class MainWindow(QtWidgets.QMainWindow):
def on_open_clicked(self):
"""Open the file"""
if QtCore.QSettings().value('files/last-directory'):
last_directory = QtCore.QSettings().value('files/last-directory')
if self.settings.value('files/last-directory'):
last_directory = self.settings.value('files/last-directory')
else:
last_directory = ''
filename = QtWidgets.QFileDialog.getOpenFileName(self, 'Open file', last_directory, filter=CHORDPRO_FILTERS)
@ -246,7 +250,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.filename = filename
file_path = Path(filename)
if file_path.exists():
QtCore.QSettings().setValue('files/last-directory', str(file_path.parent))
self.settings.setValue('files/last-directory', str(file_path.parent))
self.file_editor.setText(file_path.open().read())
def on_save_clicked(self):
@ -259,8 +263,8 @@ class MainWindow(QtWidgets.QMainWindow):
def on_save_as_clicked(self):
"""Save the file"""
if QtCore.QSettings().value('files/last-directory'):
last_directory = QtCore.QSettings().value('files/last-directory')
if self.settings.value('files/last-directory'):
last_directory = self.settings.value('files/last-directory')
else:
last_directory = ''
filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Save file', last_directory, filter=CHORDPRO_FILTERS)
@ -270,24 +274,29 @@ class MainWindow(QtWidgets.QMainWindow):
return
self.filename = filename
QtCore.QSettings().setValue('files/last-directory', str(Path(filename).parent))
self.settings.setValue('files/last-directory', str(Path(filename).parent))
self.on_save_clicked()
def on_configure_clicked(self):
"""Show the configuration dialog"""
if self.configure_dialog.exec() == QtWidgets.QDialog.Accepted:
if self.configure_dialog.exec() == QtWidgets.QDialog.Accepted and self.file_editor.text():
self.on_text_changed()
def _get_render_options(self):
"""Get all the render options from the settings"""
options = {}
settings = QtCore.QSettings()
settings.beginGroup('render')
for key in settings.allKeys():
value = settings.value(key)
if value:
options[key] = value
settings.endGroup()
self.settings.beginGroup('render')
for name, details in get_options().items():
if self.settings.contains(name):
value = self.settings.value(name)
if value is None:
continue
if details['type'] is int:
value = int(value)
elif details['type'] is bool:
value = coerce_bool(value)
options[name] = value
self.settings.endGroup()
return options
def _render_song(self, extra_styles=None):
@ -296,6 +305,10 @@ class MainWindow(QtWidgets.QMainWindow):
text = self.file_editor.text()
song = Song()
song.parse(text)
if song.metadata.get('copyright'):
extra_styles = extra_styles or ''
extra_styles += os.linesep
extra_styles += '@page {{ @bottom-left {{ content: "{}"; }} }}'.format(song.metadata.get('copyright'))
return render(song, options, extra_styles)
def on_text_changed(self):
@ -307,8 +320,8 @@ class MainWindow(QtWidgets.QMainWindow):
"""Export the current song to PDF"""
if self.filename:
last_directory = str(Path(self.filename).with_suffix('.pdf'))
elif QtCore.QSettings().value('files/last-directory'):
last_directory = QtCore.QSettings().value('files/last-directory')
elif self.settings.value('files/last-directory'):
last_directory = self.settings.value('files/last-directory')
else:
last_directory = ''
filename = QtWidgets.QFileDialog.getSaveFileName(self, 'Export to PDF', last_directory,
@ -318,10 +331,21 @@ class MainWindow(QtWidgets.QMainWindow):
if not filename:
return
filename = Path(filename)
QtCore.QSettings().setValue('files/last-directory', str(filename.parent))
self.preview_view.page().printToPdf(str(filename), QtGui.QPageLayout(QtGui.QPageSize(QtGui.QPageSize.Letter),
QtGui.QPageLayout.Portrait,
QtCore.QMarginsF()))
self.settings.setValue('files/last-directory', str(filename.parent))
# Load the page layout from settings
self.settings.beginGroup('export')
page_size = QtGui.QPageSize(int(self.settings.value('page_size', QtGui.QPageSize.A4)))
orientation = int(self.settings.value('orientation', QtGui.QPageLayout.Portrait))
unit = int(self.settings.value('unit', QtGui.QPageLayout.Millimeter))
margin_left = float(self.settings.value('margin_left', 20))
margin_right = float(self.settings.value('margin_right', 20))
margin_top = float(self.settings.value('margin_top', 20))
margin_bottom = float(self.settings.value('margin_bottom', 20))
self.settings.endGroup()
margins = QtCore.QMarginsF(QtCore.QMargins(margin_left, margin_top, margin_right, margin_bottom))
page_layout = QtGui.QPageLayout(page_size, orientation, margins, unit)
# Export to PDF
self.preview_view.page().printToPdf(str(filename), page_layout)
def on_pdf_finished(self, filename, is_success):
"""A slot to notify the user when the PDF is done"""
@ -340,7 +364,7 @@ class MainWindow(QtWidgets.QMainWindow):
# print(QtPrintSupport.QPrinterInfo.defaultPrinterName())
# default_printer = QtPrintSupport.QPrinterInfo.defaultPrinter()
# print(default_printer)
# printer = QtPrintSupport.QPrinter()
print_dialog = QtPrintSupport.QPrintDialog(QtPrintSupport.QPrinter(), self)
printer = QtPrintSupport.QPrinter()
print_dialog = QtPrintSupport.QPrintDialog(printer, self.preview_view.page().view())
if print_dialog.exec() == QtWidgets.QDialog.Accepted:
self.preview_view.page().print(print_dialog.printer(), _on_print_finished)
self.preview_view.page().print(printer, _on_print_finished)

25
src/ukatali/util.py Normal file
View File

@ -0,0 +1,25 @@
def coerce_bool(value):
"""Coerce a value to be a boolean"""
if isinstance(value, str):
return value[0].lower() in ['t', 'y', '1']
else:
return bool(value)
def convert_units(value, old_unit, new_unit):
"""Convert a value from one unit to another"""
if old_unit == 'in' and new_unit == 'mm':
return value * 25.4
if old_unit == 'mm' and new_unit == 'in':
return value / 25.4
if old_unit == 'in' and new_unit == 'pt':
return value * 72.0
if old_unit == 'pt' and new_unit == 'in':
return value / 72.0
if old_unit == 'mm' and new_unit == 'pt':
# Convert to in and then to pt
return (value / 25.4) * 72.0
if old_unit == 'pt' and new_unit == 'mm':
# Convert to in and then to mm
return (value / 72.0) * 25.4
return value