I am using the Jira Python API and have a PySide2 (Qt5) Application that needs to display Jira issues when a user selects items from a view.
If possible, I'd like to just display exactly what any issue looks like when you browse to it, minus the navbars on the side and top of the issue. Screenspace is an issue with this app, so this is important.
I'm a little unclear on the best way to do this. The way I see it I have two options:
Use the QWebEngineView and display the URL directly. But I can't currently find any documentation on how to retrieve that URL without navbars. To me this is the simplest option, and preferable.
Render the issues myself in a QLabel, QTextEdit, or some custom widget.
#1 is preferable for a number of reasons, so is there a URL I can provide to QWebEngineView that can render without the navbars?
from PySide2.QtWebEngineWidgets import QWebEngineView
url = 'https://jira.atlassian.com/browse/JRASERVER-26418'
view = QWebEngineView()
view.load(url)
view.show()
If this isn't possible, what's the most straightforward way to render the issue myself in a comparable layout?
Is there an easier 3rd option I'm not considering?
I'll add the disclaimer - I do more desktop app development and have only a limited experience with web-development. So would appreciate any details on anything I need to utilize outside of python/PySide/Qt.
As the OP points out, one possible option is to use QWebEngineView and javascript can be used to remove components like the navbar:
from PySide2 import QtCore, QtWidgets, QtWebEngineWidgets
class QJiraViewer(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.view = QtWebEngineWidgets.QWebEngineView()
self.setCentralWidget(self.view)
self.resize(640, 480)
source_code = """
// #include https://jira.atlassian.com/browse/JRASERVER-*
var target = document.getElementById("page");
var e = document.getElementById("header");
e.hidden = true;
if( target != null){
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
var elements = mutation.target.getElementsByClassName("aui-sidebar projects-sidebar fade-in");
for (var i = 0; i < elements.length; i++) {
var e = elements.item(i);
e.hidden = true;
e.parentNode.removeChild(e);
}
});
});
var config = {
childList: true,
subtree: true
};
observer.observe(target, config);
}
"""
script = QtWebEngineWidgets.QWebEngineScript()
script.setInjectionPoint(QtWebEngineWidgets.QWebEngineScript.DocumentReady)
script.setName("remove_elements")
script.setRunsOnSubFrames(False)
script.setSourceCode(source_code)
self.view.page().profile().scripts().insert(script)
def load(self, url):
self.view.load(url)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
view = QJiraViewer()
view.load(QtCore.QUrl("https://jira.atlassian.com/browse/JRASERVER-26418"))
view.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Related
I wrote a python test program like this to show openstreetmap:
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineView
import sys
def mainPyQt5():
url = 'file:///./index.html'
app = QApplication(sys.argv)
browser = QWebEngineView()
browser.load(QUrl(url))
browser.show()
sys.exit(app.exec_())
mainPyQt5()
index.html fetched by QWebEngineView simply calls openstreetmap:
<title>OSM and Leaflet</title>
<link rel = "stylesheet" href = "http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.css"/>
<div id = "map" style = "width: 900px; height: 580px"></div><script src = "http://cdn.leafletjs.com/leaflet-0.7.3/leaflet.js"></script>
<script>
// Creating map options
var mapOptions = {
center: [45.641174, 9.114828],
zoom: 10
}
// Creating a map object
var map = new L.map('map', mapOptions);
// Creating a Layer object
var layer = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
// Adding layer to the map
map.addLayer(layer);
</script>
If I fetch index.html with a ordinary browser the map is shown as expected but if I call the simple python program using QWebEngineView no tiles are downloaded from openstreetmap. If I replace openstreetmap with maps.stamen.com everything is fine both with a browser or the python script.
By default QtWebEngine does not set default headers like popular browsers do. In this case the openstreetmap server needs to know the "Accept-Language" to produce the maps since for example the names of the cities will depend on the language to filter non-browser traffic. The solution is to implement a QWebEngineUrlRequestInterceptor that adds that header:
import os.path
import sys
from PyQt5.QtCore import QUrl
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebEngineCore import QWebEngineUrlRequestInterceptor
from PyQt5.QtWebEngineWidgets import QWebEngineView
class Interceptor(QWebEngineUrlRequestInterceptor):
def interceptRequest(self, info):
info.setHttpHeader(b"Accept-Language", b"en-US,en;q=0.9,es;q=0.8,de;q=0.7")
def mainPyQt5():
CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
filename = os.path.join(CURRENT_DIR, "index.html")
app = QApplication(sys.argv)
browser = QWebEngineView()
interceptor = Interceptor()
browser.page().profile().setUrlRequestInterceptor(interceptor)
browser.load(QUrl.fromLocalFile(filename))
browser.show()
sys.exit(app.exec_())
if __name__ == "__main__":
mainPyQt5()
Consider the following PyQt program,
import sys
from PyQt5 import QtCore, QtWidgets
class dockdemo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(dockdemo, self).__init__(parent)
self.items = QtWidgets.QDockWidget("Dockable", self)
self.listWidget = QtWidgets.QListWidget()
self.listWidget.addItem("item1")
self.listWidget.addItem("item2")
self.listWidget.addItem("item3")
self.items.setWidget(self.listWidget)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.items)
self.setWindowTitle("Dock demo")
def main():
app = QtWidgets.QApplication(sys.argv)
ex = dockdemo()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
This works fine and produces a simple, docked window:
However, this isn't obeying my GTK2 dark platform theme. If I force Qt to do so by setting QT_QPA_PLATFORMTHEME=gtk2, I instead get this:
The docked window's controls are almost the same colour as the background so they're very difficult to see.
GNU Octave in C++ shows its own docked widgets controls correctly in the platform theme:
Octave's docked widgets also show the right controls when not using the system theme.
I suspect it's probably due to some of the CSS it's setting here, but I don't know exactly what: http://hg.savannah.gnu.org/hgweb/octave/file/6d0585c8ee11/libgui/src/octave-dock-widget.cc#l123
Am I doing something wrong? It feels like a bug that Qt isn't properly setting the colours for the docked window's controls unless you do something extra (and what that extra might be, I don't know yet).
Thanks to another answer here, I have a complete solution for my original problem as stated.
It appears that Qt simply hardcodes those icons without regard to the theme, but this is simple to fix.
First, we use the relative luminance to decide if a colour is bright or not,
def is_dark(qt_colour):
r, g, b = qt_colour.red(), qt_colour.green(), qt_colour.blue()
luminance = (0.2126*r + 0.7152*g + 0.0722*b)/256
return luminance < 0.5
and then we grab some icons that are identical but coloured dark and light. I just grabbed Octave's own set of icons:
widget-close-light.svg
widget-undock-light.svg
widget-close.svg
widget-undock.svg
found in its source tree. We place these icons in an img/ subdirectory/subfolder.
Then, we grab the widget's background colour,
bg_colour = self.items.palette().color(QtGui.QPalette.Background)
and depending on that colour, we set the CSS to use the light or the dark set of icons:
if is_dark(bg_colour):
self.items.setStyleSheet(
"""
QDockWidget
{
titlebar-close-icon: url(img/widget-close-light.svg);
titlebar-normal-icon: url(img/widget-undock-light.svg);
}
"""
)
else:
self.items.setStyleSheet(
"""
QDockWidget
{
titlebar-close-icon: url(img/widget-close.svg);
titlebar-normal-icon: url(img/widget-undock.svg);
}
"""
)
This results in proper icons in both light and dark themes!
The complete code now looks like this:
import sys
from PyQt5 import QtCore, QtWidgets, QtGui
def is_dark(qt_colour):
r, g, b = qt_colour.red(), qt_colour.green(), qt_colour.blue()
luminance = (0.2126*r + 0.7152*g + 0.0722*b)/256
return luminance < 0.5
class dockdemo(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(dockdemo, self).__init__(parent)
self.items = QtWidgets.QDockWidget("Dockable", self)
self.listWidget = QtWidgets.QListWidget()
self.listWidget.addItem("item1")
self.listWidget.addItem("item2")
self.listWidget.addItem("item3")
self.items.setWidget(self.listWidget)
self.addDockWidget(QtCore.Qt.RightDockWidgetArea, self.items)
bg_colour = self.items.palette().color(QtGui.QPalette.Background)
if is_dark(bg_colour):
self.items.setStyleSheet(
"""
QDockWidget
{
titlebar-close-icon: url(img/widget-close-light.svg);
titlebar-normal-icon: url(img/widget-undock-light.svg);
}
"""
)
else:
self.items.setStyleSheet(
"""
QDockWidget
{
titlebar-close-icon: url(img/widget-close.svg);
titlebar-normal-icon: url(img/widget-undock.svg);
}
"""
)
self.setWindowTitle("Dock demo")
def main():
app = QtWidgets.QApplication(sys.argv)
ex = dockdemo()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Unfortunately the Linux-default Qt Fusion style dock icons are hard-coded as XPM format images in the QFusionStyle (and also in QCommonStyle which is the fallback). And they are never colored to match the theme. A lot of the "standard" icons are like that but many are colored/opaque so the background doesn't make a big difference.
To override them you will need to either use CSS or a custom QProxyStyle.
You can see how it is done in CSS from that example you linked to.
QDockWidget
{
titlebar-close-icon: url(close.svg);
titlebar-normal-icon: url(restore.svg);
}
A custom QStyle is a little more involved...
class AppStyle : public QProxyStyle
{
public:
using QProxyStyle::QProxyStyle;
QIcon standardIcon(StandardPixmap standardIcon, const QStyleOption *option = nullptr, const QWidget *widget = nullptr) const override
{
switch (standardIcon) {
case SP_TitleBarNormalButton:
return QIcon("restore.svg");
case SP_TitleBarCloseButton:
case SP_DockWidgetCloseButton:
return QIcon("close.svg");
default:
return baseStyle()->standardIcon(standardIcon, option, widget);
}
}
QPixmap standardPixmap(StandardPixmap stdPixmap, const QStyleOption *option = nullptr, const QWidget *widget = nullptr) const override
{
switch (stdPixmap) {
case SP_TitleBarNormalButton:
case SP_TitleBarCloseButton:
case SP_DockWidgetCloseButton:
return standardIcon(stdPixmap, option, widget).pixmap(option->rect.size());
default:
return baseStyle()->standardPixmap(stdPixmap, option, widget);
}
}
};
In both cases you'd need to know the theme being used somehow (eg. that it is dark). You'd use different (or dynamic) CSS for each theme, or your custom QProxyStyle would return the correct icon for the base color. In C++ for example you could even determine if the current palette background is dark (low color value) and then return different icons based on that.
P.S. Yes it could probably be considered a "bug" or deficiency that Qt doesn't handle this "automagically" already for dark system themes -- it is also quite annoying when trying to skin an app to be dark regardless of the desktop theme. But c'est la vie.
P.P.S. Whoops, just realized I gave a C++ example for a Python question... I don't use Python with Qt so I'm afraid that's the best I can do.
I need to reimplement the "copy link" of a QWebView in the context menu for doing some other things inside the routine.
The copy link is the only one that really works inside an ajax site so I'm trying to reimplement the "download from link" passing trough this method.
The problem is that I don't know how to reimplement the basic functions of the "copy link" for retrieving the URL.
You can add extra menu items by reimplementing QWebView.contextMenu and generating a standard menu with QWebPage.createStandardContextMenu.
Then all you need to do is get a hit-test result from the position the context menu was requested from to give you the url (if there is one).
Here's a simple demo of the basic ideas:
from PyQt4 import QtGui, QtWebKit
class Browser(QtWebKit.QWebView):
def __init__(self):
super(Browser, self).__init__()
self.setHtml('''
<html><head><title>Test Page</title>
<body>
<p>link</p>
</body>
</html>
''')
def contextMenuEvent(self, event):
menu = self.page().createStandardContextMenu()
hit = self.page().currentFrame().hitTestContent(event.pos())
url = hit.linkUrl()
if not url.isEmpty():
menu.addSeparator()
action = menu.addAction('Download')
action.triggered.connect(lambda: self.download(url))
menu.exec_(event.globalPos())
def download(self, url):
print('download:', url)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
browser = Browser()
browser.setGeometry(800, 200, 400, 200)
browser.show()
sys.exit(app.exec_())
Building off this Pyside tutorial:
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_1
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_2
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_3
http://qt-project.org/wiki/PySide_QML_Tutorial_Advanced_4
I am attempting to do everything in Python and not have any java script.
The only difficulty I've run into is when calling the createObject() method of a QDeclarativeComponent which is described nicely as a "Dynamic Object Management" here:
http://qt-project.org/doc/qt-4.8/qdeclarativedynamicobjects.html
So here is a bare bones example that causes the error:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import *
class MainWindow(QDeclarativeView):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle("Main Window")
# Renders game screen
self.setSource(QUrl.fromLocalFile('game2.qml'))
# QML resizes to main window
self.setResizeMode(QDeclarativeView.SizeRootObjectToView)
# a qml object I'd like to add dynamically
self.component = QDeclarativeComponent(QDeclarativeEngine(), QUrl.fromLocalFile("Block2.qml"))
# check if were ready to construct the object
if self.component.isReady():
# create the qml object dynamically
dynamicObject = self.component.createObject(self.rootObject())
if __name__ == '__main__':
# Create the Qt Application
app = QApplication(sys.argv)
# Create and show the main window
window = MainWindow()
window.show()
# Run the main Qt loop
sys.exit(app.exec_())
With main window QML file contents ("game2.qml"):
import QtQuick 1.0
Rectangle {
id: screen
width: 490; height: 720
SystemPalette { id: activePalette }
}
And QML object I'd like to dynamically construct ("Block2.qml"):
import QtQuick 1.0
Rectangle {
id: block
}
When I run this code, it crashes at:
dynamicObject = self.component.createObject(self.rootObject())
with:
TypeError: Unknown type used to call meta function (that may be a signal): QScriptValue
I understand the parent must be a QObject but otherwise I'm not entirely sure from the docs what more it should constitute:
http://srinikom.github.io/pyside-docs/PySide/QtDeclarative/QDeclarativeComponent.html
This isn't an issue in C++ according to:
https://qt-project.org/forums/viewthread/7717
It is clearly only an issue in Pyside currently.
Any idea what might be causing this issue? Potential bug?
A work around is to rely on javascript for object creation while everything else is python. In this implementation you pass the qml file of the component and its parent to the javascript implementation that creates the component. It does a basic construction of the object. Would be ideal having pure python solution though for brevity.
import sys
from PySide.QtCore import *
from PySide.QtGui import *
from PySide.QtDeclarative import *
class MainWindow(QDeclarativeView):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle("Main Window")
# Renders game screen
self.setSource(QUrl.fromLocalFile('game2.qml'))
# QML resizes to main window
self.setResizeMode(QDeclarativeView.SizeRootObjectToView)
# a qml object I'd like to add dynamically
parent = self.rootObject()
view = QDeclarativeView()
view.setSource(QUrl.fromLocalFile("comp_create.qml"))
block = view.rootObject().create("Block2.qml", parent)
print block
block.x = 100
block.y = 200
# prove that we created the object
print block.x, block.y
if __name__ == '__main__':
# Create the Qt Application
app = QApplication(sys.argv)
# Create and show the main window
window = MainWindow()
window.show()
# Run the main Qt loop
sys.exit(app.exec_())
The only added QML is a component creator that uses javascript to create objects since Pyside currently wont work ("comp_create.qml"):
import QtQuick 1.0
Item {
id: creator
function create(qml_fname, parent) {
// for a given qml file and parent, create component
var comp = Qt.createComponent(qml_fname);
if (comp.status == Component.Ready) {
// create the object with given parent
var ob = comp.createObject(parent);
if (ob == null) {
// Error Handling
console.log("Error creating object");
}
return ob
} else if (component.status == Component.Error) {
// Error Handling
console.log("Error loading component:", component.errorString());
}
else {
component.statusChanged.connect(finishCreation);
}
return null
}
}
Note, borrowed this code mostly from:
http://qt-project.org/doc/qt-4.8/qdeclarativedynamicobjects.html
I'm writing a cross platform app in PyQt4. For a particular feature, I would like to access the QTextHtmlImporter class of Qt4. There is no direct python adapter class available in PyQt4. The class is part of the src/gui/text/qtextdocumentfragment_p.h file. Is there any way I can access that in Python?
I would like to modify QTextDocument.setHtml(), which code is:
void QTextDocument::setHtml(const QString &html) {
Q_D(QTextDocument); setUndoRedoEnabled(false);
d->clear();
QTextHtmlImporter(this, html).import();
setUndoRedoEnabled(true);
}
to
void QTextDocument::setHtml(const QString &html) {
Q_D(QTextDocument);
QTextHtmlImporter(this, html).import();
}
Basically setting the HTML without clearing the history. I planned to do this by using a derived class of PyQt4's QTextDocument overriding the setHtml function. Is there any other way to do this?
QTextHtmlImporter isn't even part of the Qt4 API, so the short answer is: no, there's no way to access it in PyQt4.
You could, of course, attempt to port the code to PyQt4, but I'm guessing that would be a non-trivial task.
The question is: why do you think you need to do this?
Why can't you use QTextCursor.insertHtml or QTextDocumentFragment.fromHtml?
EDIT
Here's an example of how to set the html in a text document without clearing the undo history:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
layout = QtGui.QVBoxLayout(self)
self.edit = QtGui.QTextEdit(self)
self.undo = QtGui.QPushButton('Undo')
self.redo = QtGui.QPushButton('Redo')
self.insert = QtGui.QPushButton('Set Html')
layout.addWidget(self.edit)
layout.addWidget(self.undo)
layout.addWidget(self.redo)
layout.addWidget(self.insert)
self.undo.clicked.connect(self.edit.undo)
self.redo.clicked.connect(self.edit.redo)
self.insert.clicked.connect(self.handleInsert)
self.edit.append('One')
self.edit.append('Two')
self.edit.append('Three')
def handleInsert(self):
cursor = QtGui.QTextCursor(self.edit.document())
cursor.select(QtGui.QTextCursor.Document)
cursor.insertHtml("""<p>Some <b>HTML</b> text</p>""")
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())