1# This file is part of the qpageview package.
2#
3# Copyright (c) 2019 - 2019 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"""
22Generic Link class and handling of links (clickable areas on a Page).
23
24The link area is in coordinates between 0.0 and 1.0, like Poppler does it.
25This way we can easily compute where the link area is on a page in different
26sizes or rotations.
27
28"""
29
30import collections
31
32from PyQt5.QtCore import pyqtSignal, QEvent, QRectF, Qt
33
34from . import page
35from . import rectangles
36
37Area = collections.namedtuple("Area", "left top right bottom")
38
39
40class Link:
41    url = ""
42    tooltip = ""
43    area = Area(0, 0, 0, 0)
44
45    def __init__(self, left, top, right, bottom, url=None, tooltip=None):
46        self.area = Area(left, top, right, bottom)
47        if url:
48            self.url = url
49        if tooltip:
50            self.tooltip = tooltip
51
52    def rect(self):
53        """Return the area attribute as a QRectF()."""
54        r = QRectF()
55        r.setCoords(*self.area)
56        return r
57
58
59class Links(rectangles.Rectangles):
60    """Manages a list of Link objects.
61
62    See the rectangles documentation for how to access the links.
63
64    """
65    def get_coords(self, link):
66        return link.area
67
68
69class LinkViewMixin:
70    """Mixin class to enhance view.View with link capabilities."""
71
72    linkHovered = pyqtSignal(page.AbstractPage, Link)
73    linkLeft = pyqtSignal()
74    linkClicked = pyqtSignal(QEvent, page.AbstractPage, Link)
75    linkHelpRequested = pyqtSignal(QEvent, page.AbstractPage, Link)
76
77    linksEnabled = True
78
79    def __init__(self, parent=None, **kwds):
80        self._currentLinkId = None
81        self._linkHighlighter = None
82        super().__init__(parent, **kwds)
83
84    def setLinkHighlighter(self, highlighter):
85        """Sets a Highlighter (see highlight.py) to highlight a link on hover.
86
87        Use None to remove an active Highlighter. By default no highlighter is
88        set to highlight links on hover.
89
90        To be able to actually *use* highlighting, be sure to also mix in the
91        HighlightViewMixin class from the highlight module.
92
93        """
94        self._linkHighlighter = highlighter
95
96    def linkHighlighter(self):
97        """Return the currently set Highlighter, if any.
98
99        By default no highlighter is set to highlight links on hover, and None
100        is returned in that case.
101
102        """
103        return self._linkHighlighter
104
105    def adjustCursor(self, pos):
106        """Adjust the cursor if pos is on a link (and linksEnabled is True).
107
108        Also emits signals when the cursor enters or leaves a link.
109
110        """
111        if self.linksEnabled:
112            page, link = self.linkAt(pos)
113            if link:
114                lid = id(link)
115            else:
116                lid = None
117            if lid != self._currentLinkId:
118                if self._currentLinkId is not None:
119                    self.linkHoverLeave()
120                self._currentLinkId = lid
121                if lid is not None:
122                    self.linkHoverEnter(page, link)
123            if link:
124                return # do not call super() if we are on a link
125        super().adjustCursor(pos)
126
127    def linkAt(self, pos):
128        """If the pos (in the viewport) is over a link, return a (page, link) tuple.
129
130        Otherwise returns (None, None).
131
132        """
133        pos = pos - self.layoutPosition()
134        page = self._pageLayout.pageAt(pos)
135        if page:
136            links = page.linksAt(pos - page.pos())
137            if links:
138                return page, links[0]
139        return None, None
140
141    def linkHoverEnter(self, page, link):
142        """Called when the mouse hovers over a link.
143
144        The default implementation emits the linkHovered(page, link) signal,
145        sets a pointing hand mouse cursor, and, if a Highlighter was set using
146        setLinkHighlighter(), highlights the link. You can reimplement this
147        method to do something different.
148
149        """
150        self.setCursor(Qt.PointingHandCursor)
151        self.linkHovered.emit(page, link)
152        if self._linkHighlighter:
153            self.highlight({page: [link.rect()]}, self._linkHighlighter, 3000)
154
155    def linkHoverLeave(self):
156        """Called when the mouse does not hover a link anymore.
157
158        The default implementation emits the linkLeft() signal, sets a default
159        mouse cursor, and, if a Highlighter was set using setLinkHighlighter(),
160        removes the highlighting of the current link. You can reimplement this
161        method to do something different.
162
163        """
164        self.unsetCursor()
165        self.linkLeft.emit()
166        if self._linkHighlighter:
167            self.clearHighlight(self._linkHighlighter)
168
169    def linkClickEvent(self, ev, page, link):
170        """Called when a link is clicked.
171
172        The default implementation emits the linkClicked(event, page, link)
173        signal. The event can be used for things like determining which button
174        was used, and which keyboard modifiers were in effect.
175
176        """
177        self.linkClicked.emit(ev, page, link)
178
179    def linkHelpEvent(self, ev, page, link):
180        """Called when a ToolTip or WhatsThis wants to appear.
181
182        The default implementation emits the linkHelpRequested(event, page, link)
183        signal. Using the event you can find the position, and the type of the
184        help event.
185
186        """
187        self.linkHelpRequested.emit(ev, page, link)
188
189    def event(self, ev):
190        """Reimplemented to handle HelpEvent for links."""
191        if self.linksEnabled and ev.type() in (QEvent.ToolTip, QEvent.WhatsThis):
192            page, link = self.linkAt(ev.pos())
193            if link:
194                self.linkHelpEvent(ev, page, link)
195                return True
196        return super().event(ev)
197
198    def mousePressEvent(self, ev):
199        """Implemented to detect clicking a link and calling linkClickEvent()."""
200        if self.linksEnabled:
201            page, link = self.linkAt(ev.pos())
202            if link:
203                self.linkClickEvent(ev, page, link)
204                return
205        super().mousePressEvent(ev)
206
207    def leaveEvent(self, ev):
208        """Implemented to leave a link, might there still be one hovered."""
209        if self.linksEnabled and self._currentLinkId is not None:
210            self.linkHoverLeave()
211            self._currentLinkId = None
212        super().leaveEvent(ev)
213
214
215