Commit 56e5df21 authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 2.17.1+ds1

parent 31f8e650
......@@ -2,6 +2,62 @@ ChangeLog of Frescobaldi, http://www.frescobaldi.org/
=====================================================
Changes in 2.17.1 -- December 26th, 2014
* Bugfixes:
- on quit, respect cancel (issue #531)
Changes in 2.17 -- December 26th, 2014
* New features:
- Preference for the number of contextual lines to show at least, when the
text view is scrolled to a cursor position (e.g. by clicking on a link, or
when jumping between search results. Wish: issue #488)
- Session import/export, contributed by Peter Bjuhr
- A session can have its own list of include paths, that can be used either
instead of or in addition to the global list of include paths, contributed
by Peter Bjuhr
- Relative mode for MIDI input, contributed by Alex Schreiber (pull request
#521)
* Improvements:
- When saving a new file for the first time, a default filename is provided,
based on composer and title of the score (wish: issue #472)
- Printing the music PDF now honors the duplex settings
- MusicXML export has improved by using the ly.music module that has a notion
of the music events and time/duration of those events. Although ly.music is
authored and maintained by Wilbert Berendsen, the MusicXML export module is
contributed by Peter Bjuhr
- workaround a LilyPond buglet in point-and-click highlighting of quoted
strings: although the textedit uri points to the closing quote, the string
is correctly highlighted
- dont disable run button of Engrave Custom dialog for autocompile jobs
- let autocompile jobs finish before starting a new one (comments issue #120)
- dont always run autocompile job for the first opened document
- after a successful compile the default music viewer (pdf or svg) is
activated. This can be suppressed in the LilyPond prefs (wish: issue #435)
- the music view does not switch documents anymore when a compile is finished
but the user is working on a different document (wish: issue #513)
- tooltips for the panel title and float/close buttons
- error handling in I/O-related operations has improved
- Ctrl+C in editor view does not copy HTML, only plain text (issue #517)
- Features that are in development can be enabled by checking Preferences→
General→Experimental Features→Enable Experimental Features. Some features
that are not completely finished are hidden by default but become visible
when this preference is enabled. New features are enabled after a restart,
or when a new main window is created using Window→New.
* Bugfixes:
- fix AttributeError: 'bool' object has no attribute 'endswith' in
lilypondinfo.py, toolcommand()
- fix AttributeError: 'NoneType' object has no attribute '_register_cursor'
(cause in music.py, get_included_document_node())
* Installation:
- Python 2.7 is now required.
* Translations:
- updated: nl, pt_BR, fr, it, de
- new (partial): Chinese Traditional, Simplified and Hong Kong by Anthony Fok
Changes in 2.0.16 -- June 9th, 2014
* Translations:
......
......@@ -35,7 +35,7 @@ See the distutils documentation for more install options.
Dependencies:
=============
Frescobaldi 2.0 is written in Python version 2.6 or 2.7 (3.x support is planned)
Frescobaldi 2.0 is written in Python version 2.7 (3.x support is planned)
and depends on Qt4.7 and PyQt4.8, and uses the python-poppler-qt4 binding to
Poppler for the built-in PDF preview.
......@@ -45,7 +45,7 @@ module from pygame; or, as a last resort, embedding the PortMidi C-library via
ctypes. MIDI is optional.
Required:
Python (2.6 or 2.7):
Python 2.7
http://www.python.org/
Qt4 (>= 4.7):
http://qt.nokia.com/
......@@ -80,9 +80,19 @@ To see the usage notes, run:
python macosx/mac-app.py -h
The application bundle will be created inside a 'dist' folder in the current
working directory. The application bundle is NOT self-contained.
working directory.
The script can build both a non-standalone system-dependent launcher and an
**almost** standalone self-contained application bundle (the script will print
instructions on the further steps needed to get a **fully** standalone
self-contained application bundle).
To use the script you need argparse (included in Python >= 2.7) and py2app.
A macosx/build-dmg.sh script is provided to build the **fully** standalone
application bundle and wrap it in a distributable DMG disk image along with
the README, ChangeLog and COPYING files.
The script assumes a specific system configuration (for details run the script
with the '-h' option), but can be easily adapted to other configurations.
For Linux distribution packagers:
=================================
......
......@@ -11,5 +11,5 @@ recursive-include frescobaldi_app *.pot *.po *.mo
recursive-include frescobaldi_app *.dic
recursive-include frescobaldi_app *.js
recursive-include frescobaldi_app *.md
recursive-include macosx *.svg *.icns *.py *.strings
recursive-include macosx *.svg *.icns *.py *.strings *.sh *.png *.json *.diff
global-exclude *~
......@@ -25,16 +25,10 @@ To test features or to experiment, you can run Frescobaldi in an interactive
Python shell. It is recommended to open a shell (e.g. by simply running python
without arguments, or by using Dreampie or IPython) and then enter:
import frescobaldi_app.main
This ensures the application starts up and uses the correct SIP api (2) for
QString and QVariant.
Alternatively, you can enter:
from frescobaldi_app.debug import *
This will also start up Frescobaldi and then install some handlers that print
This ensures the application starts up and uses the correct SIP api (2) for
QString and QVariant. This will also install some handlers that print
debugging information on certain events. And it imports the most used modules.
Currently Python 2.6 and 2.7 are supported, but the code should be designed such
......
......@@ -46,6 +46,7 @@ includes = [
'pypm',
'__future__',
'argparse',
'bisect',
'contextlib',
'difflib',
......
#!/usr/bin/python
import sys
import frescobaldi_app.main
from frescobaldi_app import toplevel
toplevel.install()
import main
import app
sys.excepthook = app.excepthook
app.instantiate() # Construct QApplication object
main.main() # Parse command line, create windows etc
sys.excepthook = app.excepthook # Show Python errors in a bugreport window
sys.exit(app.run())
......@@ -44,8 +44,8 @@ def action(collection_name, action_name):
May return None, if the named collection or action does not exist.
"""
mgr = ActionCollectionManager.instances()[0]
return mgr.action(collection_name, action_name)
for mgr in ActionCollectionManager.instances():
return mgr.action(collection_name, action_name)
class ActionCollectionManager(plugin.MainWindowPlugin):
......
......@@ -31,18 +31,15 @@ from PyQt4.QtGui import QApplication
import info
qApp = QApplication([os.path.abspath(sys.argv[0])] + sys.argv[1:])
QApplication.setApplicationName(info.name)
QApplication.setApplicationVersion(info.version)
QApplication.setOrganizationName(info.name)
QApplication.setOrganizationDomain(info.domain)
qApp = None # instantiate() puts the QApplication obj. here
windows = []
documents = []
from signals import Signal, SignalContext
# signals
appInstantiated = Signal() # Called when the QApplication is instantiated
appStarted = Signal() # Called when the main event loop is entered
aboutToQuit = Signal() # Use this and not qApp.aboutToQuit
mainwindowCreated = Signal() # MainWindow
mainwindowClosed = Signal() # MainWindow
......@@ -79,12 +76,12 @@ def openUrl(url, encoding=None):
and not documents[0].isUndoAvailable()
and not documents[0].isRedoAvailable()):
d = documents[0]
d.setUrl(url)
d.setEncoding(encoding)
d.load()
if not url.isEmpty():
d.load(url)
else:
import document
d = document.Document(url, encoding)
d = document.Document.new_from_url(url, encoding)
return d
def findDocument(url):
......@@ -98,8 +95,35 @@ def findDocument(url):
if url == d.url():
return d
def instantiate():
"""Instantiate the global QApplication object."""
global qApp
qApp = QApplication([os.path.abspath(sys.argv[0])] + sys.argv[1:])
QApplication.setApplicationName(info.name)
QApplication.setApplicationVersion(info.version)
QApplication.setOrganizationName(info.name)
QApplication.setOrganizationDomain(info.domain)
appInstantiated()
def oninit(func):
"""Call specified function on QApplication instantiation.
If the QApplication alreay has been instantiated, the function is called
directly.
As this function returns the specified function, you can use this as a
decorator.
"""
if qApp:
func()
else:
appInstantiated.connect(func)
return func
def run():
"""Enter the Qt event loop."""
"""Emit the appStarted signal and enter the Qt event loop."""
appStarted()
result = qApp.exec_()
aboutToQuit()
return result
......
......@@ -23,14 +23,15 @@ Updates a document using convert-ly.
from __future__ import unicode_literals
import difflib
import textwrap
import os
import subprocess
from PyQt4.QtCore import QSettings, QSize
from PyQt4.QtGui import (
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QGridLayout, QLabel,
QLineEdit, QTabWidget, QTextBrowser, QVBoxLayout)
QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFont,
QGridLayout, QLabel, QLineEdit, QTabWidget, QTextBrowser, QVBoxLayout)
import app
import util
......@@ -71,6 +72,7 @@ class Dialog(QDialog):
self._text = ''
self._convertedtext = ''
self._encoding = None
self.mainwindow = parent
self.fromVersionLabel = QLabel()
self.fromVersion = QLineEdit()
......@@ -80,19 +82,22 @@ class Dialog(QDialog):
self.lilyChooser = lilychooser.LilyChooser()
self.messages = QTextBrowser()
self.diff = QTextBrowser(lineWrapMode=QTextBrowser.NoWrap)
self.uni_diff = QTextBrowser(lineWrapMode=QTextBrowser.NoWrap)
self.copyCheck = QCheckBox(checked=
QSettings().value('convert_ly/copy_messages', True, bool))
self.tabw = QTabWidget()
self.tabw.addTab(self.messages, '')
self.tabw.addTab(self.diff, '')
self.tabw.addTab(self.uni_diff, '')
self.buttons = QDialogButtonBox(
QDialogButtonBox.Reset |
QDialogButtonBox.Reset | QDialogButtonBox.Save |
QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
self.buttons.accepted.connect(self.accept)
self.buttons.button(QDialogButtonBox.Ok).clicked .connect(self.accept)
self.buttons.rejected.connect(self.reject)
self.buttons.button(QDialogButtonBox.Reset).clicked.connect(self.run)
self.buttons.button(QDialogButtonBox.Save).clicked.connect(self.saveFile)
layout = QVBoxLayout()
self.setLayout(layout)
......@@ -128,7 +133,9 @@ class Dialog(QDialog):
"comment to the end of the document."))
self.tabw.setTabText(0, _("&Messages"))
self.tabw.setTabText(1, _("&Changes"))
self.tabw.setTabText(2, _("&Diff"))
self.buttons.button(QDialogButtonBox.Reset).setText(_("Run Again"))
self.buttons.button(QDialogButtonBox.Save).setText(_("Save as file"))
self.setCaption()
def saveCopyCheckSetting(self):
......@@ -137,6 +144,9 @@ class Dialog(QDialog):
def readSettings(self):
font = textformats.formatData('editor').font
self.diff.setFont(font)
diffFont = QFont("Monospace")
diffFont.setStyleHint(QFont.TypeWriter)
self.uni_diff.setFont(diffFont)
def slotLilyPondVersionChanged(self):
self.setLilyPondInfo(self.lilyChooser.lilyPondInfo())
......@@ -151,6 +161,7 @@ class Dialog(QDialog):
self.setCaption()
self.toVersion.setText(info.versionString())
self.setConvertedText()
self.setDiffText()
self.messages.clear()
def setConvertedText(self, text=''):
......@@ -163,6 +174,16 @@ class Dialog(QDialog):
wrapcolumn=100))
else:
self.diff.clear()
def setDiffText(self, text=''):
if text:
difflist = list(difflib.unified_diff(
self._text.split('\n'), text.split('\n'),
_("Current Document"), _("Converted Document")))
diffHLstr = self.diffHighl(difflist)
self.uni_diff.setHtml(diffHLstr)
else:
self.uni_diff.clear()
def convertedText(self):
return self._convertedtext or ''
......@@ -177,6 +198,7 @@ class Dialog(QDialog):
self._text = doc.toPlainText()
self._encoding = doc.encoding() or 'UTF-8'
self.setConvertedText()
self.setDiffText()
def run(self):
"""Runs convert-ly (again)."""
......@@ -217,7 +239,54 @@ class Dialog(QDialog):
return
self.messages.setPlainText(err.decode('UTF-8'))
self.setConvertedText(out.decode('UTF-8'))
self.setDiffText(out.decode('UTF-8'))
if not out or self._convertedtext == self._text:
self.messages.append('\n' + _("The document has not been changed."))
def saveFile(self):
"""Save content in tab as file"""
tabdata = self.getTabData(self.tabw.currentIndex())
doc = self.mainwindow.currentDocument()
orgname = doc.url().toLocalFile()
filename = os.path.splitext(orgname)[0] + '['+tabdata.filename+']'+'.'+tabdata.ext
caption = app.caption(_("dialog title", "Save File"))
filetypes = '{0} (*.txt);;{1} (*.htm);;{2} (*)'.format(_("Text Files"), _("HTML Files"), _("All Files"))
filename = QFileDialog.getSaveFileName(self.mainwindow, caption, filename, filetypes)
if not filename:
return False # cancelled
f = open(filename, 'w')
f.write(tabdata.text.encode('utf-8'))
f.close()
def getTabData(self, index):
"""Get content of current tab from current index"""
if index == 0:
return FileInfo('message', 'txt', self.messages.toPlainText())
elif index == 1:
return FileInfo('html-diff', 'html', self.diff.toHtml())
elif index == 2:
return FileInfo('uni-diff', 'diff', self.uni_diff.toPlainText())
def diffHighl(self, difflist):
"""Return highlighted version of input."""
result = []
for l in difflist:
if l.startswith('-'):
s = '<span style="color: red; white-space: pre-wrap;">'
elif l.startswith('+'):
s = '<span style="color: green; white-space: pre-wrap;">'
else:
s = '<span style="white-space: pre-wrap;">'
h = l.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
result.append(s + h + '</span>')
return '<br>'.join(result)
class FileInfo():
"""Holds information useful for the file saving"""
def __init__(self, filename, ext, text):
self.filename = filename
self.ext = ext
self.text = text
......@@ -11,18 +11,12 @@ from __future__ import unicode_literals
import sys
try:
from . import main
del main
except (ImportError, ValueError):
pass # this was a reload()
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from . import toplevel
toplevel.install()
import main
import app
import document
def doc_repr(self):
......@@ -63,3 +57,18 @@ def modules():
"""Print the list of loaded modules."""
print('\n'.join(v.__name__ for k, v in sorted(sys.modules.items()) if v is not None))
# avoid builtins._ being overwritten
sys.displayhook = app.displayhook
# instantiate app and create a mainwindow, etc
app.instantiate()
main.main()
# be friendly and import Qt stuff
from PyQt4.QtCore import *
from PyQt4.QtGui import *
......@@ -41,18 +41,62 @@ class Document(QTextDocument):
loaded = signals.Signal()
saved = signals.Signal()
@classmethod
def load_data(cls, url, encoding=None):
"""Class method to load document contents from an url.
This is intended to open a document without instantiating one
if loading the contents fails.
This method returns the text contents of the url as decoded text,
thus a unicode string.
"""
filename = url.toLocalFile()
# currently, we do not support non-local files
if not filename:
raise IOError("not a local file")
with open(filename) as f:
data = f.read()
return util.decode(data, encoding)
@classmethod
def new_from_url(cls, url, encoding=None):
"""Create and return a new document, loaded from url.
This is intended to open a new Document without instantiating one
if loading the contents fails.
"""
if not url.isEmpty():
text = cls.load_data(url, encoding)
d = cls(url, encoding)
if not url.isEmpty():
d.setPlainText(text)
d.setModified(False)
d.loaded()
app.documentLoaded(d)
return d
def __init__(self, url=None, encoding=None):
"""Create a new Document with url and encoding.
Does not load the contents, you should use load() for that, or
use the new_from_url() constructor to instantiate a new Document
with the contents loaded.
"""
if url is None:
url = QUrl()
super(Document, self).__init__()
self.setDocumentLayout(QPlainTextDocumentLayout(self))
self._encoding = encoding
if url is None:
url = QUrl()
self._url = url # avoid urlChanged on init
self.setUrl(url)
self.modificationChanged.connect(self.slotModificationChanged)
app.documents.append(self)
app.documentCreated(self)
self.load()
def slotModificationChanged(self):
app.documentModificationChanged(self)
......@@ -62,57 +106,65 @@ class Document(QTextDocument):
app.documentClosed(self)
app.documents.remove(self)
def load(self, keepUndo=False):
"""Loads the current url.
def load(self, url=None, encoding=None, keepUndo=False):
"""Load the specified or current url (if None was specified).
Currently only local files are supported. An IOError is raised
when trying to load a nonlocal URL.
Returns True if loading succeeded, False if an error occurred,
and None when the current url is empty or non-local.
Currently only local files are supported.
If loading succeeds and an url was specified, the url is make the
current url (by calling setUrl() internally).
If keepUndo is True, the loading can be undone (with Ctrl-Z).
"""
fileName = self.url().toLocalFile()
if fileName:
try:
with open(fileName) as f:
data = f.read()
except (IOError, OSError):
return False # errors are caught in MainWindow.openUrl()
text = util.decode(data)
if keepUndo:
c = QTextCursor(self)
c.select(QTextCursor.Document)
c.insertText(text)
else:
self.setPlainText(text)
self.setModified(False)
self.loaded()
app.documentLoaded(self)
return True
if url is None:
url = QUrl()
u = url if not url.isEmpty() else self.url()
text = self.load_data(u, encoding or self._encoding)
if keepUndo:
c = QTextCursor(self)
c.select(QTextCursor.Document)
c.insertText(text)
else:
self.setPlainText(text)
self.setModified(False)
if not url.isEmpty():
self.setUrl(url)
self.loaded()
app.documentLoaded(self)
def save(self):
"""Saves the document to the current url.
def save(self, url=None, encoding=None):
"""Saves the document to the specified or current url.
Currently only local files are supported. An IOError is raised
when trying to save a nonlocal URL.
Returns True if saving succeeded, False if an error occurred,
and None when the current url is empty or non-local.
Currently only local files are supported.
If saving succeeds and an url was specified, the url is made the
current url (by calling setUrl() internally).
"""
if url is None:
url = QUrl()
u = url if not url.isEmpty() else self.url()
filename = u.toLocalFile()
# currently, we do not support non-local files
if not filename:
raise IOError("not a local file")
# keep the url if specified when we didn't have one, even if saving
# would fail
if self.url().isEmpty() and not url.isEmpty():
self.setUrl(url)
with app.documentSaving(self):
fileName = self.url().toLocalFile()
if fileName:
try:
with open(fileName, "w") as f:
f.write(self.encodedText())
f.flush()
os.fsync(f.fileno())
except (IOError, OSError):
return False
self.setModified(False)
self.saved()
app.documentSaved(self)
return True
with open(filename, "w") as f:
f.write(self.encodedText())
f.flush()
os.fsync(f.fileno())
self.setModified(False)
if not url.isEmpty():
self.setUrl(url)
self.saved()
app.documentSaved(self)
def url(self):
return self._url
......
......@@ -64,6 +64,40 @@ def mode(document, guess=True):
"""Returns the type of the given document. See DocumentInfo.mode()."""
return info(document).mode(guess)
def defaultfilename(document):
"""Return a default filename that could be used for the document.
The name is based on the score's title, composer etc.
"""
i = info(document)
m = i.music()
import ly.music.items as mus
# which fields (in order) to harvest:
fields = ('composer', 'title')
d = {}
for h in m.find(mus.Header):
for a in h.find(mus.Assignment):
for f in fields:
if f not in d and a.name() == f:
n = a.value()
if n:
t = n.plaintext()
if t:
d[f] = t
# make filenames
for k in d:
d[k] = re.sub(r'\W+', '-', d[k]).strip('-')
filename = '-'.join(d[k] for k in fields if k in d)
if not filename:
filename = document.documentName()
ext = ly.lex.extensions[i.mode()]
return filename + ext
class DocumentInfo(plugin.DocumentPlugin):
"""Computes and caches various information about a Document."""
......@@ -113,11 +147,36 @@ class DocumentInfo(plugin.DocumentPlugin):
return self.lydocinfo().mode()
def includepath(self):
"""Returns the configured include path. Currently the document does not matter."""
"""Return the configured include path.
A path is a list of directories.
If there is a session specific include path, it is used.
Otherwise the path is taken from the LilyPond preferences.
Currently the document does not matter.
"""
# get the global include path
try:
include_path = QSettings().value("lilypond_settings/include_path", [], type(""))
except TypeError:
include_path = []
# get the session specific include path
import sessions
session_settings = sessions.currentSessionGroup()
if session_settings and session_settings.value("set-paths", False, bool):
try:
sess_path = session_settings.value("include-path", [], type(""))
except TypeError:
sess_path = []
if session_settings.value("repl-paths", False, bool):
include_path = sess_path
else:
include_path = sess_path + include_path