~ubuntu-branches/ubuntu/vivid/frescobaldi/vivid

« back to all changes in this revision

Viewing changes to frescobaldi_app/musicview/widget.py

  • Committer: Package Import Robot
  • Author(s): Ryan Kavanagh
  • Date: 2012-01-03 16:20:11 UTC
  • mfrom: (1.4.1)
  • Revision ID: package-import@ubuntu.com-20120103162011-tsjkwl4sntwmprea
Tags: 2.0.0-1
* New upstream release 
* Drop the following uneeded patches:
  + 01_checkmodules_no_python-kde4_build-dep.diff
  + 02_no_pyc.diff
  + 04_no_binary_lilypond_upgrades.diff
* Needs new dependency python-poppler-qt4
* Update debian/watch for new download path
* Update copyright file with new holders and years

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# This file is part of the Frescobaldi project, http://www.frescobaldi.org/
 
2
#
 
3
# Copyright (c) 2008 - 2011 by Wilbert Berendsen
 
4
#
 
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.
 
9
#
 
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.
 
14
#
 
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.
 
19
 
 
20
"""
 
21
The PDF preview panel widget.
 
22
"""
 
23
 
 
24
from __future__ import unicode_literals
 
25
 
 
26
import itertools
 
27
import os
 
28
import weakref
 
29
 
 
30
from PyQt4.QtCore import *
 
31
from PyQt4.QtGui import *
 
32
 
 
33
try:
 
34
    import popplerqt4
 
35
except ImportError:
 
36
    pass
 
37
 
 
38
import qpopplerview
 
39
import popplerview
 
40
 
 
41
import app
 
42
import icons
 
43
import helpers
 
44
import textformats
 
45
import tokeniter
 
46
import viewhighlighter
 
47
import ly.lex.lilypond
 
48
 
 
49
from . import pointandclick
 
50
 
 
51
 
 
52
class MusicView(QWidget):
 
53
    """Widget containing the qpopplerview.View."""
 
54
    
 
55
    zoomChanged = pyqtSignal(int, float) # mode, scale
 
56
    
 
57
    def __init__(self, dockwidget):
 
58
        """Creates the Music View for the dockwidget."""
 
59
        super(MusicView, self).__init__(dockwidget)
 
60
        
 
61
        self._positions = weakref.WeakKeyDictionary()
 
62
        self._currentDocument = None
 
63
        self._links = None
 
64
        
 
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)
 
70
        
 
71
        layout = QVBoxLayout()
 
72
        layout.setContentsMargins(0, 0, 0, 0)
 
73
        self.setLayout(layout)
 
74
        
 
75
        self.view = popplerview.View(self)
 
76
        layout.addWidget(self.view)
 
77
        app.settingsChanged.connect(self.readSettings)
 
78
        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)
 
85
        
 
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)
 
90
        
 
91
        # react if cursor of current text document moves
 
92
        dockwidget.mainwindow().currentViewChanged.connect(self.slotCurrentViewChanged)
 
93
        view = dockwidget.mainwindow().currentView()
 
94
        if view:
 
95
            self.slotCurrentViewChanged(view)
 
96
 
 
97
    def sizeHint(self):
 
98
        """Returns the initial size the PDF (Music) View prefers."""
 
99
        return self.parent().mainwindow().size() / 2
 
100
    
 
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())
 
104
        
 
105
    def openDocument(self, doc):
 
106
        """Opens a documents.Document instance."""
 
107
        self.clear()
 
108
        self._currentDocument = doc
 
109
        document = doc.document()
 
110
        if 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)
 
115
 
 
116
    def clear(self):
 
117
        """Empties the view."""
 
118
        cur = self._currentDocument
 
119
        if cur:
 
120
            self._positions[cur] = self.view.position()
 
121
        self._currentDocument = None
 
122
        self._links = None
 
123
        self._highlightRange = None
 
124
        self._highlightTimer.stop()
 
125
        self.view.clear()
 
126
        
 
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']
 
133
        color.setAlpha(128)
 
134
        self._highlightFormat.setBackground(color)
 
135
 
 
136
    def slotLinkClicked(self, ev, page, link):
 
137
        """Called when the use clicks a link.
 
138
        
 
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.
 
141
        
 
142
        """
 
143
        cursor = self._links.cursor(link, True)
 
144
        if cursor:
 
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()))
 
148
 
 
149
    def slotLinkHovered(self, page, link):
 
150
        """Called when the mouse hovers a link.
 
151
        
 
152
        If the links points to the current editor document, the token(s) it points
 
153
        at are highlighted using a transparent selection color.
 
154
        
 
155
        The highlight shows for a few seconds but disappears when the mouse moves
 
156
        off the link or when the link is clicked.
 
157
        
 
158
        """
 
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():
 
164
            return
 
165
        
 
166
        # highlight token(s) at this cursor
 
167
        source = tokeniter.Source.fromCursor(cursor, True)
 
168
        for token in source.tokens:
 
169
            break
 
170
        else:
 
171
            return
 
172
        
 
173
        cur = source.cursor(token, end=0)
 
174
        cursors = [cur]
 
175
        
 
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
 
179
            for token in source:
 
180
                if not isinstance(token, (ly.lex.Space, ly.lex.Comment)):
 
181
                    break
 
182
        end = token.end + source.block.position()
 
183
        if token == '\\markup':
 
184
            # find the end of the markup expression
 
185
            depth = source.state.depth()
 
186
            for token in source:
 
187
                if source.state.depth() < depth:
 
188
                    end = token.end + source.block.position()
 
189
                    break
 
190
        elif token == '"':
 
191
            # find the end of the string
 
192
            for token in source:
 
193
                if isinstance(token, ly.lex.StringEnd):
 
194
                    end = token.end + source.block.position()
 
195
                    break
 
196
        elif isinstance(token, ly.lex.MatchStart):
 
197
            # find the end of slur, beam. ligature, phrasing slur, etc.
 
198
            name = token.matchname
 
199
            nest = 1
 
200
            for token in source:
 
201
                if isinstance(token, ly.lex.MatchEnd) and token.matchname == name:
 
202
                    nest -= 1
 
203
                    if nest == 0:
 
204
                        cursors.append(source.cursor(token))
 
205
                        break
 
206
                elif isinstance(token, ly.lex.MatchStart) and token.matchname == name:
 
207
                    nest += 1
 
208
                    
 
209
        cur.setPosition(end, QTextCursor.KeepAnchor)
 
210
        
 
211
        view = self.parent().mainwindow().currentView()
 
212
        viewhighlighter.highlighter(view).highlight(self._highlightFormat, cursors, 2, 5000)
 
213
    
 
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)
 
219
 
 
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)
 
224
            if cursor:
 
225
                from . import tooltip
 
226
                text = tooltip.text(cursor)
 
227
            else:
 
228
                m = pointandclick.textedit_match(link.url())
 
229
                if m:
 
230
                    filename, line, column = pointandclick.readurl(m)
 
231
                    text = "{0} ({1}:{2})".format(os.path.basename(filename), line, column)
 
232
                else:
 
233
                    text = link.url()
 
234
            QToolTip.showText(pos, text, self.view.surface(), page.linkRect(link.linkArea()))
 
235
 
 
236
    def slotCurrentViewChanged(self, view, old=None):
 
237
        self.view.surface().clearHighlight(self._highlightMusicFormat)
 
238
        if old:
 
239
            old.cursorPositionChanged.disconnect(self.slotCursorPositionChanged)
 
240
        view.cursorPositionChanged.connect(self.slotCursorPositionChanged)
 
241
    
 
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
 
246
        
 
247
        view = self.parent().mainwindow().currentView()
 
248
        links = self._links.boundLinks(view.document())
 
249
        if not links:
 
250
            return # the PDF contains no references to the current text document
 
251
        
 
252
        s = links.indices(view.textCursor())
 
253
        if s is False:
 
254
            self.clearHighlighting()
 
255
        elif s:
 
256
            self.highlight(links.destinations(), s)
 
257
 
 
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
 
261
        if msec is None:
 
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]
 
268
        if count > 100:
 
269
            self._highlightTimer.start()
 
270
        else:
 
271
            self._highlightTimer.stop()
 
272
            self.updateHighlighting()
 
273
    
 
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)
 
281
    
 
282
    def clearHighlighting(self):
 
283
        """Called on timeout of the _highlightRemoveTimer."""
 
284
        self._highlightRange = None
 
285
        self.view.surface().clearHighlight(self._highlightMusicFormat)
 
286
 
 
287
    def showCurrentLinks(self):
 
288
        """Scrolls the view if necessary to show objects at current text cursor."""
 
289
        if not self._links:
 
290
            return # no PDF in viewer
 
291
            
 
292
        view = self.parent().mainwindow().currentView()
 
293
        links = self._links.boundLinks(view.document())
 
294
        if not links:
 
295
            return # the PDF contains no references to the current text document
 
296
        
 
297
        s = links.indices(view.textCursor())
 
298
        if not s:
 
299
            return
 
300
        layout = self.view.surface().pageLayout()
 
301
        rect = QRect()
 
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)
 
309
    
 
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
 
314
        # mouse inside view?
 
315
        if self.view.mapFromGlobal(QCursor.pos()) in self.view.viewport().rect():
 
316
            pos = QCursor.pos()
 
317
            pos_in_surface = self.view.surface().mapFromGlobal(pos)
 
318
            page, link = self.view.surface().pageLayout().linkAt(pos_in_surface)
 
319
            if link:
 
320
                cursor = self._links.cursor(link)
 
321
        from . import contextmenu
 
322
        contextmenu.show(pos, self.parent(), link, cursor)
 
323
 
 
324
 
 
325
class Highlighter(qpopplerview.Highlighter):
 
326
    """Simple version of qpopplerview.Highlighter that has the color settable.
 
327
    
 
328
    You must set a color before using the Highlighter.
 
329
    
 
330
    """
 
331
    def setColor(self, color):
 
332
        """Sets the color to use to draw highlighting rectangles."""
 
333
        self._color = color
 
334
    
 
335
    def color(self):
 
336
        """Returns the color set using the setColor method."""
 
337
        return self._color
 
338
 
 
339