ukatali/src/ukatali/mainwindow.py

445 lines
23 KiB
Python

# -*- coding: utf-8 -*-
from pathlib import Path
from PyQt5 import QtCore, QtGui, QtWidgets, QtWebEngineWidgets, Qsci, QtPrintSupport
from igitar import Song
from igitar.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 (*)'
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setup_ui()
self.settings = QtCore.QSettings()
self.filename = None
self.is_lyrics_mode = False
self.configure_dialog = ConfigureDialog(self)
def setup_ui(self):
self.setObjectName('MainWindow')
self.resize(800, 600)
self.editor_widget = QtWidgets.QWidget(self)
self.editor_widget.setObjectName('editor_widget')
self.editor_layout = QtWidgets.QVBoxLayout(self.editor_widget)
self.editor_layout.setContentsMargins(0, 0, 0, 0)
self.editor_layout.setSpacing(0)
self.editor_layout.setObjectName('editor_layout')
self.file_editor = Qsci.QsciScintilla(self.editor_widget)
self.file_editor.setFont(QtGui.QFont('monospace'))
self.file_editor.setColor(QtWidgets.QApplication.palette().windowText().color())
self.file_editor.setCaretWidth(2)
self.file_editor.setCaretForegroundColor(QtWidgets.QApplication.palette().windowText().color())
self.file_editor.setMarginsBackgroundColor(QtWidgets.QApplication.palette().window().color())
self.file_editor.setMarginsForegroundColor(QtWidgets.QApplication.palette().windowText().color())
self.file_editor.setMarginLineNumbers(1, True)
self.file_editor.setMarginWidth(1, '000')
self.file_editor.setObjectName('file_editor')
self.chordpro_lexer = ChordProLexer(self.file_editor)
self.file_editor.setLexer(self.chordpro_lexer)
self.editor_layout.addWidget(self.file_editor)
self.setCentralWidget(self.editor_widget)
self.menubar = QtWidgets.QMenuBar(self)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 28))
self.menubar.setObjectName('menubar')
self.file_menu = QtWidgets.QMenu(self.menubar)
self.file_menu.setObjectName('file_menu')
self.edit_menu = QtWidgets.QMenu(self.menubar)
self.edit_menu.setObjectName('edit_menu')
self.settings_menu = QtWidgets.QMenu(self.menubar)
self.settings_menu.setObjectName('settings_menu')
self.help_menu = QtWidgets.QMenu(self.menubar)
self.help_menu.setObjectName('help_menu')
self.setMenuBar(self.menubar)
self.statusbar = QtWidgets.QStatusBar(self)
self.statusbar.setObjectName('statusbar')
self.setStatusBar(self.statusbar)
self.preview_dock = QtWidgets.QDockWidget(self)
self.preview_dock.setObjectName('preview_dock')
self.preview_dock_contents = QtWidgets.QWidget()
self.preview_dock_contents.setObjectName('preview_dock_contents')
self.preview_layout = QtWidgets.QVBoxLayout(self.preview_dock_contents)
self.preview_layout.setContentsMargins(0, 0, 0, 0)
self.preview_layout.setSpacing(0)
self.preview_layout.setObjectName('preview_layout')
self.preview_toolbar = QtWidgets.QToolBar(self.preview_dock_contents)
self.preview_toolbar.setMovable(False)
self.preview_toolbar.setObjectName('preview_toolbar')
self.preview_layout.addWidget(self.preview_toolbar)
self.preview_view = QtWebEngineWidgets.QWebEngineView(self.preview_dock_contents)
self.preview_view.setUrl(QtCore.QUrl('about:blank'))
self.preview_view.setObjectName('preview_view')
self.preview_layout.addWidget(self.preview_view)
self.preview_dock.setWidget(self.preview_dock_contents)
self.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.preview_dock)
self.toolbar = QtWidgets.QToolBar(self)
self.toolbar.setMovable(False)
self.toolbar.setObjectName('toolbar')
self.addToolBar(QtCore.Qt.TopToolBarArea, self.toolbar)
self.new_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('document-new')
self.new_action.setIcon(icon)
self.new_action.setObjectName('new_action')
self.open_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('document-open')
self.open_action.setIcon(icon)
self.open_action.setObjectName('open_action')
self.save_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('document-save')
self.save_action.setIcon(icon)
self.save_action.setObjectName('save_action')
self.save_as_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('document-save-as')
self.save_as_action.setIcon(icon)
self.save_as_action.setObjectName('save_as_action')
self.print_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('document-print')
self.print_action.setIcon(icon)
self.print_action.setObjectName('print_action')
self.export_pdf_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('application-pdf')
self.export_pdf_action.setIcon(icon)
self.export_pdf_action.setObjectName('export_pdf_action')
self.export_html_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('text-html')
self.export_html_action.setIcon(icon)
self.export_html_action.setObjectName('export_html_action')
self.undo_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('edit-undo')
self.undo_action.setIcon(icon)
self.undo_action.setObjectName('undo_action')
self.redo_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('edit-redo')
self.redo_action.setIcon(icon)
self.redo_action.setObjectName('redo_action')
self.cut_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('edit-cut')
self.cut_action.setIcon(icon)
self.cut_action.setObjectName('cut_action')
self.copy_action = QtWidgets.QAction(self)
self.copy_action.setIcon(QtGui.QIcon.fromTheme('edit-copy'))
self.copy_action.setObjectName('copy_action')
self.paste_action = QtWidgets.QAction(self)
self.paste_action.setIcon(QtGui.QIcon.fromTheme('edit-paste'))
self.paste_action.setObjectName('paste_action')
self.configure_action = QtWidgets.QAction(self)
self.configure_action.setIcon(QtGui.QIcon.fromTheme('configure'))
self.configure_action.setObjectName('configure_action')
self.about_action = QtWidgets.QAction(self)
self.about_action.setIcon(QtGui.QIcon.fromTheme('help-about'))
self.about_action.setObjectName('about_action')
self.exit_action = QtWidgets.QAction(self)
self.exit_action.setIcon(QtGui.QIcon.fromTheme('application-exit'))
self.exit_action.setObjectName('exit_action')
self.hide_chords_action = QtWidgets.QAction(self)
self.hide_chords_action.setCheckable(True)
self.hide_chords_action.setChecked(False)
self.hide_chords_action.setIcon(QtGui.QIcon.fromTheme('view-hidden'))
self.hide_chords_action.setObjectName('hide_chords_action')
self.file_menu.addAction(self.new_action)
self.file_menu.addAction(self.open_action)
self.file_menu.addAction(self.save_action)
self.file_menu.addAction(self.save_as_action)
self.file_menu.addSeparator()
self.export_menu = self.file_menu.addMenu('')
self.export_menu.setObjectName('export_menu')
self.export_menu.addAction(self.export_pdf_action)
self.export_menu.addAction(self.export_html_action)
self.file_menu.addAction(self.print_action)
self.file_menu.addSeparator()
self.file_menu.addAction(self.exit_action)
self.edit_menu.addAction(self.undo_action)
self.edit_menu.addAction(self.redo_action)
self.edit_menu.addSeparator()
self.edit_menu.addAction(self.cut_action)
self.edit_menu.addAction(self.copy_action)
self.edit_menu.addAction(self.paste_action)
self.settings_menu.addAction(self.configure_action)
self.help_menu.addAction(self.about_action)
self.menubar.addAction(self.file_menu.menuAction())
self.menubar.addAction(self.edit_menu.menuAction())
self.menubar.addAction(self.settings_menu.menuAction())
self.menubar.addAction(self.help_menu.menuAction())
self.toolbar.addAction(self.new_action)
self.toolbar.addAction(self.open_action)
self.toolbar.addAction(self.save_action)
self.toolbar.addSeparator()
self.toolbar.addAction(self.export_pdf_action)
self.toolbar.addAction(self.print_action)
self.toolbar.addSeparator()
self.toolbar.addAction(self.undo_action)
self.toolbar.addAction(self.redo_action)
self.toolbar.addSeparator()
self.toolbar.addAction(self.cut_action)
self.toolbar.addAction(self.copy_action)
self.toolbar.addAction(self.paste_action)
self.toolbar.addSeparator()
self.toolbar.addAction(self.configure_action)
self.preview_toolbar.addAction(self.hide_chords_action)
self.retranslate_ui()
self.exit_action.triggered.connect(self.close)
self.undo_action.triggered.connect(self.file_editor.undo)
self.redo_action.triggered.connect(self.file_editor.redo)
self.cut_action.triggered.connect(self.file_editor.cut)
self.copy_action.triggered.connect(self.file_editor.copy)
self.paste_action.triggered.connect(self.file_editor.paste)
self.new_action.triggered.connect(self.on_new_clicked)
self.open_action.triggered.connect(self.on_open_clicked)
self.save_action.triggered.connect(self.on_save_clicked)
self.save_as_action.triggered.connect(self.on_save_as_clicked)
self.export_pdf_action.triggered.connect(self.on_export_pdf_clicked)
self.export_html_action.triggered.connect(self.on_export_html_clicked)
self.print_action.triggered.connect(self.on_print_clicked)
self.configure_action.triggered.connect(self.on_configure_clicked)
self.file_editor.textChanged.connect(self.on_text_changed)
self.preview_view.page().pdfPrintingFinished.connect(self.on_pdf_finished)
self.hide_chords_action.triggered.connect(self.on_hide_chords_clicked)
def retranslate_ui(self):
_translate = QtCore.QCoreApplication.translate
self.setWindowTitle(_translate('MainWindow', 'Untitled') + '[*] - Ukatali')
self.file_menu.setTitle(_translate('MainWindow', '&File'))
self.edit_menu.setTitle(_translate('MainWindow', '&Edit'))
self.settings_menu.setTitle(_translate('MainWindow', '&Settings'))
self.help_menu.setTitle(_translate('MainWindow', '&Help'))
self.preview_dock.setWindowTitle(_translate('MainWindow', 'Preview'))
self.toolbar.setWindowTitle(_translate('MainWindow', 'toolbar'))
self.new_action.setText(_translate('MainWindow', '&New'))
self.new_action.setToolTip(_translate('MainWindow', 'Create a new ChordPro file'))
self.new_action.setShortcut(_translate('MainWindow', 'Ctrl+N'))
self.open_action.setText(_translate('MainWindow', '&Open...'))
self.open_action.setToolTip(_translate('MainWindow', 'Open an existing ChordPro file'))
self.open_action.setShortcut(_translate('MainWindow', 'Ctrl+O'))
self.save_action.setText(_translate('MainWindow', '&Save'))
self.save_action.setToolTip(_translate('MainWindow', 'Save the current ChordPro file'))
self.save_action.setShortcut(_translate('MainWindow', 'Ctrl+S'))
self.save_as_action.setText(_translate('MainWindow', 'Save &As...'))
self.save_as_action.setToolTip(_translate('MainWindow', 'Save the current file under a new name'))
self.save_as_action.setShortcut(_translate('MainWindow', 'Ctrl+Shift+S'))
self.print_action.setText(_translate('MainWindow', '&Print'))
self.print_action.setToolTip(_translate('MainWindow', 'Print the current ChordPro file'))
self.print_action.setShortcut(_translate('MainWindow', 'Ctrl+P'))
self.export_menu.setTitle(_translate('MainWindow', '&Export'))
self.export_pdf_action.setText(_translate('MainWindow', 'Export to &PDF...'))
self.export_pdf_action.setToolTip(_translate('MainWindow', 'Export the current file as a PDF file'))
self.export_pdf_action.setShortcut(_translate('MainWindow', 'Ctrl+E'))
self.export_html_action.setText(_translate('MainWindow', 'Export to &HTML...'))
self.export_html_action.setToolTip(_translate('MainWindow', 'Export the current file as an HTML file'))
self.undo_action.setText(_translate('MainWindow', '&Undo'))
self.undo_action.setToolTip(_translate('MainWindow', 'Undo the last edit'))
self.undo_action.setShortcut(_translate('MainWindow', 'Ctrl+Z'))
self.redo_action.setText(_translate('MainWindow', '&Redo'))
self.redo_action.setToolTip(_translate('MainWindow', 'Redo the last undone action'))
self.redo_action.setShortcut(_translate('MainWindow', 'Ctrl+Shift+Z'))
self.cut_action.setText(_translate('MainWindow', 'Cu&t'))
self.cut_action.setToolTip(_translate('MainWindow', 'Cut the currently selected text'))
self.cut_action.setShortcut(_translate('MainWindow', 'Ctrl+X'))
self.copy_action.setText(_translate('MainWindow', '&Copy'))
self.copy_action.setToolTip(_translate('MainWindow', 'Copy the currently selected text to the clipboard'))
self.copy_action.setShortcut(_translate('MainWindow', 'Ctrl+C'))
self.paste_action.setText(_translate('MainWindow', '&Paste'))
self.paste_action.setToolTip(_translate('MainWindow', 'Paste the current clipboard text into the editor'))
self.paste_action.setShortcut(_translate('MainWindow', 'Ctrl+V'))
self.configure_action.setText(_translate('MainWindow', '&Configure...'))
self.configure_action.setToolTip(_translate('MainWindow', 'Configure Ukatali'))
self.about_action.setText(_translate('MainWindow', '&About'))
self.about_action.setToolTip(_translate('MainWindow', 'About Ukatali'))
self.exit_action.setText(_translate('MainWindow', 'E&xit'))
self.exit_action.setToolTip(_translate('MainWindow', 'Quit Ukatali'))
self.exit_action.setShortcut(_translate('MainWindow', 'Alt+F4'))
self.hide_chords_action.setText(_translate('MainWindow', 'Hide Chords'))
def on_new_clicked(self):
"""Start a new file"""
if self.file_editor.isModified() and QtWidgets.QMessageBox.question(self, 'Save file?',
'The current file is not saved, do you '
'want to save it now?') \
== QtWidgets.QMessageBox.Yes:
self.on_save_clicked()
self.file_editor.setText('')
self.file_editor.setModified(False)
self.filename = ''
self.setWindowTitle('Untitled[*] - Ukatali')
self.setWindowModified(False)
def on_open_clicked(self):
"""Open the file"""
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)
if isinstance(filename, tuple):
filename = filename[0]
if not filename:
return
self.filename = filename
file_path = Path(filename)
if file_path.exists():
self.settings.setValue('files/last-directory', str(file_path.parent))
self.file_editor.setText(file_path.open().read())
self.setWindowTitle('{}[*] - Ukatali'.format(file_path.name))
self.setWindowModified(False)
def on_save_clicked(self):
"""Save the file"""
if not self.filename:
self.on_save_as_clicked()
else:
with open(self.filename, 'w') as fd:
fd.write(self.file_editor.text())
self.setWindowTitle('{}[*] - Ukatali'.format(Path(self.filename).name))
self.setWindowModified(False)
def on_save_as_clicked(self):
"""Save the file"""
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)
if isinstance(filename, tuple):
filename = filename[0]
if not filename:
return
self.filename = filename
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 and self.file_editor.text():
self.on_text_changed()
def _get_render_options(self, is_lyrics_mode=False):
"""Get all the render options from the settings"""
options = {}
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
if is_lyrics_mode and 'indent' in name:
options[name] = 0
elif is_lyrics_mode and 'is_hidden' in name:
options[name] = True
self.settings.endGroup()
return options
def _render_song(self, extra_styles=None):
"""Render the current song and return the HTML"""
options = self._get_render_options(self.is_lyrics_mode)
text = self.file_editor.text()
song = Song()
try:
song.parse(text)
return render(song, options, extra_styles)
except Exception:
return None
def on_text_changed(self):
"""Update the preview when the text changes"""
html = self._render_song()
if html:
self.preview_view.setHtml(html)
def on_export_pdf_clicked(self):
"""Export the current song to PDF"""
if self.filename:
last_directory = str(Path(self.filename).with_suffix('.pdf'))
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,
filter='PDF files (.pdf)*')
if isinstance(filename, tuple):
filename = filename[0]
if not filename:
return
filename = Path(filename)
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_export_html_clicked(self):
"""Export the current song to HTML"""
if self.filename:
last_directory = str(Path(self.filename).with_suffix('.html'))
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 HTML', last_directory,
filter='HTML files (.html)*')
if isinstance(filename, tuple):
filename = filename[0]
if not filename:
return
filename = Path(filename)
self.settings.setValue('files/last-directory', str(filename.parent))
# Export to HTML
try:
with filename.open('w') as html_file:
html = self._render_song()
if html:
html_file.write(html)
QtWidgets.QMessageBox.information(self, 'Export to HTML Successful',
'Successfully exported to "{}"'.format(filename.name))
else:
QtWidgets.QMessageBox.critical(self, 'Error Exporting to HTML',
'There was an error while exporting to HTML')
except Exception as e:
QtWidgets.QMessageBox.critical(self, 'Error Exporting to HTML',
'There was an error while exporting to HTML:\n{e}'.format(e=e))
def on_pdf_finished(self, filename, is_success):
"""A slot to notify the user when the PDF is done"""
if is_success:
QtWidgets.QMessageBox.information(self, 'Export to PDF Successful',
'Successfully exported to "{}"'.format(Path(filename).name))
else:
QtWidgets.QMessageBox.critical(self, 'Error Exporting to PDF', 'There was an error while exporting to PDF')
def on_print_finished(self, is_success):
print(is_success)
def on_print_clicked(self):
"""Print the current song"""
printer = QtPrintSupport.QPrinter(QtPrintSupport.QPrinter.HighResolution)
print_dialog = QtPrintSupport.QPrintDialog(printer, self.preview_view.page().view())
if print_dialog.exec() == QtWidgets.QDialog.Accepted:
self.preview_view.page().print(printer, self.on_print_finished)
def on_hide_chords_clicked(self, is_checked):
"""Hide the chords in the preview window"""
self.is_lyrics_mode = is_checked
if self.file_editor.text():
self.on_text_changed()