Commit f884afa9 authored by SVN-Git Migration's avatar SVN-Git Migration

Imported Upstream version 3.0.0+ds1

parent 2845dc0d
*.pyc
*~
build/
dist/
......@@ -2,17 +2,44 @@ ChangeLog of Frescobaldi, http://www.frescobaldi.org/
=====================================================
Changes in 2.20.0 -- Mai ??, 2016
Changes in 3.0.0 -- February 17th, 2017
* Requirement changes:
- Frescobaldi now requires Python3.2+, Qt5, PyQt5, python-poppler-qt5
* New features:
- Zoom with pinch gesture in Music View, contributed by David Rydh
- An option (enabled by default) to move the cursor to the end of the line
when PageDown is pressed on the last line, and to move the cursor to the
start of the first line if PageUp is pressed on the first line
* Improvements:
- Retina display support in Music View, contributed by David Rydh
Changes in 2.20.0 -- February 17th, 2017
* New features:
- New Manuscript viewer tool, displaying an "engraver's copy",
contributed by Peter Bjuhr and Urs Liska
- Copy selected text in Music View
- New command Edit->Move to include file...
- New quick remove actions to remove beams and ligatures from selected music
- Search tool in the keyboard shortcuts preferences page (#690)
* Improvements:
- Fit Width in Music View now fits two pages in width, if in two-page mode
- the Music View now remembers the page layout mode
- Jump to next or previous bookmark now respects surrounding lines setting
- Better default save path, looking at last edited document (#162)
* Bug fixes:
- fix #716 position of open document tab bar changes on engrave
- Midi input fixes by David Rydh:
* fix #797 and #853, now honour Midi input port setting
* in Midi input, ces and bis now have the correct octave
* fix interruption of Midi input by other events than note events
- Midi input now uses correct channel, fix by David Kastrup
- fix #857 UnicodeDecodeError on some types of \displayMusic command output
- fix #891 QTextBlock not hashable anymore
- fix #862 midi not loaded on first document load
* Translations:
- new Swedish translation contributed by Dag Odenhall
- updated Dutch by Wilbert Berendsen
......
Metadata-Version: 1.1
Name: frescobaldi
Version: 3.0.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 :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Text Editors
Metadata-Version: 1.1
Name: frescobaldi
Version: 3.0.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 :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Text Editors
This diff is collapsed.
python-ly
python-poppler-qt5
......@@ -109,7 +109,7 @@ def html():
version = _("Version {version}").format(version = appinfo.version)
description = _("A LilyPond Music Editor")
copyright = _("Copyright (c) {year} by {author}").format(
year = "2008-2016",
year = "2008-2017",
author = """<a href="mailto:{0}" title="{1}">{2}</a>""".format(
appinfo.maintainer_email,
_("Send an e-mail message to the maintainers."),
......
......@@ -136,7 +136,7 @@ def move_to_include_file(cursor, parent_widget=None):
message = _("Could not write to: {url}").format(url=filename),
strerror = e.strerror,
errno = e.errno)
QMessageBox.critical(self, app.caption(_("Error")), msg)
QMessageBox.critical(parent_widget, app.caption(_("Error")), msg)
return
filename = os.path.relpath(filename, dirname)
command = '\\include "{0}"\n'.format(filename)
......
......@@ -18,7 +18,11 @@
# See http://www.gnu.org/licenses/ for more information.
"""
A Frescobaldi (LilyPond) document.
A Frescobaldi document.
This contains the text the user can edit in Frescobaldi. In most cases it will
be a LilyPond source file, but other file types can be used as well.
"""
......@@ -197,7 +201,8 @@ class Document(QTextDocument):
self._encoding = encoding
def encodedText(self):
"""Returns the text of the document encoded in the correct encoding.
"""Return the text of the document as a bytes string encoded in the
correct encoding.
The line separator is '\\n' on Unix/Linux/Mac OS X, '\\r\\n' on Windows.
......@@ -208,7 +213,12 @@ class Document(QTextDocument):
return util.encode(text, self.encoding())
def documentName(self):
""" Returns a suitable name for this document. """
"""Return a suitable name for this document.
This is only to be used for display. If the url of the document is
empty, something like "Untitled" or "Untitled (3)" is returned.
"""
if self._url.isEmpty():
if self._num == 1:
return _("Untitled")
......
......@@ -57,6 +57,8 @@ class DocumentActions(plugin.MainWindowPlugin):
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_beams.triggered.connect(self.quickRemoveBeams)
ac.tools_quick_remove_ligatures.triggered.connect(self.quickRemoveLigatures)
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)
......@@ -80,6 +82,8 @@ class DocumentActions(plugin.MainWindowPlugin):
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_beams.setEnabled(selection)
self.actionCollection.tools_quick_remove_ligatures.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)
......@@ -160,6 +164,14 @@ class DocumentActions(plugin.MainWindowPlugin):
import quickremove
quickremove.slurs(self.mainwindow().textCursor())
def quickRemoveBeams(self):
import quickremove
quickremove.beams(self.mainwindow().textCursor())
def quickRemoveLigatures(self):
import quickremove
quickremove.ligatures(self.mainwindow().textCursor())
def quickRemoveDynamics(self):
import quickremove
quickremove.dynamics(self.mainwindow().textCursor())
......@@ -192,6 +204,8 @@ class Actions(actioncollection.ActionCollection):
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_beams = QAction(parent)
self.tools_quick_remove_ligatures = QAction(parent)
self.tools_quick_remove_dynamics = QAction(parent)
self.tools_quick_remove_fingerings = QAction(parent)
self.tools_quick_remove_markup = QAction(parent)
......@@ -217,6 +231,8 @@ class Actions(actioncollection.ActionCollection):
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_beams.setText(_("Remove &Beams"))
self.tools_quick_remove_ligatures.setText(_("Remove &Ligatures"))
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)"))
......
......@@ -80,7 +80,7 @@ class Engraver(plugin.MainWindowPlugin):
doc = self.stickyDocument()
if not doc:
doc = self.mainwindow().currentDocument()
if not doc.url().isEmpty():
if doc and not doc.url().isEmpty():
master = variables.get(doc, "master")
if master:
url = doc.url().resolved(QUrl(master))
......
......@@ -62,7 +62,7 @@ class FileExport(plugin.MainWindowPlugin):
return False # cancelled
import ly.musicxml
writer = ly.musicxml.writer()
writer.parse_text(doc.toPlainText())
writer.parse_text(doc.toPlainText(), orgname)
xml = writer.musicxml()
# put the Frescobaldi version in the xml file
software = xml.root.find('.//encoding/software')
......
......@@ -27,7 +27,7 @@
integration of the functionality into LilyPond proper.
Then the following code has to be run inside LilyPond
when a -ddebug-layout command line option is present.
For an inclusion in LilyPond the strategy has to be modified:
Currently all options have to be explicitly activated.
This is because we have the checkbox UI in Frescobaldi.
......@@ -38,44 +38,55 @@
command line options or #(ly:set-option ...) commands.
%}
\include "./lilypond-version-predicates.ily"
debugLayoutOptions =
#(define-void-function (parser location)()
;; include the optional custom file first.
;; This way it can for example define configuration variables.
(if (ly:get-option 'debug-custom-file)
;; Add a custom file for debugging layout
(ly:parser-include-string parser
(format "\\include \"~A\"\n" (ly:get-option 'debug-custom-file))))
;; include preview options depending on the
;; presence or absence of command line switches
(if (ly:get-option 'debug-control-points)
;; display control points
(ly:parser-include-string parser "\\include \"display-control-points.ily\""))
(if (ly:get-option 'debug-voices)
;; color \voiceXXX music
(ly:parser-include-string parser "\\include \"color-voices.ily\""))
(if (ly:get-option 'debug-directions)
;; color grobs switched with \xxxUp or \xxxDown
(ly:parser-include-string parser "\\include \"color-directions.ily\""))
(if (ly:get-option 'debug-grob-anchors)
;; Add a dot for the anchor of each grob
(ly:parser-include-string parser "\\include \"display-grob-anchors.ily\""))
(if (ly:get-option 'debug-grob-names)
;; Add a dot for the anchor of each grob
(ly:parser-include-string parser "\\include \"display-grob-names.ily\""))
(if (ly:get-option 'debug-paper-columns)
;; Add a dot for the anchor of each grob
(ly:parser-include-string parser "\\include \"info-paper-columns.ily\""))
(if (ly:get-option 'debug-display-skylines)
;; display skylines
;; -> this is very intrusive, so handle with care!
;; should be switched off by default
(ly:set-option 'debug-skylines #t))
;; the option should be named debug-skylines,
;; this name clash has to be resolved!
(if (ly:get-option 'debug-annotate-spacing)
;; Add a dot for the anchor of each grob
(ly:parser-include-string parser "\\include \"annotate-spacing.ily\"")))
(let*
((lily-version-new (lilypond-greater-than? '(2 19 21)))
(cond-inc
;; conditionally choose the syntax to include a file
;; based on the current LilyPond version
(lambda (filename)
(if lily-version-new
(ly:parser-include-string (format "\\include \"./~a\"" filename))
(ly:parser-include-string parser (format "\\include \"./~a\"" filename))))))
;; include the optional custom file first.
;; This way it can for example define configuration variables.
(if (ly:get-option 'debug-custom-file)
;; Add a custom file for debugging layout
(cond-inc (ly:get-option 'debug-custom-file)))
;; include preview options depending on the
;; presence or absence of command line switches
(if (ly:get-option 'debug-control-points)
;; display control points
(cond-inc "display-control-points.ily"))
(if (ly:get-option 'debug-voices)
;; color \voiceXXX music
(cond-inc "color-voices.ily"))
(if (ly:get-option 'debug-directions)
;; color grobs switched with \xxxUp or \xxxDown
(cond-inc "color-directions.ily"))
(if (ly:get-option 'debug-grob-anchors)
;; Add a dot for the anchor of each grob
(cond-inc "display-grob-anchors.ily"))
(if (ly:get-option 'debug-grob-names)
;; Add a dot for the anchor of each grob
(cond-inc "display-grob-names.ily"))
(if (ly:get-option 'debug-paper-columns)
;; Add a dot for the anchor of each grob
(cond-inc "info-paper-columns.ily"))
(if (ly:get-option 'debug-display-skylines)
;; display skylines
;; -> this is very intrusive, so handle with care!
;; should be switched off by default
(ly:set-option 'debug-skylines #t))
;; the option should be named debug-skylines,
;; this name clash has to be resolved!
(if (ly:get-option 'debug-annotate-spacing)
;; Add a dot for the anchor of each grob
(cond-inc "annotate-spacing.ily"))))
\debugLayoutOptions
......@@ -84,7 +95,7 @@ debugLayoutOptions =
initialisation phase, at a point
when any (ly:set-option ...) commands in the input file(s) have
been already parsed.
#(if (ly:get-option 'debug-layout)
;; check for additional command line options
#{
......
......@@ -127,7 +127,10 @@ class LogWidget(log.Log):
self._errors.append((pos, self.cursor.position(), url))
else:
if type == job.STDOUT:
message = message.encode('latin1').decode('utf-8')
# we use backslashreplace because LilyPond sometimes seems to write
# incorrect utf-8 to standard output in \displayMusic, \displayScheme
# functions etc.
message = message.encode('latin1').decode('utf-8', 'backslashreplace')
super(LogWidget, self).writeMessage(message, type)
def slotAnchorClicked(self, url):
......
......@@ -495,9 +495,10 @@ class MainWindow(QMainWindow):
def openDocument(self):
""" Displays an open dialog to open one or more documents. """
if app.documents:
ext = os.path.splitext(self.currentDocument().url().path())[1]
directory = os.path.dirname(self.currentDocument().url().toLocalFile()) or app.basedir()
d = self.currentDocument()
if d:
ext = os.path.splitext(d.url().path())[1]
directory = os.path.dirname(d.url().toLocalFile()) or app.basedir()
else:
ext = ".ly"
directory = app.basedir()
......@@ -521,7 +522,14 @@ class MainWindow(QMainWindow):
if filename:
filetypes = app.filetypes(os.path.splitext(filename)[1])
else:
directory = app.basedir() # default directory to save to
# find a suitable directory to save to
for d in self.historyManager.documents()[1::]:
if d.url().toLocalFile():
directory = os.path.dirname(d.url().toLocalFile())
break
else:
directory = app.basedir() # default directory to save to
import documentinfo
import ly.lex
filename = os.path.join(directory, documentinfo.defaultfilename(doc))
......
......@@ -388,6 +388,8 @@ def menu_tools_quick_remove(mainwindow):
m.addAction(ac.tools_quick_remove_ornaments)
m.addAction(ac.tools_quick_remove_instrument_scripts)
m.addAction(ac.tools_quick_remove_slurs)
m.addAction(ac.tools_quick_remove_beams)
m.addAction(ac.tools_quick_remove_ligatures)
m.addAction(ac.tools_quick_remove_dynamics)
m.addAction(ac.tools_quick_remove_fingerings)
m.addAction(ac.tools_quick_remove_markup)
......
......@@ -83,7 +83,8 @@ class MidiIn(object):
def noteevent(self, notetype, channel, notenumber, value):
targetchannel = self.widget().channel()
if channel == targetchannel or targetchannel == 0: # '0' captures all
if targetchannel == 0 or channel == targetchannel-1: # '0' captures all
# midi channels start at 1 for humans and 0 for programs
if notetype == 9 and value > 0: # note on with velocity > 0
notemapping = elements.NoteMapping(self.widget().keysignature(), self.widget().accidentals()=='sharps')
note = elements.Note(notenumber, notemapping)
......
......@@ -29,6 +29,7 @@ from PyQt5.QtWidgets import (
import app
import css
import qutil
import listmodel
import midihub
import gadgets.drag
......@@ -86,6 +87,7 @@ class Widget(QWidget):
self._player.stateChanged.connect(self.slotPlayerStateChanged)
self.slotPlayerStateChanged(False)
dockwidget.mainwindow().currentDocumentChanged.connect(self.loadResults)
app.documentLoaded.connect(self.slotDocumentLoaded)
app.jobFinished.connect(self.slotUpdatedFiles)
app.aboutToQuit.connect(self.stop)
midihub.aboutToRestart.connect(self.slotAboutToRestart)
......@@ -208,6 +210,18 @@ class Widget(QWidget):
if not self._timeSlider.isSliderDown():
self._display.setTime(time)
def slotDocumentLoaded(self, document):
"""Called when a new document is loaded.
Only calls slotUpdatedFiles when this is the first document, as that
slot will be called anyway when the current document is switched. When
the first document is loaded, it is loaded into the existing empty
document, so mainwindow.currentDocumentChanged() will never be emitted.
"""
if len(app.documents) == 1:
self.slotUpdatedFiles(document)
def slotUpdatedFiles(self, document, job=None):
"""Called when there are new MIDI files."""
mainwindow = self.parentWidget().mainwindow()
......@@ -250,7 +264,7 @@ class Widget(QWidget):
def slotDocumentClosed(self, document):
if document == self._document:
self._document = None
self._fileSelector.clear()
self._fileSelector.setModel(listmodel.ListModel([]))
self._player.stop()
self._player.clear()
self.updateTimeSlider()
......
......@@ -117,7 +117,15 @@ class MusicViewPanel(panel.Panel):
ac.music_reload.triggered.connect(self.reloadView)
self.actionCollection.music_sync_cursor.setChecked(
QSettings().value("musicview/sync_cursor", False, bool))
mode = QSettings().value("muziekview/layoutmode", "single", str)
if mode == "double_left":
ac.music_two_pages_first_left.setChecked(True)
elif mode == "double_right":
ac.music_two_pages_first_right.setChecked(True)
else: # mode == "single":
ac.music_single_pages.setChecked(True)
def translateUI(self):
self.setWindowTitle(_("window title", "Music View"))
self.toggleViewAction().setText(_("&Music View"))
......@@ -128,8 +136,18 @@ class MusicViewPanel(panel.Panel):
w.zoomChanged.connect(self.slotMusicZoomChanged)
w.updateZoomInfo()
w.view.surface().selectionChanged.connect(self.updateSelection)
w.view.surface().pageLayout().setPagesPerRow(1) # default to single
w.view.surface().pageLayout().setPagesFirstRow(0) # pages
# read layout mode setting before using the widget
layout = w.view.surface().pageLayout()
if self.actionCollection.music_two_pages_first_right.isChecked():
layout.setPagesPerRow(2)
layout.setPagesFirstRow(1)
elif self.actionCollection.music_two_pages_first_left.isChecked():
layout.setPagesPerRow(2)
layout.setPagesFirstRow(0)
else: # "single"
layout.setPagesPerRow(1) # default to single
layout.setPagesFirstRow(0) # pages
import qpopplerview.pager
self._pager = p = qpopplerview.pager.Pager(w.view)
......@@ -150,6 +168,31 @@ class MusicViewPanel(panel.Panel):
QTimer.singleShot(0, open)
return w
def setPageLayoutMode(self, mode):
"""Change the page layout and store the setting as well.
The mode is "single", "double_left" or "double_right".
"single": a vertical row of single pages
"double_left": two pages besides each other, first page is a left page
"double_right": two pages, first page is a right page.
"""
layout = self.widget().view.surface().pageLayout()
if mode == "double_right":
layout.setPagesPerRow(2)
layout.setPagesFirstRow(1)
elif mode == "double_left":
layout.setPagesPerRow(2)
layout.setPagesFirstRow(0)
elif mode == "single":
layout.setPagesPerRow(1)
layout.setPagesFirstRow(0)
else:
raise ValueError("wrong mode value")
QSettings().setValue("muziekview/layoutmode", mode)
layout.update()
def updateSelection(self, rect):
self.actionCollection.music_copy_image.setEnabled(bool(rect))
self.actionCollection.music_copy_text.setEnabled(bool(rect))
......@@ -234,24 +277,15 @@ class MusicViewPanel(panel.Panel):
@activate
def viewSinglePages(self):
layout = self.widget().view.surface().pageLayout()
layout.setPagesPerRow(1)
layout.setPagesFirstRow(0)
layout.update()
self.setPageLayoutMode("single")
@activate
def viewTwoPagesFirstRight(self):
layout = self.widget().view.surface().pageLayout()
layout.setPagesPerRow(2)
layout.setPagesFirstRow(1)
layout.update()
self.setPageLayoutMode("double_right")
@activate
def viewTwoPagesFirstLeft(self):
layout = self.widget().view.surface().pageLayout()
layout.setPagesPerRow(2)
layout.setPagesFirstRow(0)
layout.update()
self.setPageLayoutMode("double_left")
@activate
def jumpToCursor(self):
......
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
No preview for this file type
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -38,6 +38,7 @@ import preferences
from widgets.shortcuteditdialog import ShortcutEditDialog
from widgets.schemeselector import SchemeSelector
from widgets.lineedit import LineEdit
_lastaction = '' # last selected action name (saved during running but not on exit)
......@@ -52,6 +53,9 @@ class Shortcuts(preferences.Page):
self.scheme = SchemeSelector(self)
layout.addWidget(self.scheme)
self.searchEntry = LineEdit()
self.searchEntry.setPlaceholderText(_("Search..."))
layout.addWidget(self.searchEntry)
self.tree = QTreeWidget(self)
self.tree.setHeaderLabels([_("Command"), _("Shortcut")])
self.tree.setRootIsDecorated(False)
......@@ -64,6 +68,7 @@ class Shortcuts(preferences.Page):
layout.addWidget(self.edit)
# signals
self.searchEntry.textChanged.connect(self.updateFilter)
self.scheme.currentChanged.connect(self.slotSchemeChanged)
self.scheme.changed.connect(self.changed)
self.tree.currentItemChanged.connect(self.slotCurrentItemChanged)
......@@ -253,7 +258,33 @@ class Shortcuts(preferences.Page):
item.setShortcuts(shortcuts, scheme)
self.changed.emit()
def updateFilter(self):
"""Called when the search text changes."""
search = self.searchEntry.text()
scheme = self.scheme.currentScheme()
def hidechildren(item):
hideparent = True
for n in range(item.childCount()):
c = item.child(n)
if c.childCount():
# submenu item
if hidechildren(c):
c.setHidden(True)
else:
c.setHidden(False)
c.setExpanded(True)
hideparent = False
elif isinstance(c, ShortcutItem):
# shortcut item, should be the case
if c.matches(scheme, search):
c.setHidden(False)
hideparent = False
else:
c.setHidden(True)
return hideparent
hidechildren(self.tree.invisibleRootItem())
class ShortcutItem(QTreeWidgetItem):
def __init__(self, action, collection, name):
QTreeWidgetItem.__init__(self)
......@@ -335,5 +366,19 @@ class ShortcutItem(QTreeWidgetItem):
text += " " + _("(default)")
self.setText(1, text)
def matches(self, scheme, text):
"""Return True if the text matches our description or shortcuts.
Shortcuts are checked in the specified scheme.
The match is case insensitive.
"""
text = text.lower()
if text in self.text(0).lower():
return True
for shortcut in self.shortcuts(scheme):
if text in shortcut.toString(QKeySequence.NativeText).lower():
return True