For the past few days I've been getting my head around Qt's MVC architecture, namely the QAbstractItemModel
, QAbstractItemDelegate
and QAbstractItemView
. The theory is sound, but it breaks its own rules sometimes.
model.data()
returns not only data, but visuals too such asDecorationRole
,FontRole
andBackgroundRole
. Having said that, it's arguable whetherDisplayRole
is any less graphical.model.data()
contain references to "row" and "column". Things only relevant to drawing, as the data itself - such as a hierarchy - has no notion of columns or rows.QStandardItemModel
resides inQtGui
, presumably because it takes the above concept even further, with concepts likeappendRow
andappendColumn
.
I'm struggling to integrate the model in the way prescribed by the Qt MVC architecture.. this time when interacting with non-views like regular widgets. I'd like for the project choice button to get its data from the model too, as it's already there.
For future reference, here's where I got with this..
code
import os
import sys
import logging
from Qt import QtWidgets, QtCore, QtGui
# os.environ["QHONESTMODEL_NO_THREADING"] = "1"
from launchapp2 import model, delegates
from launchapp2.vendor import qhonestmodel
logging.basicConfig()
os.environ["REZ_PACKAGES_PATH"] = os.pathsep.join([
r"C:\Users\manima\packages",
r"C:\Users\manima\Dropbox\dev\anima\.cache\packages\ext",
r"C:\Users\manima\Dropbox\dev\anima\.cache\packages\int",
r"C:\Users\manima\Dropbox\dev\anima\.cache\packages\td",
r"C:\Users\manima\Dropbox\dev\anima\.cache\packages\converted",
])
app = QtWidgets.QApplication(sys.argv)
root = r"C:\Users\manima\Dropbox\dev\anima\.cache\projects"
root = model.Root(root)
main = model.Main(root)
view = QtWidgets.QTreeView()
view.setEditTriggers(view.DoubleClicked)
view.setItemDelegate(delegates.DelegateProxy(view))
view.setModel(main)
view.header().resizeSection(0, 270)
class Project(QtWidgets.QWidget):
projectChanged = QtCore.Signal(int) # row
def __init__(self, parent=None):
super(Project, self).__init__(parent)
widgets = {
"icon": QtWidgets.QToolButton(),
"label": QtWidgets.QLineEdit(),
"choice": QtWidgets.QComboBox(),
"menu": QtWidgets.QMenu(),
}
layout = QtWidgets.QGridLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(widgets["icon"], 0, 0, 2, 1)
layout.addWidget(widgets["label"], 0, 1)
layout.addWidget(widgets["choice"], 1, 1)
widgets["icon"].setPopupMode(widgets["icon"].InstantPopup)
widgets["icon"].setFixedSize(50, 50)
widgets["label"].setEnabled(False)
widgets["icon"].setMenu(widgets["menu"])
widgets["menu"].triggered.connect(self.onMenuTriggered)
self._widgets = widgets
def onMenuTriggered(self, action):
print("%s selected" % action.text())
self.projectChanged.emit(action.text())
class ProjectDelegate(QtWidgets.QAbstractItemDelegate):
def __init__(self, widget, parent=None):
super(ProjectDelegate, self).__init__(parent)
print("Listening to projectChanged..")
widget.projectChanged.connect(
lambda: self.commitData.emit(widget)
)
def setEditorData(self, editor, index):
model = index.model()
item = index.internalPointer()
row = 1 # This should come from the menu widget
try:
projects = model.createIndex(0, 0, item.child(row))
except IndexError:
# Keep fulfilling promises until the desired row is reached
model.createIndex(0, 0, item.child(-1)).data(QtCore.Qt.DisplayRole)
return
menu = editor._widgets["menu"]
menu.clear()
def on_accept(project):
project = project.text()
group = QtWidgets.QActionGroup(menu)
group.triggered.connect(on_accept)
for child in item.children():
print("Adding %s" % child)
index = model.createIndex(child.row(), 0, child)
if isinstance(child, qhonestmodel.QPromiseItem):
index.data(QtCore.Qt.DisplayRole)
continue
text = child.text()
action = QtWidgets.QAction(text, menu)
action.setCheckable(True)
if project == index.data(QtCore.Qt.DisplayRole):
action.setChecked(True)
group.addAction(action)
menu.addAction(action)
value = projects.data(QtCore.Qt.DisplayRole)
editor._widgets["label"].setText(value)
choice = editor._widgets["choice"]
choice.clear()
item = projects.internalPointer()
for child in item.children():
index = model.createIndex(child.row(), 0, child)
if isinstance(child, qhonestmodel.QPromiseItem):
index.data(QtCore.Qt.DisplayRole)
continue
text = child.text()
choice.addItem(text)
def setModelData(self, editor, model, index):
value = editor._widgets["label"].text()
model.setData(index, 1, qhonestmodel.CurrentRowRole)
print("Writing %s to %s.." % (value, model))
project = Project()
mapper1 = QtWidgets.QDataWidgetMapper()
mapper1.setModel(main)
mapper1.setItemDelegate(ProjectDelegate(project))
mapper1.addMapping(project, 0)
mapper1.toFirst()
window = QtWidgets.QDialog()
layout = QtWidgets.QVBoxLayout(window)
layout.addWidget(project)
layout.addWidget(view)
window.resize(470, 500)
window.show()
app.exec_()
Rez doesn't support overruling a resolved version that breaks the original constraints.
For example, if you ask for alita
which requires maya-2017
, you can't then ask for maya-2018
as that breaks the requirements of alita
. But overruling versions is a fundamental tool in testing new versions of sofware with an existing configuation. You may for example need to confirm whether or not maya-2019
does in fact work using the original requirements.
To account for this, I've implemented a mechanism which allows you to accomplish just that.
The way it works is this.
- A context is resolved as per-usual
- On launch of a command, an override is translated into a Variant
- The variant replaces its corresponding variant in the context
- The command is executed
This way, there is no additional resolving but rather the resolve is used to initialise the versions defined in that list of packages. The user is then free to edit this list as he sees fit. Overridden versions are highlighted to inform the user of the act.
In addition, the way it is implemented currently means that overrides are global. Which means that if you override python-2.7
within any app, the override is persistent across all apps. It's not clear whether this is desireable just yet, it was just a consequence of this particular implementation.
The downside of this version however is that fetching those versions happens synchronously. When there are a large number of versions for a package, this could easily cause the GUI to lock up whilst it's busy fetching them. In addition, versions are currently fetched every time you perform an override. This is highly dynamic, but also very inefficient as versions on disk/network are unlikely to change this frequently.
With the previous section in mind, I've struggled to make sense out of how I'd like the async nature of fetching data from a model to actually work, and will be revisiting this shortly.
Some environment variables are paths and are difficult to visualise as one long string. So now those are made into lists.
Sometimes, you've got a lot of things on-screen at once and want to zone in on just one of them. Now you can CTRL+Click on any dock to isolate it.
Once you've overridden a version, it's important to keep track of which version is the recommended or default version. Now you can right-click an item and reset an item to default.
Now you can configure whether you want more than a single dock visible at any given time.
LA2 is designed for both artists and developers. To avoid overwhelming the artist, advanced controls are now hidden behind a "Show Advanced Controls" checkbox.
With MVC temporarily put aside, I managed to tick a few boxes.
- Override Let the user override a package version with any arbitrary alternative
- PATH-like children Make list out of PATH-like variables for QJsonModel, so the user can browse those more easily
- Isolate Tab Ctrl+click on tab to hide all others
- Reset Override Right+click a package version to reset it to its original value
- Interactive Overrides Store user-overrides using the Command-pattern, such that they may be applied (or not) on-demand, visualised interactively and stored on disk for reuse on relaunch.
- Allow multiple tabs Let the user decide whether or not to allow more than a single tab be visible at a time. Can help keep clutter down.
- Developer Mode Some UI items aren't relevant to the artist, such as picking a version of a given project. Relegate this to a "developer mode", similar to launchapp(1).
- Formalise package metadata For
_apps
and_icons
; was thinking of making a._metadata = {}
variable in each package with optional data such as these. Don't think there's anything official provided by Rez, but if you know anything let met know! - Edited indicator visually indicate whether a package is manually added or edited
Keep ticking those boxes.
- Right-click open path to package for debugging the
package.py
on disk (if stored on filesystem, could also come from e.g. database) - Disable package Right-click to temporarily disable the inclusion of a package
- Total number of packages Spot total number at a glance
- Override Environment Append/subtract variables and contents of variables, such as adding more paths to
PYTHONPATH
- App Tab Improvements. This really needs some love and is quite bare at the moment.
- Show available "tools" for an application, e.g.
maya
andmayapy
for the Maya package, includingterminal
for almost every other package, to launch a plain shell in the desired environment. - Show already-running commands, like how many Maya's there are
- Show environment overrides for a given application
- Show available "tools" for an application, e.g.