A Tour of the Code
At the moment, the code is very minimalist and straightforward, I’m expecting the contents and overall layout to change rapidly. Pull-requests are welcome.
To ignite this fire, here’s a quick walkthrough of how things currently are.
Project Layout
pyblish-lite/ | The main Python package, containing a __init__.py
font/ | Font assets, in .tff format
img/ | Image assets, in .png format
__init__.py |
__main__.py | This makes the module executable via `python -m pyblish_lite`
app.css | Application stylesheet
app.py | Main application source, to be divided into app.py and control.py
model.py |
util.py | Generic, shared functionality
view.py |
README.md |
LICENSE | LGPL
Stylesheet
The look of this project is entirely governed by CSS, the only exception being properties that it cannot modify. This includes colors and fonts, but also sizes, padding and position offsets (overall positions are governed by QLayout’s).
Documentation
Model/View separation
Similar to Pyblish QML, this project separates between what is drawn and the information upon which drawing is based. It is very important to keep communication in app.py
(later control.py
), persistent data in model.py
and graphics in view.py
.
If in doubt, start in control.py
and look for ways to extract code into either view.py
or model.py
. In general, anything that requires thought or handling of any kind is best suited for control.py
, whereas information processing and storage functionality goes into model.py
. Finally, custom widgets and drawing goes into view.py
, including delegates.
Weak spots
Most things are pretty straightforward and should last, whereas others need immediate work.
1. Getting and setting data in the model
The model is currently being fed data associated to a particular “role”. Roles are Qt-speak for “key”, where each “key” has an associated “value”.
instance.data["key"] = "value"
qt["role"] = "value"
The primary difference is that roles are integers and can only be integers, which is why they are typically assigned a variable with a more descriptive name, such as QtCore.Qt.DisplayRole
for values meant for display, which in our case is either instance.data["name"]
or Plugin.__name__
.
At the moment, data from instances and plug-ins are parsed into individual roles and entered into the model. The problem with that is that the model has no knowledge of where these roles came from or how to modify their original key.
For example.
-
QtCore.Qt.EditRole
is assigned instance.data["publish"]
- View fetches
QtCore.Qt.EditRole
and displays the status of a checkbox
- User modifies said checkbox
- The model should modify
instance.data["publish"]
but doesn’t know how to do that.
2. Delegates
There is one delegate at the moment, the checkbox. This checkbox is implemented such that an editor is persistently available on-top of its cell, the cell of which is in a QTableView.
Having an editor is convenient, as it provides an editable standard Widget which we can style normally via CSS.
On the other hand, the editor maintains its own state and later communicates this to the model. The model may then either reject or accept this change, but the editor couldn’t care less and so we must round-trip and tell it what has happened. This is not good model/view separation.
A better way, and one employed in Pyblish QML, is for the delegate to simply communicate that “The user just pressed me” and for control.py
to decide what to do about it.
For this, I suspect we need to do our own drawing with QPainter, which shouldn’t be too difficult. The difficult part, and the one I am less familiar with, is how information travels between the model, view and delegate.
Here is one way of doing this.
from Qt import QtWidgets, QtGui, QtCore
class CheckBoxDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
if index.column() == 0:
# First column, we are painting a checkbox
painter.save()
# Draw hollow square
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(option.rect))
pen = QtGui.QPen(QtCore.Qt.white, 1)
painter.setPen(pen)
painter.drawPath(path)
# Should we fill it?
if index.data(QtCore.Qt.EditRole) & QtCore.Qt.Checked:
painter.fillPath(path, QtCore.Qt.white)
painter.restore()
else:
# Some other column, go on your business as usual.
return super(CheckBoxDelegate, self).paint(painter, option, index)
As you can see, here the delegate is querying data from the model directly via the index; index.data(Role)
. Whether this is the way to go or not, I can’t say.
3. State Machine
At the moment, the footer buttons are explicitly hidden at the press of the reset or publish buttons, and later shown at the opportune time.
In a small GUI as this currently is, that’s ok. But as the amount of fork in the roads in terms of state start to grow, things will get hairy and we will need some method of managing this complexity.