New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clickable links #2467
Clickable links #2467
Conversation
Since the qtconsole can display hyperlinks, it would be useful to allow interacting with them. This adds showing a tooltip when the mouse is over a link. The tooltip code stores the anchor in ConsoleWidget._anchor, so when the user right-clicks to select the context menu for "Open Link" or "Copy Link Address", it uses the text that was displayed and not whats under the current context menu pointer location. Also storing the anchor allows me to check to see if we've already displayed that anchor on a new mouseMoveEvent so the tooltip doesn't keep getting redrawn.
Test the _anchor updating for implementing qtconsole clickable links.
I don't think we want to completely override the Otherwise things look pretty good to me! |
As a start, I think you can probably move the logic to |
Ok, I wasn't sure if the the eventFilter was for handling events or if it was just controlling what reached the control. I'll try moving it tonight. |
Bradly Froehle pointed out my original implementation broke selecting text and suggested moving the link tooltip handling to eventFilter. The current version doesn't report that it handled the mouse move message, I think this may be better as in addition to showing the tool tip you can still select text inside the linkable object. (Its always been a problem to select some chunk of link text in gnome-terminal as their link handling overrides the other mouse selecting functionality)
New patch committed to clickable-links. Do I need to resubmit the pull request, do you get notifications on new pushes? |
@detrout : No need to resubmit, but we don't automatically get notified, so it's useful to comment - as you did - so we know it's been updated. |
The test code wasn't updated to test the eventFilter based implementation.
I forgot to update my test case for the mouse hover code. Hopefully the text never ends up 50 mouse units high. |
Am I right that this is currently waiting for review from people who know about Qt? |
I'm not aware of anything else I need to do for this patch. Potential future work would be adding support for detecting plain text URLs and converting them to links like recently added to the HTML notebook, but I could see wanting to change the default qtconsol formatting as well. |
I'm not quite familiar enough with Qt to give a final okay here. Also I have some comments which I'll post in the diff view. |
|
||
elif etype == QtCore.QEvent.MouseMove: | ||
anchor = self._control.anchorAt(event.pos()) | ||
if len(anchor) == 0: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as if not anchor
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I bet it'd work, I'm just not used to the "if not anchor:" idiom so tend to be uncertain if the empty string is false.
Also is there any reason to abstract the hide/show tooltip into a signal/slot framework? |
if len(anchor) == 0: | ||
self._anchor = None | ||
QtGui.QToolTip.hideText() | ||
elif anchor != self._anchor: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test temporary variable anchor to see if the pointer moved over a different anchor.
Why anchor temporary variable The reason for storing the result of "anchor = self._control.anchorAt(event.pos())" as a temporary variable instead of directly into self._anchor is I wanted to check to see if the pointer was still over the same anchor (line 443). I'd seen some applications where the tooltip keeps getting redrawn in slightly different locations as the pointer moves and it's quite annoying. This way the tooltip gets drawn once until the anchorAt is empty or is over a different anchor. I was assuming it's possible for two anchors to be next to each other without receiving a mouse move message that would clear self._anchor. Performance of calling self._control.anchorAt As for performance. I'm basically depending on Qt to batch the mouse move messages to keep the lookup call count low. The other solution I'd thought of was to have receiving a mouse move reset and then start a short timer, and only do the self._control.anchorAt call once the pointer hadn't moved for 200ms. Then I decided I wasn't sure where to put the timer class and went for the simpler to read solution. I'm not sure of a good way to test which is lighter-weight between the timer and just calling anchorAt. |
Yes, okay. I don't think performance is an issue here. I'm a little worried about the fragility of I've found the following to work well: In elif etype == QtCore.QEvent.MouseMove:
anchor = self._control.anchorAt(event.pos())
QtGui.QToolTip.showText(event.globalPos(), anchor, self._control) In anchor = self._control.anchorAt(pos)
if anchor:
menu.addSeparator()
self.copy_link_action = menu.addAction(
'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
self.open_link_action = menu.addAction(
'Open Link', lambda: self.open_anchor(anchor=anchor)) def copy_anchor(self, anchor):
"""Copy anchor text to the clipboard."""
QtGui.QApplication.clipboard().setText(anchor)
def open_anchor(self, anchor):
"""Open selected anchor in the default webbrowser."""
webbrowser.open(anchor) |
Alternatively, if we don't like tooltips we could use a status bar: elif etype == QtCore.QEvent.MouseMove:
anchor = self._control.anchorAt(event.pos())
tip = QtGui.QStatusTipEvent(anchor)
QtGui.QApplication.sendEvent(self._control, tip) although this requires creating a QStatusBar by calling |
I can see wanting to remove the added class variable, but I do want to protect against redrawing, though if using the status bar redrawing would probably be safe, since it avoids having the text drift around the screen as .pos changes. I can see two possibilities to prevent unnecessary QToolTip.showText calls in elif etype == QtCore.QEvent.MouseMove:
anchor = self._control.anchorAt(event.pos())
if not anchor:
QtGui.QToolTip.hideText()
elif anchor not in QtGui.QToolTip.text():
QtGui.QToolTip.showText(event.globalPos(), anchor) That would drop self._anchor and use the QToolTip text as the temporary variable for checking to see if we drifted over a different link. (Also your choice between The other possibility would be: elif etype == QtCore.QEvent.MouseMove:
anchor = self._control.anchorAt(event.pos())
if not anchor:
QtGui.QToolTip.hideText()
elif not QtGui.QToolTip.isVisible():
QtGui.QToolTip.showText(event.globalPos(), anchor) That should remove a tooltip if there's no anchor, show a tool tip if there isn't currently one, and do nothing if there is a tooltip and an anchor. However it can't tell if the user moved over a different anchor. For either of those implementations, I'd use the lambda function implementation you described for _context_menu_make. |
If you read the documentation for QToolTip, you'll see that it already protects against redrawing. In addition, passing the empty string is equivalent to calling |
That's remarkably convenient of them. Thank you for calling that part of the documentation out. Fixing up patch as you described, I should have it posted in a few minutes. |
Bradley Froehle recommended removing self._anchor and just requesting the anchor text for a position as it was needed. He also pointed out that Qt handles hiding the tooltip for the empty string and suppressing redraw messages which simplified the event handler code. The only problem I found is when updating my test code the tooltip takes a moment to disappear and caches the text value for a time longer than a function call. So I can't verify that the tooltip is cleared when the mouse moves off the anchor.
I think everything's been updated. Anything else I should do? |
Ahh, thanks for the update. Unfortunately GitHub doesn't send emails when you just push some changes. :/ Merging now. |
Qt console: Add "Open Link" & "Copy Link Address" to right click menu.
Good to know that I should poke the pull request when finishing up the patch. (Whoohoo! I successfully contributed code!) |
@detrout Thanks again. A nice improvement, I think. 👍 |
Qt console: Add "Open Link" & "Copy Link Address" to right click menu.
This implements a mechanism for selecting hyperlinks in the rich text editor.
It's using QtGui.QTextEdit's anchorAt to determine the existence of a link, so links that are printed without being wrapped in aren't going to be detected.
The implementation conditionally adds "Open Link" and "Copy Link Anchor" as inspired by gnome-terminal. (I didn't spend very much time thinking about the order of menu options)
A typical browser security feature is to show the user the target of a link before they can actually select it, I implemented that by adding a tooltop when the mouse hovers over an anchor.
That was implemented by turning on mouseMoveEvent messages on the rich QTextEdit controland checking to see if an anchor is under the current mouse position. If it is and its a different anchor than what we're currently displaying (or not displaying) it'll pop up the tooltip.
The advantage to storing the displayed anchor in ConsoleWidget._anchor allows me to be sure that the thing the user is opening was displayed to to them, and to prevent the tooltip from constantly getting redrawn as you move the mouse around the link.
The plain text control QPlainTextEdit does have the anchorAt method, but I was imagining lacked any way to receive html text so I didn't bother turning on the mouse move tracking for it.