Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ For example, "ax" matches "AddMix [Merge]" and "Axis [3D]". If you
type "ax[3" or "ax 3" (ax-space-space-3) it will only match "Axis
[3D]"

## Dependencies

tabtabtab-nuke now requires [Qt.py](https://github.com/mottosso/Qt.py) which has been added as a compatibility layer.

## Change log

* `v1.0`
Expand Down Expand Up @@ -177,4 +181,4 @@ type "ax[3" or "ax 3" (ax-space-space-3) it will only match "Axis
reason.
[Github issue #13](https://github.com/dbr/tabtabtab-nuke/issues/13)

* Support PySide2
* Uses [Qt.py](https://github.com/mottosso/Qt.py) to support PySide2
137 changes: 68 additions & 69 deletions tabtabtab.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,24 @@
import os
import sys

try:
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtCore import Qt
except ImportError:
try:
from PySide import QtCore, QtGui, QtGui as QtWidgets
from PySide.QtCore import Qt
except ImportError:
import sip
for mod in ("QDate", "QDateTime", "QString", "QTextStream", "QTime", "QUrl", "QVariant"):
sip.setapi(mod, 2)
from Qt import QtCore, QtGui, QtWidgets

from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import Qt
QtCore.Signal = QtCore.pyqtSignal


def find_menu_items(menu, _path = None):
def find_menu_items(menu, _path=None):
"""Extracts items from a given Nuke menu

Returns a list of strings, with the path to each item

Ignores divider lines and hidden items (ones like "@;&CopyBranch" for shift+k)
Ignores divider lines and hidden items
(ones like "@;&CopyBranch" for shift+k)

>>> found = find_menu_items(nuke.menu("Nodes"))
>>> found.sort()
>>> found[:5]
['3D/Axis', '3D/Camera', '3D/CameraTracker', '3D/DepthGenerator', '3D/Geometry/Card']
[
'3D/Axis', '3D/Camera', '3D/CameraTracker',
'3D/DepthGenerator', '3D/Geometry/Card'
]
"""
import nuke

Expand All @@ -53,7 +43,7 @@ def find_menu_items(menu, _path = None):
# Remove all ToolSets delete commands
continue

sub_found = find_menu_items(menu = i, _path = subpath)
sub_found = find_menu_items(menu=i, _path=subpath)
found.extend(sub_found)
elif isinstance(i, nuke.MenuItem):
if i.name() == "":
Expand All @@ -69,7 +59,7 @@ def find_menu_items(menu, _path = None):
return found


def nonconsec_find(needle, haystack, anchored = False):
def nonconsec_find(needle, haystack, anchored=False):
"""checks if each character of "needle" can be found in order (but not
necessarily consecutivly) in haystack.
For example, "mm" can be found in "matchmove", but not "move2d"
Expand Down Expand Up @@ -116,7 +106,6 @@ def nonconsec_find(needle, haystack, anchored = False):
# ..?
return True


# Turn haystack into list of characters (as strings are immutable)
haystack = [hay for hay in str(haystack)]

Expand Down Expand Up @@ -149,7 +138,7 @@ def nonconsec_find(needle, haystack, anchored = False):


class NodeWeights(object):
def __init__(self, fname = None):
def __init__(self, fname=None):
self.fname = fname
self._weights = {}
self._successful_load = False
Expand All @@ -161,31 +150,33 @@ def load(self):
def _load_internal():
import json
if not os.path.isfile(self.fname):
print "Weight file does not exist"
print("Weight file does not exist")
return
f = open(self.fname)
self._weights = json.load(f)
f.close()

# Catch any errors, print traceback and continue
# Catch any errors, print(traceback and continue)
try:
_load_internal()
self._successful_load = True
except Exception:
print "Error loading node weights"
print("Error loading node weights")
import traceback
traceback.print_exc()
self._successful_load = False

def save(self):
if self.fname is None:
print "Not saving node weights, no file specified"
print("Not saving node weights, no file specified")
return

if not self._successful_load:
# Avoid clobbering existing weights file on load error
print "Not writing weights file because %r previously failed to load" % (
self.fname)
print(
"Not writing weights file because %r previously failed to "
"load." % self.fname
)
return

def _save_internal():
Expand All @@ -194,24 +185,24 @@ def _save_internal():
if not os.path.isdir(ndir):
try:
os.makedirs(ndir)
except OSError, e:
if e.errno != 17: # errno 17 is "already exists"
except OSError as err:
if err.errno != 17: # errno 17 is "already exists"
raise

f = open(self.fname, "w")
# TODO: Limit number of saved items to some sane number
json.dump(self._weights, fp = f)
json.dump(self._weights, fp=f)
f.close()

# Catch any errors, print traceback and continue
# Catch any errors, print(traceback and continue)
try:
_save_internal()
except Exception:
print "Error saving node weights"
print("Error saving node weights")
import traceback
traceback.print_exc()

def get(self, k, default = 0):
def get(self, k, default=0):
if len(self._weights.values()) == 0:
maxval = 1.0
else:
Expand All @@ -227,7 +218,7 @@ def increment(self, key):


class NodeModel(QtCore.QAbstractListModel):
def __init__(self, mlist, weights, num_items = 15, filtertext = ""):
def __init__(self, mlist, weights, num_items=15, filtertext=""):
super(NodeModel, self).__init__()

self.weights = weights
Expand All @@ -254,7 +245,9 @@ def update(self):
for n in self._all:
# Turn "3D/Shader/Phong" into "Phong [3D/Shader]"
menupath = n['menupath'].replace("&", "")
uiname = "%s [%s]" % (menupath.rpartition("/")[2], menupath.rpartition("/")[0])
uiname = "%s [%s]" % (
menupath.rpartition("/")[2], menupath.rpartition("/")[0]
)

if nonconsec_find(filtertext, uiname.lower(), anchored=True):
# Matches, get weighting and add to list of stuff
Expand All @@ -267,21 +260,22 @@ def update(self):
'score': score})

# Store based on scores (descending), then alphabetically
s = sorted(scored, key = lambda k: (-k['score'], k['text']))
s = sorted(scored, key=lambda k: (-k['score'], k['text']))

self._items = s
self.modelReset.emit()

def rowCount(self, parent = QtCore.QModelIndex()):
def rowCount(self, parent=QtCore.QModelIndex()):
"""Reimplementation of QtCore.QAbstractListModel virtual method."""
return min(self.num_items, len(self._items))

def data(self, index, role = Qt.DisplayRole):
if role == Qt.DisplayRole:
def data(self, index, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.DisplayRole:
# Return text to display
raw = self._items[index.row()]['text']
return raw

elif role == Qt.DecorationRole:
elif role == QtCore.Qt.DecorationRole:
weight = self._items[index.row()]['score']

hue = 0.4
Expand All @@ -296,19 +290,9 @@ def data(self, index, role = Qt.DisplayRole):
pix.fill(col)
return pix

elif role == Qt.BackgroundRole:
elif role == QtCore.Qt.BackgroundRole:
# Removing unreachable code.
return
weight = self._items[index.row()]['score']

hue = 0.4
sat = weight ** 2 # gamma saturation to make faster falloff

sat = min(1.0, sat)

if index.row() % 2 == 0:
return QtGui.QColor.fromHsvF(hue, sat, 0.9)
else:
return QtGui.QColor.fromHsvF(hue, sat, 0.8)
else:
# Ignore other roles
return None
Expand All @@ -334,7 +318,6 @@ class TabyLineEdit(QtWidgets.QLineEdit):
pressed_arrow = QtCore.Signal(str)
cancelled = QtCore.Signal()


def event(self, event):
"""Make tab trigger returnPressed

Expand All @@ -349,7 +332,7 @@ def event(self, event):
return True

elif is_keypress and event.key() == QtCore.Qt.Key_Up:
# These could be done in keyPressedEvent, but.. this is already here
# These could be done in keyPressedEvent, but..this is already here
self.pressed_arrow.emit("up")
return True

Expand All @@ -366,8 +349,8 @@ def event(self, event):


class TabTabTabWidget(QtWidgets.QDialog):
def __init__(self, on_create = None, parent = None, winflags = None):
super(TabTabTabWidget, self).__init__(parent = parent)
def __init__(self, on_create=None, parent=None, winflags=None):
super(TabTabTabWidget, self).__init__(parent=parent)
if winflags is not None:
self.setWindowFlags(winflags)

Expand All @@ -381,14 +364,17 @@ def __init__(self, on_create = None, parent = None, winflags = None):
self.input = TabyLineEdit()

# Node weighting
self.weights = NodeWeights(os.path.expanduser("~/.nuke/tabtabtab_weights.json"))
self.weights.load() # weights.save() called in close method
self.weights = NodeWeights(
os.path.expanduser("~/.nuke/tabtabtab_weights.json")
)
self.weights.load() # weights.save() called in close method

import nuke
nodes = find_menu_items(nuke.menu("Nodes")) + find_menu_items(nuke.menu("Nuke"))
nodes = find_menu_items(nuke.menu("Nodes"))
nodes += find_menu_items(nuke.menu("Nuke"))

# List of stuff, and associated model
self.things_model = NodeModel(nodes, weights = self.weights)
self.things_model = NodeModel(nodes, weights=self.weights)
self.things = QtWidgets.QListView()
self.things.setModel(self.things_model)

Expand All @@ -405,8 +391,10 @@ def __init__(self, on_create = None, parent = None, winflags = None):
self.input.textChanged.connect(self.update)

# Reset selection on text change
self.input.textChanged.connect(lambda: self.move_selection(where="first"))
self.move_selection(where = "first") # Set initial selection
self.input.textChanged.connect(
lambda: self.move_selection(where="first")
)
self.move_selection(where="first") # Set initial selection

# Create node when enter/tab is pressed, or item is clicked
self.input.returnPressed.connect(self.create)
Expand Down Expand Up @@ -439,8 +427,9 @@ def clamp(val, mi, ma):

def move_selection(self, where):
if where not in ["first", "up", "down"]:
raise ValueError("where should be either 'first', 'up', 'down', not %r" % (
where))
raise ValueError(
"where should be either 'first', 'up', 'down', not %r" % where
)

first = where == "first"
up = where == "up"
Expand All @@ -460,6 +449,11 @@ def move_selection(self, where):
count = self.things_model.rowCount()
if new > count-1:
new = 0
else:
# TODO: Figure out what to do if it is neither up nor down.
raise ValueError(
"where should be either 'first', 'up', 'down', not %r" % where
)

self.things.setCurrentIndex(self.things_model.index(new))

Expand Down Expand Up @@ -521,12 +515,14 @@ def create(self):
self.input.setText(prev_string)

# Create node, increment weight and close
self.cb_on_create(thing = thing)
self.cb_on_create(thing=thing)
self.weights.increment(thing['menupath'])
self.close()


_tabtabtab_instance = None


def main():
global _tabtabtab_instance

Expand All @@ -545,9 +541,12 @@ def on_create(thing):
try:
thing['menuobj'].invoke()
except ImportError:
print "Error creating %s" % thing
print("Error creating %s" % thing)

t = TabTabTabWidget(on_create = on_create, winflags = Qt.FramelessWindowHint)
t = TabTabTabWidget(
on_create=on_create,
winflags=QtCore.Qt.FramelessWindowHint
)

# Make dialog appear under cursor, as Nuke's builtin one does
t.under_cursor()
Expand All @@ -570,6 +569,6 @@ def on_create(thing):
m_edit.addCommand("Tabtabtab", main, "Tab")
except ImportError:
# For testing outside Nuke
app = QtGui.QApplication(sys.argv)
app = QtWidgets.QApplication(sys.argv)
main()
app.exec_()