Skip to content

Instantly share code, notes, and snippets.

@mottosso
Last active June 6, 2019 18:13
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/71f8a84db7f230c2f4075e459c483d3d to your computer and use it in GitHub Desktop.
Save mottosso/71f8a84db7f230c2f4075e459c483d3d to your computer and use it in GitHub Desktop.
6th June 2019 - Ticking boxes pt. 2

Today I continued ticking boxes.


Git Revision as Version Number

I've made the number of total commits visible in the version of launchapp2.

$ python -m launchapp2 --version
1.1.23

Where 23 is the number of commits made to the current branch. This number is then embedded into the release version, as launchapp2/__version__.py, such that it can be deployed and used without Git.

The current implementation has a few caveats.

  • Switching to another branch, e.g. dev, resets the count to 0
  • Incrementing the major or minor version does not reset the count, which is should

What I'd like to have happen is for the increment to restart per minor version. That is, 1.0 starts at 0, so does 1.1 and so forth. For that, I think we might need a branch per minor version, which may or may not be desirable.

Regardless, the benefit is that now it's no longer necessary to increment the patch version. Now a new commit implies an increment to the patch version, which I think makes sense, is expected and help avoid needless manual increments.


Override Bug

I noticed I had broken the override persistence yesterday, and fixed that. But it really isn't pretty. I'd like to return to implementing all data in one model once I'm finished with the todo-list. But until then, the functionality is there, despite not being particularly extensible or maintainable.

I also neglected to point out that this feature is currently incompatible with the command-line. I'd like for everything achievable through the GUI to also be possible via the CLI, but this currently is not supported by Rez.

Here is where I'm investigating whether it's suitable for inclusion.

If not, I'll have to find another way, or accept the fact that you can't override via the CLI.


Open package location

On rare occasions, especially during package development, you need to know where a particualr package is actually coming from. It could reside on any of the entries in REZ_PACKAGE_PATH, like how a Python module could come from any entry on PYTHONPATH if one package exists in more than one location.

So now you can right-click and get to the bottom of that.

launchapp2_openfile


Disable Package

Sometimes, one or more packages can cause issues and you'd like experiment with what it would be like if it wasn't part of the resolved context. Now you can right-click and disable any package. Like with overrides, a disabled package is global to the application and applies to any application or project.

launchapp_disable

One of the things I noticed as a result of this is that we'd like to be able to right-click to open a package location even when a package has been overridden. Because the override isn't actually applied until you try and launch an application, the location actually opened is from the original version.

To solve this, we would need to perform this override and in-place, which would have been fine except for actually finding this package can take a moment depending on I/O. Will need to think about how to best approach this. Spontaneously, I'd imagine it being OK for the user to right-click override and see a "Applying override.." message as it works to perform the operation in the background. There's no need to wait around for it to finish, however it would introduce a need to delay launch of an application until all overrides have been accounted for, like a sort of queue. Not impossible, but it would add to the complexity of the application, for perhaps very little added benefit.

Another caveat with this feature, as a result of this particular implementation being relatively naive, is that you can't both override and disable a package at the same time. Overriding will re-enable, and disabling will remove the override. On top of this, we also want the Environment tab to reflect the resulting environment for this new package list.

Both of these will be addressed once I've gotten a better hold of MVC and overall model for the application, any is one of the reason I wanted it done ahead of time in the first place..


Total Count

Sometimes, the number of packages for a given show can reach tens to hundreds and it can be difficult to grasp whether a change to, say the project requirements, affected the total number of packages involved.

Now you can spot the total count, along with how many are overridden and disabled, without having to scroll through and look for them.

launchapp_totalcount

The only thing to note here from a development perspective is that the status updates dynamically as expected, but does so by listening to two signals which won't be relevant once all data resides in a single model.

  1. modelReset which is emitted whenever we change app. This won't happen at all with the mono-model.
  2. dataChanged which is emitted when overriding or disabling. This is too generic for a complete model, as it would fire for a change to any item in the model. For now, the packages have their own model, which works as expected.

Both of those are implementation details we can address in time.


Overriding Package Caveat

One of the ways in which the current implementation is flawed is that it presumes there is only ever 1 variant. This isn't always true. For example, the python package could and likely do exists for both Windows and Linux, in which case it would have a variant for each. If you override python at the moment, it'll pick one of those at random (and spit out a warning).

We can address this by resolving a request, rather than fetching a package explicitly. For example, the below example would fetch the relevant package for Python.

replacement = next(rez.packages_.iter_packages("python", "3.6"))
replacement = next(replacement.iter_variants())

But you'll notice, on fetching a variant, I simply ask for the first one. This may or may not be the right one for the platform.

So intead, we can do this.

replacement = rez.resolved_context.ResolvedContext(["python-3.6"]).resolved_packages[0]

Now we're leveraging Rez to resolve a single package, taking into account any implicit packages relevant for a working variant. The consequence of this however is that resolving a context is more expensive than simply fetching a package off disk. So I've accounted for this by fetching everything in a separate thread.


Global Tooltip

Tooltips can be useful for when you're interested to learn more about a particular widget or item in a list of items. In some applications, tooltips also appear in the status bar, which can be an even quicker way of finding information if what you're searching for a particular widget, rather than wanting to know more about one you've already found.

Now you've got both.

launchapp_tooltip

There were a few ways of implementing this. I wanted the status bar to update interactively as the user hovered over items and so I could have implemented onEnterEvent for relevant widgets and updated the status bar from there. But that would have required an additional method override for every relevant widget, and wouldn't account for tooltips coming from non-widget items, like from the list of applications.

So instead I installed a global event filter to listen on whenever the mouse moved within the window. If it should happen to move over a widget, we'll read that widgets tooltip and use it to update the status bar.

Even this has a few options, with their own pros and cons.

  1. Implement mouseMoveEvent on the top-level window
  2. Install an event filter on top-level Window, and listen for MouseMove
  3. Install an event filter on application, and listen for MouseMove
  4. Install an event filter on either, but listen for ToolTip event

(1) isn't feasible, as child widgets can block these from triggering, even with setMouseTracking(True).

(2) is better, but children are still able to block events from reaching the parent.

(3) Works well, but comes at the cost of catching absolutely every event in the application - including paint events which can trigger thousands of times per second - only to filter out the ones relevant to us. It also means setting the toolTip more often than necessary, as moving the mouse within the surface of one widget would re-trigger the statusbar update. Performance doesn't appear to be an issue, so this is what I went with.

(4) is heavily specialised to capture only the events that matter to us, however they also retain the delay between hovering and showing the message which makes them unsuitable for this purpose.

The current implementation is small enough to include here in full.

class Window(...):
    ...

    # Enable mouse tracking for tooltips
    QtWidgets.QApplication.instance().installEventFilter(self)

    def eventFilter(self, obj, event):
        """Forward tooltips to status bar whenever the mouse moves"""
        if event.type() == QtCore.QEvent.MouseMove:
            try:
                self.statusBar().showMessage(obj.toolTip(), timeout=2000)
            except (AttributeError, IndexError):
                pass

        # Forward the event to subsequent listeners
        return False

PyQt5 and setIcon

On running the GUI with PySide and PyQt5, I noticed an unexpected crash. PySide instakilled, and wouldn't even tell my why, but PyQt5 kindly let me know that QPushButton.setIcon, unlike PySide2, isn't able to take a QPixmap for argument.


QStatusBar.setMessage(timeout=1000)

Interestingly, I found another difference between PyQt and PySide, in that PyQt doesn't support timeout as a keyword argument to setMessage.

As a result, the GUI is now yet again compatible with Python 2 and 3, along with all Qt.py bindings.


Overriding Environment

I was going to implement this, but it's such an edge-case that I don't think we'll be needing this.


Ticked Boxes

  1. Automatic patch-version Associate a release to a Git commit, to automatically increment a new release if made from a newer commit.
  2. Right-click open path to package for debugging the package.py on disk (if stored on filesystem, could also come from e.g. database)
  3. Disable package Right-click to temporarily disable the inclusion of a package
  4. Total number of packages Spot total number at a glance
  5. Secure Package Override Ensure the correct variant is picked up on overriding a package
  6. Global tooltip As you hover over any tooltip enabled widget, display this in the statusbar
  7. Clarify meaning of the icons up-top Via e.g. status-bar help or labels underneath icons
  8. Developer Mode setting To hide/unhide various advanced features, such as selecting a version of a project package
  9. Visually highlight edited requirements
  10. List for PATH-like varibales Make PATH variables into lists in QJsonModel
  11. Override Environment Append/subtract variables and contents of variables, such as adding more paths to PYTHONPATH

Tomorrow

I'll revisit the App Tab, turn it from a rather empty and wasteful area in into an actually useful thing, with available tools for a given app, what instances of the command is already running along with the ability to kill and organise them, along with which overrides are currently in action, along with the ability to disable those.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment