Commit 89101c12 authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 2.19.0+ds1

parent a46a9033
......@@ -2,6 +2,56 @@ ChangeLog of Frescobaldi, http://www.frescobaldi.org/
=====================================================
Changes in 2.19.0 -- April 22nd, 2016
* Requirement changes:
- Frescobaldi now requires python-ly 0.9.4
* New features:
- Tools->Quick Remove->Remove Fingerings
- Tools->Quick Remove->Remove Comments
- Tools->Pitch->Simplify Accidentals
- It is now configurable whether the document tabs have a close button
- The new LilyPond feature to embed source code files in the PDF (LilyPond >=
2.19.39) can be used in publish mode and the custom engrave dialog (#813)
- Clicking a TOC item in the Music View jumps to its destination (#803)
- When copying music to an image, a new option has been added to render the
image twice as large and scale it smoothly down, which improves images at
smaller DPI values.
- An option to keep the text cursor in the current line, when using the
horizontal arrow keys (off by default) (wish #779)
* Improvements:
- LilyPond 2.18+ \relative { ... } without start pitch is now supported
- It is possible to use no start pitch on abs->rel conversion and specify
the desired behaviour using two checkboxes in the tools->pitch menu.
- Clicking a point and click link in the Music View now remembers the previous
position
- Autocompile was not triggered in some circumstances.
Now it is also triggered:
* when a document is saved
* when undoing a change after a save (i.e. the undo would reset the
"modifified" flag of the document)
- When tapping a tempo in the Score Wizard, it is now configurable whether
a "common" metronome value is picked, or the exact tapped BPM (#792)
- Allow zooming to 800% in Music View (#800)
- When closing a document that has an engrave job running, the user is warned
and can choose whether to wait for the job to complete, to abort it, or to
cancel the closing.
- Tabbar and document list show in the document icon whether the last
engraving was successful (#636)
- Comment and Uncomment snippets are improved and now in the Snippet menu
- Score Wizard:
- add C-Melody Sax (#810)
* Bug fixes:
- fix #669 make click and drag working again
- fix #786 'Replace all' only works when run twice
- fix #793 Command autocompletion doesn’t work in figuremode
- fix #806 MIDI file not updated in MIDI Player when using "master" variable
- fix #807 search does not realize that content is changed
- fix #808 \figuremode should be enclosed within \new FiguredBass
* Translations:
- updated: Dutch, Italian
Changes in 2.18.2 -- December 26th, 2015
* Requirement changes:
......
......@@ -53,7 +53,7 @@ Required:
http://qt.nokia.com/
PyQt4 (>= 4.8.3):
http://www.riverbankcomputing.co.uk/software/pyqt/
python-ly (>= 0.9):
python-ly (>= 0.9.4):
https://pypi.python.org/pypi/python-ly
Poppler:
http://poppler.freedesktop.org/
......
include README* COPYING THANKS INSTALL TODO ChangeLog
include frescobaldi.desktop
include frescobaldi.1
include frescobaldi.png
include *.py
recursive-include frescobaldi_app README*
recursive-include frescobaldi_app *.png *.svg *.ico index.theme
recursive-include frescobaldi_app *.ly *.ily Makefile
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 *.sh *.png *.json *.diff
recursive-include windows *.bmp *.py
global-exclude *~
Metadata-Version: 1.1
Name: frescobaldi
Version: 2.18.2
Version: 2.19.0
Summary: LilyPond Music Editor
Home-page: http://www.frescobaldi.org/
Author: Wilbert Berendsen
......
Metadata-Version: 1.1
Name: frescobaldi
Version: 2.19.0
Summary: LilyPond Music Editor
Home-page: http://www.frescobaldi.org/
Author: Wilbert Berendsen
Author-email: info@frescobaldi.org
License: GPL
Description: Frescobaldi is an advanced text editor to edit LilyPond sheet music files. Features include an integrated PDF preview and a powerful Score Wizard.
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: MacOS X
Classifier: Environment :: Win32 (MS Windows)
Classifier: Environment :: X11 Applications :: Qt
Classifier: Intended Audience :: End Users/Desktop
Classifier: License :: OSI Approved :: GNU General Public License (GPL)
Classifier: Natural Language :: Chinese (Simplified)
Classifier: Natural Language :: Chinese (Traditional)
Classifier: Natural Language :: Czech
Classifier: Natural Language :: Dutch
Classifier: Natural Language :: English
Classifier: Natural Language :: French
Classifier: Natural Language :: Galician
Classifier: Natural Language :: German
Classifier: Natural Language :: Italian
Classifier: Natural Language :: Polish
Classifier: Natural Language :: Portuguese (Brazilian)
Classifier: Natural Language :: Russian
Classifier: Natural Language :: Spanish
Classifier: Natural Language :: Turkish
Classifier: Natural Language :: Ukranian
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Python :: 2
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.2
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Text Editors
This diff is collapsed.
python-ly
python-poppler-qt4
......@@ -110,7 +110,7 @@ def html():
version = _("Version {version}").format(version = appinfo.version)
description = _("A LilyPond Music Editor")
copyright = _("Copyright (c) {year} by {author}").format(
year = "2008-2015",
year = "2008-2016",
author = """<a href="mailto:{0}" title="{1}">{2}</a>""".format(
appinfo.maintainer_email,
_("Send an e-mail message to the maintainers."),
......
......@@ -26,7 +26,7 @@ from __future__ import unicode_literals
# these variables are also used by the distutils setup
name = "frescobaldi"
version = "2.18.2"
version = "2.19.0"
description = "LilyPond Music Editor"
long_description = \
"Frescobaldi is an advanced text editor to edit LilyPond sheet music files. " \
......
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
#
# Copyright (c) 2016 - 2016 by Wilbert Berendsen
#
# 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; either version 2
# of the License, or (at your option) any later version.
#
# 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# See http://www.gnu.org/licenses/ for more information.
"""
Simple eventfilter that alters the behaviour of the horizontal arrow keys
to not go to the next or previous block.
"""
from PyQt4.QtCore import QEvent, QObject, QSettings
from PyQt4.QtGui import QKeySequence
import app
def handle(edit, ev):
"""Return True if an horizontal arrow key press event was handled.
edit: Q(Plain)TextEdit instance
ev: the KeyPress QEvent
"""
if ev == QKeySequence.MoveToNextChar or ev == QKeySequence.SelectNextChar:
return edit.textCursor().atBlockEnd()
elif ev == QKeySequence.MoveToPreviousChar or ev == QKeySequence.SelectPreviousChar:
return edit.textCursor().atBlockStart()
return False
class Handler(QObject):
handle = False
def eventFilter(self, obj, ev):
if self.handle and ev.type() == QEvent.KeyPress:
return handle(obj, ev)
return False
handler = Handler()
def _setup():
handler.handle = QSettings().value("view_preferences/keep_cursor_in_line", False, bool)
app.settingsChanged.connect(_setup)
_setup()
......@@ -45,7 +45,7 @@ class Analyzer(object):
self.column = column = cursor.position() - block.position()
self.text = text = block.text()[:column]
self.model = None
# make a list of tokens exactly ending at the cursor position
# and let state follow
state = self.state = tokeniter.state(block)
......@@ -59,12 +59,12 @@ class Analyzer(object):
state.follow(t)
if t.end == column:
break
self.last = tokens[-1] if tokens else ''
self.lastpos = self.last.pos if self.last else column
parser = state.parser()
# Map the parser class to a group of tests to return the model.
# Try the tests until a model is returned.
try:
......@@ -80,31 +80,31 @@ class Analyzer(object):
def completions(self, cursor):
"""Analyzes text at cursor and returns a tuple (position, model).
The position is an integer specifying the column in the line where the last
text starts that should be completed.
The model list the possible completions. If the model is None, there are no
suitable completions.
This function does its best to return extremely meaningful completions
for the context the cursor is in.
"""
self.analyze(cursor)
return self.column, self.model
def document_cursor(self):
"""Return the current QTextCursor, to harvest info from its document.
By default this is simply the cursor given on analyze() or completions()
but you can override this method to provide another cursor. This can
be useful when the completion occurs in a small QTextDocument, which is
in fact a part of the main document.
"""
return self.cursor
def tokenclasses(self):
"""Return the list of classes of the tokens."""
return list(map(type, self.tokens))
......@@ -128,7 +128,7 @@ class Analyzer(object):
self.backuntil(lx.Space)
cursor = self.document_cursor()
return documentdata.doc(cursor.document()).bookcommands(cursor)
def bookpart(self):
"""\\bookpart {"""
self.backuntil(lx.Space)
......@@ -179,7 +179,7 @@ class Analyzer(object):
if '\\clef' in self.tokens[-4:-1]:
self.backuntil(lx.Space, lp.StringQuotedStart)
return completiondata.lilypond_clefs
def repeat(self):
"""complete \\repeat types"""
if '\\repeat' in self.tokens[-4:-1]:
......@@ -196,7 +196,8 @@ class Analyzer(object):
"""complete \\include """
if '\\include' in self.tokens[-4:-2]:
self.backuntil(lp.StringQuotedStart)
dir = self.last[:self.last.rfind(os.sep)] if os.sep in self.last else None
sep = '/' # Even on Windows, LilyPond uses the forward slash
dir = self.last[:self.last.rfind(sep)] if sep in self.last else None
cursor = self.document_cursor()
return documentdata.doc(cursor.document()).includenames(cursor, dir)
......@@ -213,7 +214,7 @@ class Analyzer(object):
self.column = self.lastpos
cursor = self.document_cursor()
return documentdata.doc(cursor.document()).lyriccommands(cursor)
def music_glyph(self):
r"""Complete \markup \musicglyph names."""
try:
......@@ -237,7 +238,7 @@ class Analyzer(object):
if self.last != '"':
self.column = self.lastpos
return completiondata.midi_instruments
def font_name(self):
"""Complete #'font-name = #"..."""
try:
......@@ -247,14 +248,14 @@ class Analyzer(object):
if self.last != '"':
self.column = self.lastpos
return completiondata.font_names()
def scheme_word(self):
"""Complete scheme word from scheme functions, etc."""
if isinstance(self.last, scm.Word):
self.column = self.lastpos
cursor = self.document_cursor()
return documentdata.doc(cursor.document()).schemewords()
def markup(self):
"""\\markup {"""
if self.last.startswith('\\'):
......@@ -269,7 +270,7 @@ class Analyzer(object):
self.column = self.lastpos + m.start()
cursor = self.document_cursor()
return documentdata.doc(cursor.document()).markup(cursor)
def markup_top(self):
"""\\markup ... in music or toplevel"""
if self.last.startswith('\\') and isinstance(self.last,
......@@ -277,7 +278,7 @@ class Analyzer(object):
self.column = self.lastpos
cursor = self.document_cursor()
return documentdata.doc(cursor.document()).markup(cursor)
def header(self):
"""\\header {"""
if '=' in self.tokens[-3:] or self.last.startswith('\\'):
......@@ -297,7 +298,7 @@ class Analyzer(object):
if self.last[:1].isalpha():
self.column = self.lastpos
return completiondata.lilypond_paper_variables
def layout(self):
"""\\layout {"""
self.backuntil(lx.Space)
......@@ -322,7 +323,7 @@ class Analyzer(object):
if cmd_in(self.tokens[-3:-1]):
self.backuntil(lx.Space)
return completiondata.lilypond_engravers
def context_variable_set(self):
if '=' in self.tokens[-4:]:
if isinstance(self.last, scm.Word):
......@@ -383,17 +384,17 @@ class Analyzer(object):
return completiondata.lilypond_grob_properties(self.tokens[i])
self.backuntil(lp.DotPath, lx.Space)
return completiondata.lilypond_grob_properties(self.tokens[i], False)
def revert(self):
"""test for \\revert in general music expressions
(because the revert parser drops out of invalid constructs, which happen
during typing).
"""
if '\\revert' in self.tokens:
return self.override()
def set_unset(self):
"""\\set and \\unset"""
tokenclasses = self.tokenclasses()
......@@ -429,7 +430,7 @@ class Analyzer(object):
self.column = self.lastpos
cursor = self.document_cursor()
return documentdata.doc(cursor.document()).schemewords()
def accidental_style(self):
"""test for \accidentalStyle"""
try:
......@@ -524,6 +525,29 @@ class Analyzer(object):
revert,
general_music,
),
lp.ParseDrumMode: (
markup_top,
tweak,
scheme_word,
key,
clef,
repeat,
hide_omit,
revert,
general_music,
),
lp.ParseFigureMode: (
markup_top,
tweak,
scheme_word,
key,
clef,
repeat,
accidental_style,
hide_omit,
revert,
general_music,
),
lp.ParseMarkup: (
markup,
),
......@@ -620,4 +644,3 @@ class Analyzer(object):
revert,
),
}
......@@ -45,6 +45,7 @@ markup = (
'markuplist',
'pageBreak',
'noPageBreak',
'noPageTurn',
)
# these can occur (almost) everywhere
......@@ -69,6 +70,7 @@ inputmodes = (
# commands that only occur at the global file level
toplevel = (
'defineBarLine',
'language',
'version',
'sourcefileline',
......
......@@ -161,6 +161,10 @@ class DocumentDataSource(plugin.DocumentPlugin):
if not f.endswith('init.ly')
and f.islower()))
# forward slashes on Windows (issue #804)
if os.name == "nt":
names = [name.replace('\\', '/') for name in names]
return listmodel.ListModel(names)
......
......@@ -39,6 +39,7 @@ class Document(QTextDocument):
urlChanged = signals.Signal() # new url, old url
closed = signals.Signal()
loaded = signals.Signal()
saving = signals.SignalContext()
saved = signals.Signal()
@classmethod
......@@ -158,7 +159,7 @@ class Document(QTextDocument):
# would fail
if self.url().isEmpty() and not url.isEmpty():
self.setUrl(url)
with app.documentSaving(self):
with self.saving(), app.documentSaving(self):
with open(filename, "wb") as f:
f.write(self.encodedText())
f.flush()
......
......@@ -51,11 +51,13 @@ class DocumentActions(plugin.MainWindowPlugin):
ac.tools_reformat.triggered.connect(self.reFormat)
ac.tools_remove_trailing_whitespace.triggered.connect(self.removeTrailingWhitespace)
ac.tools_convert_ly.triggered.connect(self.convertLy)
ac.tools_quick_remove_comments.triggered.connect(self.quickRemoveComments)
ac.tools_quick_remove_articulations.triggered.connect(self.quickRemoveArticulations)
ac.tools_quick_remove_ornaments.triggered.connect(self.quickRemoveOrnaments)
ac.tools_quick_remove_instrument_scripts.triggered.connect(self.quickRemoveInstrumentScripts)
ac.tools_quick_remove_slurs.triggered.connect(self.quickRemoveSlurs)
ac.tools_quick_remove_dynamics.triggered.connect(self.quickRemoveDynamics)
ac.tools_quick_remove_fingerings.triggered.connect(self.quickRemoveFingerings)
ac.tools_quick_remove_markup.triggered.connect(self.quickRemoveMarkup)
mainwindow.currentDocumentChanged.connect(self.updateDocActions)
......@@ -71,11 +73,13 @@ class DocumentActions(plugin.MainWindowPlugin):
def updateSelectionActions(self, selection):
self.actionCollection.edit_cut_assign.setEnabled(selection)
self.actionCollection.tools_quick_remove_comments.setEnabled(selection)
self.actionCollection.tools_quick_remove_articulations.setEnabled(selection)
self.actionCollection.tools_quick_remove_ornaments.setEnabled(selection)
self.actionCollection.tools_quick_remove_instrument_scripts.setEnabled(selection)
self.actionCollection.tools_quick_remove_slurs.setEnabled(selection)
self.actionCollection.tools_quick_remove_dynamics.setEnabled(selection)
self.actionCollection.tools_quick_remove_fingerings.setEnabled(selection)
self.actionCollection.tools_quick_remove_markup.setEnabled(selection)
def currentView(self):
......@@ -130,6 +134,10 @@ class DocumentActions(plugin.MainWindowPlugin):
import convert_ly
convert_ly.convert(self.mainwindow())
def quickRemoveComments(self):
import quickremove
quickremove.comments(self.mainwindow().textCursor())
def quickRemoveArticulations(self):
import quickremove
quickremove.articulations(self.mainwindow().textCursor())
......@@ -150,6 +158,10 @@ class DocumentActions(plugin.MainWindowPlugin):
import quickremove
quickremove.dynamics(self.mainwindow().textCursor())
def quickRemoveFingerings(self):
import quickremove
quickremove.fingerings(self.mainwindow().textCursor())
def quickRemoveMarkup(self):
import quickremove
quickremove.markup(self.mainwindow().textCursor())
......@@ -168,11 +180,13 @@ class Actions(actioncollection.ActionCollection):
self.tools_reformat = QAction(parent)
self.tools_remove_trailing_whitespace = QAction(parent)
self.tools_convert_ly = QAction(parent)
self.tools_quick_remove_comments = QAction(parent)
self.tools_quick_remove_articulations = QAction(parent)
self.tools_quick_remove_ornaments = QAction(parent)
self.tools_quick_remove_instrument_scripts = QAction(parent)
self.tools_quick_remove_slurs = QAction(parent)
self.tools_quick_remove_dynamics = QAction(parent)
self.tools_quick_remove_fingerings = QAction(parent)
self.tools_quick_remove_markup = QAction(parent)
self.edit_cut_assign.setIcon(icons.get('edit-cut'))
......@@ -189,10 +203,12 @@ class Actions(actioncollection.ActionCollection):
self.tools_reformat.setText(_("&Format"))
self.tools_remove_trailing_whitespace.setText(_("Remove Trailing &Whitespace"))
self.tools_convert_ly.setText(_("&Update with convert-ly..."))
self.tools_quick_remove_comments.setText(_("Remove &Comments"))
self.tools_quick_remove_articulations.setText(_("Remove &Articulations"))
self.tools_quick_remove_ornaments.setText(_("Remove &Ornaments"))
self.tools_quick_remove_instrument_scripts.setText(_("Remove &Instrument Scripts"))
self.tools_quick_remove_slurs.setText(_("Remove &Slurs"))
self.tools_quick_remove_dynamics.setText(_("Remove &Dynamics"))
self.tools_quick_remove_fingerings.setText(_("Remove &Fingerings"))
self.tools_quick_remove_markup.setText(_("Remove Text &Markup (from music)"))
......@@ -63,6 +63,10 @@ class DocumentIconProvider(plugin.DocumentPlugin):
icon = 'pushpin'
elif doc.isModified():
icon = 'document-save'
elif job and not job.is_running() and not job.is_aborted() and job.success:
icon = 'document-compile-success'
elif job and not job.is_running() and not job.is_aborted():
icon = 'document-compile-failed'
else:
icon = 'text-plain'
return icons.get(icon)
......
......@@ -69,6 +69,7 @@ class Engraver(plugin.MainWindowPlugin):
app.jobFinished.connect(self.openDefaultView)
app.sessionChanged.connect(self.slotSessionChanged)
app.saveSessionData.connect(self.slotSaveSessionData)
app.documentClosed.connect(self.slotDocumentClosed)
mainwindow.aboutToClose.connect(self.saveSettings)
self.loadSettings()
app.languageChanged.connect(self.updateStickyActionText)
......@@ -184,7 +185,7 @@ class Engraver(plugin.MainWindowPlugin):
if mode == 'preview':
args = ['-dpoint-and-click']
elif mode == 'publish':
args = ['-dno-point-and-click']
args = None # command.defaultJob() will handle publish mode
elif mode == 'layout-control':
args = panelmanager.manager(
self.mainwindow()).layoutcontrol.widget().preview_options()
......@@ -213,6 +214,30 @@ class Engraver(plugin.MainWindowPlugin):
except IOError:
pass ## saving was not possible (e.g. happens when read only)
def queryCloseDocument(self, doc):
"""Return True whether a document can be closed.
When no job is running, True is immediately returned.
When a job is running, the user is asked whether to abort the job (not
for autocompile ("hidden") jobs).
"""
job = jobmanager.job(doc)
if not job or not job.is_running() or jobattributes.get(job).hidden:
return True
msgbox = QMessageBox(QMessageBox.Warning,
_("Warning"),
_("An engrave job is running for the document \"{name}\".\n"
"Do you want to abort the running job?").format(name=doc.documentName()),
QMessageBox.Abort | QMessageBox.Cancel,
self.mainwindow())
abort_button = msgbox.button(QMessageBox.Abort)
signal = lambda: abort_button.click()
job.done.connect(signal)
msgbox.exec_()
job.done.disconnect(signal)
return msgbox.clickedButton() == abort_button
def runJob(self, job, document):
"""Runs the engraving job on behalf of document."""
jobattributes.get(job).mainwindow = self.mainwindow()
......@@ -280,6 +305,12 @@ class Engraver(plugin.MainWindowPlugin):
from . import lytools
lytools.show_available_fonts(self.mainwindow(), info)
def slotDocumentClosed(self, doc):
"""Called when the user closes a document. Aborts a running Job."""
job = jobmanager.job(doc)
if job and job.is_running():
job.abort()
def slotSessionChanged(self):
"""Called when the session is changed."""
import sessions
......
......@@ -33,6 +33,8 @@ The log is not displayed.
from __future__ import unicode_literals
import contextlib
from PyQt4.QtCore import QSettings, Qt, QTimer
import app
......@@ -77,9 +79,11 @@ class AutoCompiler(plugin.MainWindowPlugin):
if old:
old.contentsChanged.disconnect(self.startTimer)
old.loaded.disconnect(self.startTimer)
old.saved.disconnect(self.startTimer)
if new:
new.contentsChanged.connect(self.startTimer)
new.loaded.connect(self.startTimer)
new.saved.connect(self.startTimer)
if self._enabled:
self.startTimer()
......@@ -115,7 +119,7 @@ class AutoCompiler(plugin.MainWindowPlugin):
class AutoCompileManager(plugin.DocumentPlugin):
def __init__(self, document):
document.contentsChanged.connect(self.slotDocumentContentsChanged, Qt.QueuedConnection)
document.saved.connect(self.slotDocumentSaved)
document.saving.connect(self.slotDocumentSaving)
document.loaded.connect(self.initialize)
jobmanager.manager(document).started.connect(self.slotJobStarted)
self.initialize()
......@@ -155,13 +159,24 @@ class AutoCompileManager(plugin.DocumentPlugin):
def slotDocumentContentsChanged(self):
"""Called when the user modifies the document."""
if self.document().isModified(): # not when a template was applied
doc = self.document()
if doc.isModified() or doc.isRedoAvailable(): # not when a template was applied
self._dirty = True
def slotDocumentSaved(self):
"""Called when the document is saved. Forces auto-compile once."""
self._dirty = True
self._hash = None
@contextlib.contextmanager
def slotDocumentSaving(self):
"""Called while the document is being saved.
Forces auto-compile once if the document was modified before saving.
"""
modified = self.document().isModified()
try:
yield
finally:
if modified:
self._dirty = True
self._hash = None