Compare commits

...

12 Commits

5 changed files with 170 additions and 46 deletions

View File

@ -1,3 +1,51 @@
[metadata]
name = ukatali
author = Raoul Snyman
author_email = raoul@snyman.info
description = A ChordPro GUI, written in Python and Qt5
long_description = file:README.rst
long_description_content_type = text/x-rst
url = https://git.snyman.info/raoul/ukatali
license = MIT
classifiers =
Development Status :: 3 - Alpha
Intended Audience :: End Users/Desktop
License :: OSI Approved :: MIT License
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Topic :: Utilities
keywords =
music
chords
guitar
[options]
package_dir =
= src
packages = find:
python_requires = >=3.7
setup_requires =
setuptools_scm
install_requires =
igitar
PyQt5
PyQtWebEngine
QScintilla
[options.packages.find]
where = src
[options.entry_points]
console_scripts =
ukatali = ukatali.app:run_ukutali
[bdist_wheel]
universal = 1
[pep8] [pep8]
exclude=resources.py,vlc.py exclude=resources.py,vlc.py
max-line-length = 120 max-line-length = 120

2
setup.py Normal file
View File

@ -0,0 +1,2 @@
from setuptools import setup
setup(use_scm_version=True, setup_requires=['setuptools_scm'])

View File

@ -2,7 +2,7 @@
from functools import partial from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
from chordpro.renderers.html import get_options, get_option_groups from igitar.renderers.html import get_options, get_option_groups
from ukatali.util import coerce_bool, convert_units from ukatali.util import coerce_bool, convert_units

View File

@ -1,7 +1,7 @@
import re import re
from PyQt5 import QtCore, QtGui, QtWidgets, Qsci from PyQt5 import QtCore, QtGui, QtWidgets, Qsci
from chordpro.constants import KNOWN_DIRECTIVES, KNOWN_VERSE_TYPES from igitar.constants import KNOWN_DIRECTIVES, KNOWN_VERSE_TYPES
PALETTE_ROLES = { PALETTE_ROLES = {
'window': QtGui.QPalette.WindowText, 'window': QtGui.QPalette.WindowText,

View File

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
from pathlib import Path 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 igitar import Song
from chordpro.renderers.html import render, get_options from igitar.renderers.html import render, get_options
from ukatali.configuredialog import ConfigureDialog from ukatali.configuredialog import ConfigureDialog
from ukatali.lexer import ChordProLexer from ukatali.lexer import ChordProLexer
@ -19,6 +18,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.setup_ui() self.setup_ui()
self.settings = QtCore.QSettings() self.settings = QtCore.QSettings()
self.filename = None self.filename = None
self.is_lyrics_mode = False
self.configure_dialog = ConfigureDialog(self) self.configure_dialog = ConfigureDialog(self)
def setup_ui(self): def setup_ui(self):
@ -67,6 +67,10 @@ class MainWindow(QtWidgets.QMainWindow):
self.preview_layout.setContentsMargins(0, 0, 0, 0) self.preview_layout.setContentsMargins(0, 0, 0, 0)
self.preview_layout.setSpacing(0) self.preview_layout.setSpacing(0)
self.preview_layout.setObjectName('preview_layout') 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 = QtWebEngineWidgets.QWebEngineView(self.preview_dock_contents)
self.preview_view.setUrl(QtCore.QUrl('about:blank')) self.preview_view.setUrl(QtCore.QUrl('about:blank'))
self.preview_view.setObjectName('preview_view') self.preview_view.setObjectName('preview_view')
@ -74,6 +78,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.preview_dock.setWidget(self.preview_dock_contents) self.preview_dock.setWidget(self.preview_dock_contents)
self.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.preview_dock) self.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.preview_dock)
self.toolbar = QtWidgets.QToolBar(self) self.toolbar = QtWidgets.QToolBar(self)
self.toolbar.setMovable(False)
self.toolbar.setObjectName('toolbar') self.toolbar.setObjectName('toolbar')
self.addToolBar(QtCore.Qt.TopToolBarArea, self.toolbar) self.addToolBar(QtCore.Qt.TopToolBarArea, self.toolbar)
self.new_action = QtWidgets.QAction(self) self.new_action = QtWidgets.QAction(self)
@ -96,10 +101,14 @@ class MainWindow(QtWidgets.QMainWindow):
icon = QtGui.QIcon.fromTheme('document-print') icon = QtGui.QIcon.fromTheme('document-print')
self.print_action.setIcon(icon) self.print_action.setIcon(icon)
self.print_action.setObjectName('print_action') self.print_action.setObjectName('print_action')
self.export_action = QtWidgets.QAction(self) self.export_pdf_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('document-export') icon = QtGui.QIcon.fromTheme('application-pdf')
self.export_action.setIcon(icon) self.export_pdf_action.setIcon(icon)
self.export_action.setObjectName('export_action') 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) self.undo_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('edit-undo') icon = QtGui.QIcon.fromTheme('edit-undo')
self.undo_action.setIcon(icon) self.undo_action.setIcon(icon)
@ -113,31 +122,34 @@ class MainWindow(QtWidgets.QMainWindow):
self.cut_action.setIcon(icon) self.cut_action.setIcon(icon)
self.cut_action.setObjectName('cut_action') self.cut_action.setObjectName('cut_action')
self.copy_action = QtWidgets.QAction(self) self.copy_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('edit-copy') self.copy_action.setIcon(QtGui.QIcon.fromTheme('edit-copy'))
self.copy_action.setIcon(icon)
self.copy_action.setObjectName('copy_action') self.copy_action.setObjectName('copy_action')
self.paste_action = QtWidgets.QAction(self) self.paste_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('edit-paste') self.paste_action.setIcon(QtGui.QIcon.fromTheme('edit-paste'))
self.paste_action.setIcon(icon)
self.paste_action.setObjectName('paste_action') self.paste_action.setObjectName('paste_action')
self.configure_action = QtWidgets.QAction(self) self.configure_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('configure') self.configure_action.setIcon(QtGui.QIcon.fromTheme('configure'))
self.configure_action.setIcon(icon)
self.configure_action.setObjectName('configure_action') self.configure_action.setObjectName('configure_action')
self.about_action = QtWidgets.QAction(self) self.about_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('help-about') self.about_action.setIcon(QtGui.QIcon.fromTheme('help-about'))
self.about_action.setIcon(icon)
self.about_action.setObjectName('about_action') self.about_action.setObjectName('about_action')
self.exit_action = QtWidgets.QAction(self) self.exit_action = QtWidgets.QAction(self)
icon = QtGui.QIcon.fromTheme('application-exit') self.exit_action.setIcon(QtGui.QIcon.fromTheme('application-exit'))
self.exit_action.setIcon(icon)
self.exit_action.setObjectName('exit_action') 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.new_action)
self.file_menu.addAction(self.open_action) self.file_menu.addAction(self.open_action)
self.file_menu.addAction(self.save_action) self.file_menu.addAction(self.save_action)
self.file_menu.addAction(self.save_as_action) self.file_menu.addAction(self.save_as_action)
self.file_menu.addSeparator() self.file_menu.addSeparator()
self.file_menu.addAction(self.export_action) 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.addAction(self.print_action)
self.file_menu.addSeparator() self.file_menu.addSeparator()
self.file_menu.addAction(self.exit_action) self.file_menu.addAction(self.exit_action)
@ -157,7 +169,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.toolbar.addAction(self.open_action) self.toolbar.addAction(self.open_action)
self.toolbar.addAction(self.save_action) self.toolbar.addAction(self.save_action)
self.toolbar.addSeparator() self.toolbar.addSeparator()
self.toolbar.addAction(self.export_action) self.toolbar.addAction(self.export_pdf_action)
self.toolbar.addAction(self.print_action) self.toolbar.addAction(self.print_action)
self.toolbar.addSeparator() self.toolbar.addSeparator()
self.toolbar.addAction(self.undo_action) self.toolbar.addAction(self.undo_action)
@ -168,6 +180,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.toolbar.addAction(self.paste_action) self.toolbar.addAction(self.paste_action)
self.toolbar.addSeparator() self.toolbar.addSeparator()
self.toolbar.addAction(self.configure_action) self.toolbar.addAction(self.configure_action)
self.preview_toolbar.addAction(self.hide_chords_action)
self.retranslate_ui() self.retranslate_ui()
self.exit_action.triggered.connect(self.close) self.exit_action.triggered.connect(self.close)
@ -176,18 +189,21 @@ class MainWindow(QtWidgets.QMainWindow):
self.cut_action.triggered.connect(self.file_editor.cut) self.cut_action.triggered.connect(self.file_editor.cut)
self.copy_action.triggered.connect(self.file_editor.copy) self.copy_action.triggered.connect(self.file_editor.copy)
self.paste_action.triggered.connect(self.file_editor.paste) 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.open_action.triggered.connect(self.on_open_clicked)
self.save_action.triggered.connect(self.on_save_clicked) self.save_action.triggered.connect(self.on_save_clicked)
self.save_as_action.triggered.connect(self.on_save_as_clicked) self.save_as_action.triggered.connect(self.on_save_as_clicked)
self.export_action.triggered.connect(self.on_export_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.print_action.triggered.connect(self.on_print_clicked)
self.configure_action.triggered.connect(self.on_configure_clicked) self.configure_action.triggered.connect(self.on_configure_clicked)
self.file_editor.textChanged.connect(self.on_text_changed) self.file_editor.textChanged.connect(self.on_text_changed)
self.preview_view.page().pdfPrintingFinished.connect(self.on_pdf_finished) 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): def retranslate_ui(self):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
self.setWindowTitle(_translate('MainWindow', 'Ukatali')) self.setWindowTitle(_translate('MainWindow', 'Untitled') + '[*] - Ukatali')
self.file_menu.setTitle(_translate('MainWindow', '&File')) self.file_menu.setTitle(_translate('MainWindow', '&File'))
self.edit_menu.setTitle(_translate('MainWindow', '&Edit')) self.edit_menu.setTitle(_translate('MainWindow', '&Edit'))
self.settings_menu.setTitle(_translate('MainWindow', '&Settings')) self.settings_menu.setTitle(_translate('MainWindow', '&Settings'))
@ -209,9 +225,12 @@ class MainWindow(QtWidgets.QMainWindow):
self.print_action.setText(_translate('MainWindow', '&Print')) self.print_action.setText(_translate('MainWindow', '&Print'))
self.print_action.setToolTip(_translate('MainWindow', 'Print the current ChordPro file')) self.print_action.setToolTip(_translate('MainWindow', 'Print the current ChordPro file'))
self.print_action.setShortcut(_translate('MainWindow', 'Ctrl+P')) self.print_action.setShortcut(_translate('MainWindow', 'Ctrl+P'))
self.export_action.setText(_translate('MainWindow', '&Export...')) self.export_menu.setTitle(_translate('MainWindow', '&Export'))
self.export_action.setToolTip(_translate('MainWindow', 'Export the current file as a different format')) self.export_pdf_action.setText(_translate('MainWindow', 'Export to &PDF...'))
self.export_action.setShortcut(_translate('MainWindow', 'Ctrl+E')) 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.setText(_translate('MainWindow', '&Undo'))
self.undo_action.setToolTip(_translate('MainWindow', 'Undo the last edit')) self.undo_action.setToolTip(_translate('MainWindow', 'Undo the last edit'))
self.undo_action.setShortcut(_translate('MainWindow', 'Ctrl+Z')) self.undo_action.setShortcut(_translate('MainWindow', 'Ctrl+Z'))
@ -234,6 +253,20 @@ class MainWindow(QtWidgets.QMainWindow):
self.exit_action.setText(_translate('MainWindow', 'E&xit')) self.exit_action.setText(_translate('MainWindow', 'E&xit'))
self.exit_action.setToolTip(_translate('MainWindow', 'Quit Ukatali')) self.exit_action.setToolTip(_translate('MainWindow', 'Quit Ukatali'))
self.exit_action.setShortcut(_translate('MainWindow', 'Alt+F4')) 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): def on_open_clicked(self):
"""Open the file""" """Open the file"""
@ -252,6 +285,8 @@ class MainWindow(QtWidgets.QMainWindow):
if file_path.exists(): if file_path.exists():
self.settings.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()) self.file_editor.setText(file_path.open().read())
self.setWindowTitle('{}[*] - Ukatali'.format(file_path.name))
self.setWindowModified(False)
def on_save_clicked(self): def on_save_clicked(self):
"""Save the file""" """Save the file"""
@ -260,6 +295,8 @@ class MainWindow(QtWidgets.QMainWindow):
else: else:
with open(self.filename, 'w') as fd: with open(self.filename, 'w') as fd:
fd.write(self.file_editor.text()) fd.write(self.file_editor.text())
self.setWindowTitle('{}[*] - Ukatali'.format(Path(self.filename).name))
self.setWindowModified(False)
def on_save_as_clicked(self): def on_save_as_clicked(self):
"""Save the file""" """Save the file"""
@ -282,7 +319,7 @@ class MainWindow(QtWidgets.QMainWindow):
if self.configure_dialog.exec() == QtWidgets.QDialog.Accepted and self.file_editor.text(): if self.configure_dialog.exec() == QtWidgets.QDialog.Accepted and self.file_editor.text():
self.on_text_changed() self.on_text_changed()
def _get_render_options(self): def _get_render_options(self, is_lyrics_mode=False):
"""Get all the render options from the settings""" """Get all the render options from the settings"""
options = {} options = {}
self.settings.beginGroup('render') self.settings.beginGroup('render')
@ -296,27 +333,31 @@ class MainWindow(QtWidgets.QMainWindow):
elif details['type'] is bool: elif details['type'] is bool:
value = coerce_bool(value) value = coerce_bool(value)
options[name] = 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() self.settings.endGroup()
return options return options
def _render_song(self, extra_styles=None): def _render_song(self, extra_styles=None):
"""Render the current song and return the HTML""" """Render the current song and return the HTML"""
options = self._get_render_options() options = self._get_render_options(self.is_lyrics_mode)
text = self.file_editor.text() text = self.file_editor.text()
song = Song() song = Song()
try:
song.parse(text) 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) return render(song, options, extra_styles)
except Exception:
return None
def on_text_changed(self): def on_text_changed(self):
"""Update the preview when the text changes""" """Update the preview when the text changes"""
html = self._render_song() html = self._render_song()
if html:
self.preview_view.setHtml(html) self.preview_view.setHtml(html)
def on_export_clicked(self): def on_export_pdf_clicked(self):
"""Export the current song to PDF""" """Export the current song to PDF"""
if self.filename: if self.filename:
last_directory = str(Path(self.filename).with_suffix('.pdf')) last_directory = str(Path(self.filename).with_suffix('.pdf'))
@ -342,11 +383,42 @@ class MainWindow(QtWidgets.QMainWindow):
margin_top = float(self.settings.value('margin_top', 20)) margin_top = float(self.settings.value('margin_top', 20))
margin_bottom = float(self.settings.value('margin_bottom', 20)) margin_bottom = float(self.settings.value('margin_bottom', 20))
self.settings.endGroup() self.settings.endGroup()
margins = QtCore.QMarginsF(QtCore.QMargins(margin_left, margin_top, margin_right, margin_bottom)) margins = QtCore.QMarginsF(margin_left, margin_top, margin_right, margin_bottom)
page_layout = QtGui.QPageLayout(page_size, orientation, margins, unit) page_layout = QtGui.QPageLayout(page_size, orientation, margins, unit)
# Export to PDF # Export to PDF
self.preview_view.page().printToPdf(str(filename), page_layout) 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): def on_pdf_finished(self, filename, is_success):
"""A slot to notify the user when the PDF is done""" """A slot to notify the user when the PDF is done"""
if is_success: if is_success:
@ -355,16 +427,18 @@ class MainWindow(QtWidgets.QMainWindow):
else: else:
QtWidgets.QMessageBox.critical(self, 'Error Exporting to PDF', 'There was an error while exporting to PDF') QtWidgets.QMessageBox.critical(self, 'Error Exporting to PDF', 'There was an error while exporting to PDF')
def on_print_clicked(self): def on_print_finished(self, is_success):
"""Print the current song"""
def _on_print_finished(is_success):
print(is_success) print(is_success)
# print(QtPrintSupport.QPrinterInfo.defaultPrinterName()) def on_print_clicked(self):
# default_printer = QtPrintSupport.QPrinterInfo.defaultPrinter() """Print the current song"""
# print(default_printer) printer = QtPrintSupport.QPrinter(QtPrintSupport.QPrinter.HighResolution)
printer = QtPrintSupport.QPrinter()
print_dialog = QtPrintSupport.QPrintDialog(printer, self.preview_view.page().view()) print_dialog = QtPrintSupport.QPrintDialog(printer, self.preview_view.page().view())
if print_dialog.exec() == QtWidgets.QDialog.Accepted: if print_dialog.exec() == QtWidgets.QDialog.Accepted:
self.preview_view.page().print(printer, _on_print_finished) 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()