1
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
3
# Copyright (c) 2008 - 2011 by Wilbert Berendsen
5
# This program is free software; you can redistribute it and/or
6
# modify it under the terms of the GNU General Public License
7
# as published by the Free Software Foundation; either version 2
8
# of the License, or (at your option) any later version.
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# See http://www.gnu.org/licenses/ for more information.
21
The PDF preview panel widget.
24
from __future__ import unicode_literals
30
from PyQt4.QtCore import *
31
from PyQt4.QtGui import *
46
import viewhighlighter
47
import ly.lex.lilypond
49
from . import pointandclick
52
class MusicView(QWidget):
53
"""Widget containing the qpopplerview.View."""
55
zoomChanged = pyqtSignal(int, float) # mode, scale
57
def __init__(self, dockwidget):
58
"""Creates the Music View for the dockwidget."""
59
super(MusicView, self).__init__(dockwidget)
61
self._positions = weakref.WeakKeyDictionary()
62
self._currentDocument = None
65
self._highlightFormat = QTextCharFormat()
66
self._highlightMusicFormat = Highlighter()
67
self._highlightRange = None
68
self._highlightTimer = QTimer(singleShot=True, interval= 250, timeout=self.updateHighlighting)
69
self._highlightRemoveTimer = QTimer(singleShot=True, timeout=self.clearHighlighting)
71
layout = QVBoxLayout()
72
layout.setContentsMargins(0, 0, 0, 0)
73
self.setLayout(layout)
75
self.view = popplerview.View(self)
76
layout.addWidget(self.view)
77
app.settingsChanged.connect(self.readSettings)
79
self.view.setViewMode(qpopplerview.FitWidth)
80
self.view.surface().linkClicked.connect(self.slotLinkClicked)
81
self.view.surface().linkHovered.connect(self.slotLinkHovered)
82
self.view.surface().linkLeft.connect(self.slotLinkLeft)
83
self.view.surface().setShowUrlTips(False)
84
self.view.surface().linkHelpRequested.connect(self.slotLinkHelpRequested)
86
self.view.viewModeChanged.connect(self.updateZoomInfo)
87
self.view.surface().pageLayout().scaleChanged.connect(self.updateZoomInfo)
88
self.view.setContextMenuPolicy(Qt.CustomContextMenu)
89
self.view.customContextMenuRequested.connect(self.showContextMenu)
91
# react if cursor of current text document moves
92
dockwidget.mainwindow().currentViewChanged.connect(self.slotCurrentViewChanged)
93
view = dockwidget.mainwindow().currentView()
95
self.slotCurrentViewChanged(view)
98
"""Returns the initial size the PDF (Music) View prefers."""
99
return self.parent().mainwindow().size() / 2
101
def updateZoomInfo(self):
102
"""Called when zoom and viewmode of the qpopplerview change, emit zoomChanged."""
103
self.zoomChanged.emit(self.view.viewMode(), self.view.surface().pageLayout().scale())
105
def openDocument(self, doc):
106
"""Opens a documents.Document instance."""
108
self._currentDocument = doc
109
document = doc.document()
111
self._links = pointandclick.links(document)
112
self.view.load(document)
113
position = self._positions.get(doc, (0, 0, 0))
114
self.view.setPosition(position)
117
"""Empties the view."""
118
cur = self._currentDocument
120
self._positions[cur] = self.view.position()
121
self._currentDocument = None
123
self._highlightRange = None
124
self._highlightTimer.stop()
127
def readSettings(self):
128
"""Reads the settings from the user's preferences."""
129
# background and highlight colors of music view
130
colors = textformats.formatData('editor').baseColors
131
self._highlightMusicFormat.setColor(colors['musichighlight'])
132
color = colors['selectionbackground']
134
self._highlightFormat.setBackground(color)
136
def slotLinkClicked(self, ev, page, link):
137
"""Called when the use clicks a link.
139
If the links is a textedit link, opens the document and puts the cursor there.
140
Otherwise, call the helpers module to open the destination.
143
cursor = self._links.cursor(link, True)
145
self.parent().mainwindow().setTextCursor(cursor, findOpenView=True)
146
elif ev.button() != Qt.RightButton and isinstance(link, popplerqt4.Poppler.LinkBrowse):
147
helpers.openUrl(QUrl(link.url()))
149
def slotLinkHovered(self, page, link):
150
"""Called when the mouse hovers a link.
152
If the links points to the current editor document, the token(s) it points
153
at are highlighted using a transparent selection color.
155
The highlight shows for a few seconds but disappears when the mouse moves
156
off the link or when the link is clicked.
159
self.view.surface().highlight(self._highlightMusicFormat,
160
[(page, link.linkArea().normalized())], 2000)
161
self._highlightRange = None
162
cursor = self._links.cursor(link)
163
if not cursor or cursor.document() != self.parent().mainwindow().currentDocument():
166
# highlight token(s) at this cursor
167
source = tokeniter.Source.fromCursor(cursor, True)
168
for token in source.tokens:
173
cur = source.cursor(token, end=0)
176
# some heuristic to find the relevant range(s) the linked grob represents
177
if isinstance(token, ly.lex.lilypond.Direction):
178
# a _, - or ^ is found; find the next token
180
if not isinstance(token, (ly.lex.Space, ly.lex.Comment)):
182
end = token.end + source.block.position()
183
if token == '\\markup':
184
# find the end of the markup expression
185
depth = source.state.depth()
187
if source.state.depth() < depth:
188
end = token.end + source.block.position()
191
# find the end of the string
193
if isinstance(token, ly.lex.StringEnd):
194
end = token.end + source.block.position()
196
elif isinstance(token, ly.lex.MatchStart):
197
# find the end of slur, beam. ligature, phrasing slur, etc.
198
name = token.matchname
201
if isinstance(token, ly.lex.MatchEnd) and token.matchname == name:
204
cursors.append(source.cursor(token))
206
elif isinstance(token, ly.lex.MatchStart) and token.matchname == name:
209
cur.setPosition(end, QTextCursor.KeepAnchor)
211
view = self.parent().mainwindow().currentView()
212
viewhighlighter.highlighter(view).highlight(self._highlightFormat, cursors, 2, 5000)
214
def slotLinkLeft(self):
215
"""Called when the mouse moves off a previously highlighted link."""
216
self.clearHighlighting()
217
view = self.parent().mainwindow().currentView()
218
viewhighlighter.highlighter(view).clear(self._highlightFormat)
220
def slotLinkHelpRequested(self, pos, page, link):
221
"""Called when a ToolTip wants to appear above the hovered link."""
222
if isinstance(link, popplerqt4.Poppler.LinkBrowse):
223
cursor = self._links.cursor(link)
225
from . import tooltip
226
text = tooltip.text(cursor)
228
m = pointandclick.textedit_match(link.url())
230
filename, line, column = pointandclick.readurl(m)
231
text = "{0} ({1}:{2})".format(os.path.basename(filename), line, column)
234
QToolTip.showText(pos, text, self.view.surface(), page.linkRect(link.linkArea()))
236
def slotCurrentViewChanged(self, view, old=None):
237
self.view.surface().clearHighlight(self._highlightMusicFormat)
239
old.cursorPositionChanged.disconnect(self.slotCursorPositionChanged)
240
view.cursorPositionChanged.connect(self.slotCursorPositionChanged)
242
def slotCursorPositionChanged(self):
243
"""Called when the user moves the text cursor."""
244
if not self.isVisible() or not self._links:
245
return # not visible of no PDF in the viewer
247
view = self.parent().mainwindow().currentView()
248
links = self._links.boundLinks(view.document())
250
return # the PDF contains no references to the current text document
252
s = links.indices(view.textCursor())
254
self.clearHighlighting()
256
self.highlight(links.destinations(), s)
258
def highlight(self, destinations, slice, msec=None):
259
"""(Internal) Highlights the from the specified destinations the specified slice."""
260
count = slice.stop - slice.start
262
msec = 5000 if count > 1 else 2000 # show selections longer
263
self._highlightRemoveTimer.start(msec)
264
if self._highlightRange == slice:
265
return # don't redraw if same
266
self._highlightRange = slice
267
self._destinations = destinations[slice]
269
self._highlightTimer.start()
271
self._highlightTimer.stop()
272
self.updateHighlighting()
274
def updateHighlighting(self):
275
"""Really orders the view's surface to draw the highlighting."""
276
layout = self.view.surface().pageLayout()
277
areas = [(layout[pageNum], rect)
278
for dest in self._destinations
279
for pageNum, rect in dest]
280
self.view.surface().highlight(self._highlightMusicFormat, areas)
282
def clearHighlighting(self):
283
"""Called on timeout of the _highlightRemoveTimer."""
284
self._highlightRange = None
285
self.view.surface().clearHighlight(self._highlightMusicFormat)
287
def showCurrentLinks(self):
288
"""Scrolls the view if necessary to show objects at current text cursor."""
290
return # no PDF in viewer
292
view = self.parent().mainwindow().currentView()
293
links = self._links.boundLinks(view.document())
295
return # the PDF contains no references to the current text document
297
s = links.indices(view.textCursor())
300
layout = self.view.surface().pageLayout()
302
for dest in links.destinations()[s]:
303
for pageNum, r in dest:
304
rect = rect.united(layout[pageNum].linkRect(r.normalized()))
305
# not larger than viewport
306
rect.setSize(rect.size().boundedTo(self.view.viewport().size()))
307
self.view.center(rect.center())
308
self.highlight(links.destinations(), s, 10000)
310
def showContextMenu(self):
311
"""Called when the user right-clicks or presses the context menu key."""
312
pos = self.view.mapToGlobal(QPoint(0, 0))
313
link, cursor = None, None
315
if self.view.mapFromGlobal(QCursor.pos()) in self.view.viewport().rect():
317
pos_in_surface = self.view.surface().mapFromGlobal(pos)
318
page, link = self.view.surface().pageLayout().linkAt(pos_in_surface)
320
cursor = self._links.cursor(link)
321
from . import contextmenu
322
contextmenu.show(pos, self.parent(), link, cursor)
325
class Highlighter(qpopplerview.Highlighter):
326
"""Simple version of qpopplerview.Highlighter that has the color settable.
328
You must set a color before using the Highlighter.
331
def setColor(self, color):
332
"""Sets the color to use to draw highlighting rectangles."""
336
"""Returns the color set using the setColor method."""