Skip to content

Instantly share code, notes, and snippets.

@mottosso
Created June 5, 2019 14:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mottosso/cb7d70ddbad998fb0296d947be74ffac to your computer and use it in GitHub Desktop.
Save mottosso/cb7d70ddbad998fb0296d947be74ffac to your computer and use it in GitHub Desktop.
5th June 2019 - Ticking boxes

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.

  1. model.data() returns not only data, but visuals too such as DecorationRole, FontRole and BackgroundRole. Having said that, it's arguable whether DisplayRole is any less graphical.
  2. 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.
  3. QStandardItemModel resides in QtGui, presumably because it takes the above concept even further, with concepts like appendRow and appendColumn.

QDataWidgetMapper and Delegate

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_()

Simple Version Overruling

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.

launchapp_4

The way it works is this.

  1. A context is resolved as per-usual
  2. On launch of a command, an override is translated into a Variant
  3. The variant replaces its corresponding variant in the context
  4. 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.


PATH visualisation

Some environment variables are paths and are difficult to visualise as one long string. So now those are made into lists.

launchapp_5


Dock Management

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.

launchapp_6


Default Version

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.

launchapp2_7


Multiple Docks

Now you can configure whether you want more than a single dock visible at any given time.

launchapp_8


Advanced Controls

LA2 is designed for both artists and developers. To avoid overwhelming the artist, advanced controls are now hidden behind a "Show Advanced Controls" checkbox.

launchapp_9


Todos

With MVC temporarily put aside, I managed to tick a few boxes.

  1. Override Let the user override a package version with any arbitrary alternative
  2. PATH-like children Make list out of PATH-like variables for QJsonModel, so the user can browse those more easily
  3. Isolate Tab Ctrl+click on tab to hide all others
  4. Reset Override Right+click a package version to reset it to its original value
  5. 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.
  6. 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.
  7. 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).
  8. 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!
  9. Edited indicator visually indicate whether a package is manually added or edited

Tomorrow

Keep ticking those boxes.

  1. Right-click open path to package for debugging the package.py on disk (if stored on filesystem, could also come from e.g. database)
  2. Disable package Right-click to temporarily disable the inclusion of a package
  3. Total number of packages Spot total number at a glance
  4. Override Environment Append/subtract variables and contents of variables, such as adding more paths to PYTHONPATH
  5. App Tab Improvements. This really needs some love and is quite bare at the moment.
    1. Show available "tools" for an application, e.g. maya and mayapy for the Maya package, including terminal for almost every other package, to launch a plain shell in the desired environment.
    2. Show already-running commands, like how many Maya's there are
    3. Show environment overrides for a given application
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment