diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0205d62..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pyc -.DS_Store diff --git a/UNLICENSE b/UNLICENSE deleted file mode 100644 index 3816d18..0000000 --- a/UNLICENSE +++ /dev/null @@ -1,26 +0,0 @@ -Copyright 2012 Ben Dickson (dbr) - -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to diff --git a/data_test.py b/data_test.py deleted file mode 100644 index 34257a2..0000000 --- a/data_test.py +++ /dev/null @@ -1 +0,0 @@ -menu_items = ['Image/Read', 'Image/Write', 'Image/Constant', 'Image/CheckerBoard', 'Image/ColorBars', 'Image/ColorWheel', 'Image/CurveTool', 'Image/Viewer', 'Draw/Roto', 'Draw/RotoPaint', 'Draw/Dither', 'Draw/DustBust', 'Draw/Grain', 'Draw/ScannedGrain', 'Draw/Glint', 'Draw/Grid', 'Draw/Flare', 'Draw/LightWrap', 'Draw/MarkerRemoval', 'Draw/Noise', 'Draw/Radial', 'Draw/Ramp', 'Draw/Rectangle', 'Draw/Sparkles', 'Draw/Text', 'Time/Add 3:2 pulldown', 'Time/Remove 3:2 pulldown', 'Time/AppendClip', 'Time/FrameBlend', 'Time/FrameHold', 'Time/FrameRange', 'Time/OFlow', 'Time/Retime', 'Time/TemporalMedian', 'Time/TimeBlur', 'Time/NoTimeBlur', 'Time/TimeEcho', 'Time/TimeOffset', 'Time/TimeWarp', 'Channel/Shuffle', 'Channel/ShuffleCopy', 'Channel/Copy', 'Channel/ChannelMerge', 'Channel/Add', 'Channel/Remove', 'Color/Math/Add', 'Color/Math/Multiply', 'Color/Math/Gamma', 'Color/Math/ClipTest', 'Color/Math/ColorMatrix', 'Color/Math/Expression', 'Color/3D LUT/CMSTestPattern', 'Color/3D LUT/GenerateLUT', 'Color/3D LUT/Vectorfield (Apply 3D LUT)', 'Color/Clamp', 'Color/ColorLookup', 'Color/Colorspace', 'Color/ColorTransfer', 'Color/ColorCorrect', 'Color/Crosstalk', 'Color/Exposure', 'Color/Grade', 'Color/Histogram', 'Color/HistEQ', 'Color/HueCorrect', 'Color/HueShift', 'Color/HSVTool', 'Color/Invert', 'Color/Log2Lin', 'Color/PLogLin', 'Color/MinColor', 'Color/Posterize', 'Color/RolloffContrast', 'Color/Saturation', 'Color/Sampler', 'Color/SoftClip', 'Color/Toe', 'Color/Truelight', 'Filter/Blur', 'Filter/Bilateral', 'Filter/BumpBoss', 'Filter/Convolve', 'Filter/Defocus', 'Filter/DegrainBlue', 'Filter/DegrainSimple', 'Filter/Denoise', 'Filter/DirBlur', 'Filter/EdgeBlur', 'Filter/EdgeDetect', 'Filter/Emboss', 'Filter/Erode (fast)', 'Filter/Erode (filter)', 'Filter/Erode (blur)', 'Filter/Glow', 'Filter/GodRays', 'Filter/Laplacian', 'Filter/LevelSet', 'Filter/Matrix...', 'Filter/Median', 'Filter/MotionBlur2D', 'Filter/MotionBlur3D', 'Filter/Sharpen', 'Filter/Soften', 'Filter/VectorBlur', 'Filter/VolumeRays', 'Filter/ZBlur', 'Filter/ZSlice', 'Keyer/Difference', 'Keyer/HueKeyer', 'Keyer/IBKGizmo', 'Keyer/IBKColour', 'Keyer/Keyer', 'Keyer/Primatte', 'Keyer/Keylight', 'Keyer/Ultimatte', 'Merge/AddMix', 'Merge/KeyMix', 'Merge/ContactSheet', 'Merge/CopyBBox', 'Merge/CopyRectangle', 'Merge/Dissolve', 'Merge/LayerContactSheet', 'Merge/Merge', 'Merge/Merges/Plus', 'Merge/Merges/Matte', 'Merge/Merges/Multiply', 'Merge/Merges/In', 'Merge/Merges/Out', 'Merge/Merges/Screen', 'Merge/Merges/Max', 'Merge/Merges/Min', 'Merge/Merges/Absminus', 'Merge/MergeExpression', 'Merge/Switch', 'Merge/TimeDissolve', 'Merge/Premult', 'Merge/Unpremult', 'Merge/Blend', 'Merge/ZMerge', 'Transform/Transform', 'Transform/TransformMasked', 'Transform/Card3D', 'Transform/AdjustBBox', 'Transform/BlackOutside', 'Transform/CameraShake', 'Transform/Crop', 'Transform/CornerPin', 'Transform/SphericalTransform', 'Transform/IDistort', 'Transform/LensDistortion', 'Transform/Mirror', 'Transform/Position', 'Transform/Reformat', 'Transform/Reconcile3D', 'Transform/PointsTo3D', 'Transform/PlanarTracker', 'Transform/Tracker', 'Transform/TVIScale', 'Transform/GridWarp', 'Transform/SplineWarp', 'Transform/Stabilize', 'Transform/STMap', 'Transform/Tile', '3D/Axis', '3D/Geometry/Card', '3D/Geometry/Cube', '3D/Geometry/Cylinder', '3D/Geometry/Modeler', '3D/Geometry/PointCloudGenerator', '3D/Geometry/PoissonMesh', '3D/Geometry/Sphere', '3D/Geometry/ReadGeo', '3D/Geometry/WriteGeo', '3D/Lights/Light', '3D/Lights/Point', '3D/Lights/Direct', '3D/Lights/Spot', '3D/Lights/Environment', '3D/Modify/TransformGeo', '3D/Modify/MergeGeo', '3D/Modify/CrosstalkGeo', '3D/Modify/DisplaceGeo', '3D/Modify/GeoSelect', '3D/Modify/LookupGeo', '3D/Modify/LogGeo', '3D/Modify/Normals', '3D/Modify/ProceduralNoise', '3D/Modify/RadialDistort', '3D/Modify/Trilinear', '3D/Modify/UVProject', '3D/Shader/ApplyMaterial', '3D/Shader/BasicMaterial', '3D/Shader/FillMat', '3D/Shader/MergeMat', '3D/Shader/BlendMat', '3D/Shader/Project3D', '3D/Shader/Diffuse', '3D/Shader/Emission', '3D/Shader/Phong', '3D/Shader/Specular', '3D/Shader/Displacement', '3D/Shader/RenderMan/Reflection', '3D/Shader/RenderMan/Refraction', '3D/Camera', '3D/CameraTracker', '3D/DepthGenerator', '3D/ProjectionSolver', '3D/Scene', '3D/ScanlineRender', '3D/RenderMan/PrmanRender', 'Particles/ParticleEmitter', 'Particles/ParticleBounce', 'Particles/ParticleCurve', 'Particles/ParticleDirectionalForce', 'Particles/ParticleDrag', 'Particles/ParticleExpression', 'Particles/ParticleMerge', 'Particles/ParticleMotionAlign', 'Particles/ParticleGravity', 'Particles/ParticleLookAt', 'Particles/ParticlePointForce', 'Particles/ParticleSpeedLimit', 'Particles/ParticleSpawn', 'Particles/ParticleTurbulence', 'Particles/ParticleVortex', 'Particles/ParticleWind', 'Particles/ParticleSettings', 'Particles/ParticleToGeo', 'Deep/DeepColorCorrect', 'Deep/DeepCrop', 'Deep/DeepExpression', 'Deep/DeepFromFrames', 'Deep/DeepFromImage', 'Deep/DeepHoldout', 'Deep/DeepMerge', 'Deep/DeepRead', 'Deep/DeepRecolor', 'Deep/DeepReformat', 'Deep/DeepSample', 'Deep/DeepToImage', 'Deep/DeepToPoints', 'Deep/DeepTransform', 'Views/Stereo/Anaglyph', 'Views/Stereo/MixViews', 'Views/Stereo/SideBySide', 'Views/Stereo/ReConverge', 'Views/JoinViews', 'Views/OneView', 'Views/ShuffleViews', 'Views/Split and Join', 'MetaData/ViewMetaData', 'MetaData/CompareMetaData', 'MetaData/ModifyMetaData', 'MetaData/CopyMetaData', 'MetaData/AddTimeCode', 'ToolSets/Create', 'ToolSets/Particles/P_DustHit', 'ToolSets/Particles/P_FogBox', 'ToolSets/Particles/P_RainBox', 'ToolSets/Particles/P_SnowBox', 'ToolSets/Particles/P_Sparks', 'ToolSets/Particles/P_Streaky', 'ToolSets/Particles/P_Trail', 'ToolSets/Particles/P_VolumetricLight', 'ToolSets/Particles/P_Waveform', 'Other/AudioRead', 'Other/Assert', 'Other/Backdrop', 'Other/DiskCache', 'Other/Dot', 'Other/Input', 'Other/Output', 'Other/NoOp', 'Other/PostageStamp', 'Other/Group', 'Other/Precomp', 'Other/StickyNote', 'Other/All plugins/Update', 'FurnaceCore/F_Align', 'FurnaceCore/F_DeFlicker2', 'FurnaceCore/F_Kronos', 'FurnaceCore/F_MatchGrade', 'FurnaceCore/F_MotionBlur', 'FurnaceCore/F_ReGrain', 'FurnaceCore/F_RigRemoval', 'FurnaceCore/F_Steadiness', 'FurnaceCore/F_VectorGenerator', 'FurnaceCore/F_WireRemoval'] diff --git a/imgs/nuke_tab.png b/imgs/nuke_tab.png deleted file mode 100644 index 803b959..0000000 Binary files a/imgs/nuke_tab.png and /dev/null differ diff --git a/imgs/tabtabtab.png b/imgs/tabtabtab.png deleted file mode 100644 index 50c3a0e..0000000 Binary files a/imgs/tabtabtab.png and /dev/null differ diff --git a/readme.md b/readme.md index 8948640..3c9d969 100644 --- a/readme.md +++ b/readme.md @@ -1,180 +1,17 @@ -"tabtabtab", an alternative "tab node creator thingy" for [The -Foundry's Nuke](http://www.thefoundry.co.uk/products/nuke) +# tabtabtab-nuke (deprecated) -It does substring matching (so "blr" matches "blur"), and weights your -most used nodes first (so if I make the useful "Add [math]" node -often, it appears higher in the list than "Add 3:2 pulldown") +> **This fork is no longer maintained here.** +> +> Development has moved to a standalone repository: +> **[charlesangus/tabtabtab-nuke](https://github.com/charlesangus/tabtabtab-nuke)** -# Installation +--- -Put tabtabtab on PYTHONPATH or NUKE_PATH somewhere (maybe in `~/.nuke/`) +The active fork includes: - mkdir -p ~/.nuke - cd ~/.nuke - curl -O https://raw.github.com/dbr/tabtabtab-nuke/master/tabtabtab.py +- Multi-monitor fix (popup opens under the mouse on the correct screen) +- PySide6 support (Nuke 15+) +- Node icons and per-category colour blocks +- CI/CD: automated tests on push, versioned release `.zip` on tag -Then to your `~/.nuke/menu.py` add: - - def ttt(): - import tabtabtab - m_graph = nuke.menu("Node Graph") - m_graph.addCommand("Tabtabtab", tabtabtab.main, "Tab") - - try: - ttt() - except Exception: - import traceback - traceback.print_exc() - - -The original menu will still be available via "Ctrl+Tab". You can -change the last "Tab" argument to another shortcut if you wish. - - -## Notes - -This requires Nuke 6.3v5 or higher (for the integrated PySide module) - -For older versions of 6.3, you must have -[a custom PyQt installation][pyqtinstall]. For 6.2, see -[Dylan Palmboom's tabtabtabLegacy fork][legacy], which may also work -in even more ancient version. - -[pyqtinstall]: http://docs.thefoundry.co.uk/nuke/63/pythondevguide/custom_panels.html#extending-nuke-with-pyqt -[legacy]: http://www.nukepedia.com/gizmos/python-scripts/ui/tabtabtablegacy/ - -Relevant Foundry bug-id's: - -* [Fixed in Nuke 6.3v8] Bug #23576, "Segmentation Fault on Nuke exit after using a custom -PySide window" (causes a mostly harmless "Segmentation fault" message -on exiting Nuke) - -* Feature #11662 to get this functionality integrated into Nuke - -In order to support Nuke 9, you need to use a different snippet in -your `menu.py`, adding the tabtabtab call to the `Node Graph` instead -of the Edit menu (see the updated installation instructions). The -same snippet works in older verisons of Nuke. - -## More elabourate description - -With the default "tab thing", you press tab, start typing a node name -(like "Blu") and you get a list of nodes that start with "Blu" (like -"Blur"): - -![Nuke's builtin tab thing](imgs/nuke_tab.png) - -"tabtabtab" works very similarly, but does substring matching: - -![tabtabtab](imgs/tabtabtab.png) - -In this example, "tra" finds "Transform" as you'd hope, but also other -nodes, such as "Trilinear"... This is appearing because the letters -"tra" can be found in order, "TRilineAr". - -While this seems like it might cause lots of useless results, you -quickly memorise shortcuts like "trear" which matches "TRilinEAR" and -very little else. More usefully, "trage" will uniquely match "TRAnsformGEo" -in an easy to type way, "rdg" matches "ReaDGeo" - -The first letter must match, so "geo" will match "GeoSelect", but not -"ReadGeo". However "rgeo" will match "ReadGeo" - -## Weighting - -Each time you create a node, it's "weight" is increase - the higher -the weight, the higher the node appears in the list. - -This means if you create lots of "Add [math]" nodes, it will be -weighted highly, so all you might need to type is tab then "a", and it -will be at the top of the list. - -The nodes weight is shown by the block to the right of the node - the -more green, the higher the weighting - -## Matching menu location - -If you start typing a shortcut, it will only match the part before the -"[Filter/SubMenu]" (e.g "blf" will not match "Blur [Filter]") - -However if you type either "[" or two spaces, you can match against -the menu location (the part in "[...]") - -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]" - -## Change log - -* `v1.0` - * Basically working - -* `v1.1` - * Node weights are saved - -* `v1.2` - * Window appears under cursor - -* `v1.3` - * Created node remains selected between tab's, meaning "tabtab" - creates the previously node - * Clicking a node in the list creates it - * Window doesn't go offscreen if cursor is near edge - -* `v1.4` - - * Blocks Nuke UI when active. This greatly improves usability when - the weights are slow to load (e.g in heavy Nuke script, or slow - home-dir access), as it prevents key-presses intended for - tabtabtab from being handled by Nuke (possibly creating - new/modifying nodes etc) - * Up/down arrow keys cycle correctly - * Indicator blocks now actually indicate node weights, instead of - always being green. The blocks are also now narrower, which looks - nicer - * Prevent vertical scrollbar (reduced number of shown items to 15) - * Node weights are loaded on every invokation, preventing - overwriting of values with multiple Nuke instances - -* `v1.5` - - * Fixes crash-on-exit for Nuke 6.3v8 (as Nuke bug #23576 is closed) - * Window now closes properly - -* `v1.6` - - * Code to add to `menu.py` more robust, so tabtabtab errors will - never prevent Nuke from starting - - * Search string starting with space will disable the non-consecutive - searching, so `[tab][space]scen` will create a `Scene` instead of - a highly weight `ScanlineRender` - - * Exposes menu items in `nuke.menu("Nuke")` along with the nodes. - Meaning items in the "File" menu etc are exposed, for example - `[tab]exit` will be the same as "File > Exit" - -* `v1.7` - - * `ToolSets/Delete` submenu is excluded from tabtabtab. - [Github issue #6](https://github.com/dbr/tabtabtab-nuke/issues/6) - - * Document that `Ctrl+Tab` opens the original tab menu - - * Fixed bug which caused the node list to stop updating - - [Github issue #10](https://github.com/dbr/tabtabtab-nuke/issues/10) - - * Fixed bug where "last used node" might have matched a different - node (contrived example: the restored `Blur [Filter]` search text - might have matched the more highly weighted `Blur2 [Filter]`) - -* `v1.8` - - * Installation instructions updated to support Nuke 9 - - * Weights file no longer overwritten if it fails to load for some - reason. - [Github issue #13](https://github.com/dbr/tabtabtab-nuke/issues/13) - - * Support PySide2 +Please open issues and pull requests on the new repo. diff --git a/tabtabtab.py b/tabtabtab.py deleted file mode 100644 index d44fa7b..0000000 --- a/tabtabtab.py +++ /dev/null @@ -1,575 +0,0 @@ -"""Alternative "tab node creator thingy" for The Foundry's Nuke - -homepage: https://github.com/dbr/tabtabtab-nuke -license: http://unlicense.org/ -""" - -__version__ = "1.8" - -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 PyQt4 import QtCore, QtGui - from PyQt4.QtCore import Qt - QtCore.Signal = QtCore.pyqtSignal - - -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) - - >>> found = find_menu_items(nuke.menu("Nodes")) - >>> found.sort() - >>> found[:5] - ['3D/Axis', '3D/Camera', '3D/CameraTracker', '3D/DepthGenerator', '3D/Geometry/Card'] - """ - import nuke - - found = [] - - mi = menu.items() - for i in mi: - if isinstance(i, nuke.Menu): - # Sub-menu, recurse - mname = i.name().replace("&", "") - subpath = "/".join(x for x in (_path, mname) if x is not None) - - if "ToolSets/Delete" in subpath: - # Remove all ToolSets delete commands - continue - - sub_found = find_menu_items(menu = i, _path = subpath) - found.extend(sub_found) - elif isinstance(i, nuke.MenuItem): - if i.name() == "": - # Skip dividers - continue - if i.name().startswith("@;"): - # Skip hidden items - continue - - subpath = "/".join(x for x in (_path, i.name()) if x is not None) - found.append({'menuobj': i, 'menupath': subpath}) - - return found - - -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" - "m2" can be found in "move2d", but not "matchmove" - - >>> nonconsec_find("m2", "move2d") - True - >>> nonconsec_find("m2", "matchmove") - False - - Anchored ensures the first letter matches - - >>> nonconsec_find("atch", "matchmove", anchored = False) - True - >>> nonconsec_find("atch", "matchmove", anchored = True) - False - >>> nonconsec_find("match", "matchmove", anchored = True) - True - - If needle starts with a string, non-consecutive searching is disabled: - - >>> nonconsec_find(" mt", "matchmove", anchored = True) - False - >>> nonconsec_find(" ma", "matchmove", anchored = True) - True - >>> nonconsec_find(" oe", "matchmove", anchored = False) - False - >>> nonconsec_find(" ov", "matchmove", anchored = False) - True - """ - - if "[" not in needle: - haystack = haystack.rpartition(" [")[0] - - if len(haystack) == 0 and len(needle) > 0: - # "a" is not in "" - return False - - elif len(needle) == 0 and len(haystack) > 0: - # "" is in "blah" - return True - - elif len(needle) == 0 and len(haystack) == 0: - # ..? - return True - - - # Turn haystack into list of characters (as strings are immutable) - haystack = [hay for hay in str(haystack)] - - if needle.startswith(" "): - # "[space]abc" does consecutive search for "abc" in "abcdef" - if anchored: - if "".join(haystack).startswith(needle.lstrip(" ")): - return True - else: - if needle.lstrip(" ") in "".join(haystack): - return True - - if anchored: - if needle[0] != haystack[0]: - return False - else: - # First letter matches, remove it for further matches - needle = needle[1:] - del haystack[0] - - for needle_atom in needle: - try: - needle_pos = haystack.index(needle_atom) - except ValueError: - return False - else: - # Dont find string in same pos or backwards again - del haystack[:needle_pos + 1] - return True - - -class NodeWeights(object): - def __init__(self, fname = None): - self.fname = fname - self._weights = {} - self._successful_load = False - - def load(self): - if self.fname is None: - return - - def _load_internal(): - import json - if not os.path.isfile(self.fname): - 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 - try: - _load_internal() - self._successful_load = True - except Exception: - 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" - 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) - return - - def _save_internal(): - import json - ndir = os.path.dirname(self.fname) - if not os.path.isdir(ndir): - try: - os.makedirs(ndir) - except OSError, e: - if e.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) - f.close() - - # Catch any errors, print traceback and continue - try: - _save_internal() - except Exception: - print "Error saving node weights" - import traceback - traceback.print_exc() - - def get(self, k, default = 0): - if len(self._weights.values()) == 0: - maxval = 1.0 - else: - maxval = max(self._weights.values()) - maxval = max(1, maxval) - maxval = float(maxval) - - return self._weights.get(k, default) / maxval - - def increment(self, key): - self._weights.setdefault(key, 0) - self._weights[key] += 1 - - -class NodeModel(QtCore.QAbstractListModel): - def __init__(self, mlist, weights, num_items = 15, filtertext = ""): - super(NodeModel, self).__init__() - - self.weights = weights - self.num_items = num_items - - self._all = mlist - self._filtertext = filtertext - - # _items is the list of objects to be shown, update sets this - self._items = [] - self.update() - - def set_filter(self, filtertext): - self._filtertext = filtertext - self.update() - - def update(self): - filtertext = self._filtertext.lower() - - # Two spaces as a shortcut for [ - filtertext = filtertext.replace(" ", "[") - - scored = [] - 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]) - - if nonconsec_find(filtertext, uiname.lower(), anchored=True): - # Matches, get weighting and add to list of stuff - score = self.weights.get(n['menupath']) - - scored.append({ - 'text': uiname, - 'menupath': n['menupath'], - 'menuobj': n['menuobj'], - 'score': score}) - - # Store based on scores (descending), then alphabetically - s = sorted(scored, key = lambda k: (-k['score'], k['text'])) - - self._items = s - self.modelReset.emit() - - def rowCount(self, parent = QtCore.QModelIndex()): - return min(self.num_items, len(self._items)) - - def data(self, index, role = Qt.DisplayRole): - if role == Qt.DisplayRole: - # Return text to display - raw = self._items[index.row()]['text'] - return raw - - elif role == Qt.DecorationRole: - weight = self._items[index.row()]['score'] - - hue = 0.4 - sat = weight - - if index.row() % 2 == 0: - col = QtGui.QColor.fromHsvF(hue, sat, 0.9) - else: - col = QtGui.QColor.fromHsvF(hue, sat, 0.8) - - pix = QtGui.QPixmap(6, 12) - pix.fill(col) - return pix - - elif role == Qt.BackgroundRole: - 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 - - def getorig(self, selected): - # TODO: Is there a way to get this via data()? There's no - # Qt.DataRole or something (only DisplayRole) - - if len(selected) > 0: - # Get first selected index - selected = selected[0] - - else: - # Nothing selected, get first index - selected = self.index(0) - - # TODO: Maybe check for IndexError? - selected_data = self._items[selected.row()] - return selected_data - - -class TabyLineEdit(QtWidgets.QLineEdit): - pressed_arrow = QtCore.Signal(str) - cancelled = QtCore.Signal() - - - def event(self, event): - """Make tab trigger returnPressed - - Also emit signals for the up/down arrows, and escape. - """ - - is_keypress = event.type() == QtCore.QEvent.KeyPress - - if is_keypress and event.key() == QtCore.Qt.Key_Tab: - # Can't access tab key in keyPressedEvent - self.returnPressed.emit() - return True - - elif is_keypress and event.key() == QtCore.Qt.Key_Up: - # These could be done in keyPressedEvent, but.. this is already here - self.pressed_arrow.emit("up") - return True - - elif is_keypress and event.key() == QtCore.Qt.Key_Down: - self.pressed_arrow.emit("down") - return True - - elif is_keypress and event.key() == QtCore.Qt.Key_Escape: - self.cancelled.emit() - return True - - else: - return super(TabyLineEdit, self).event(event) - - -class TabTabTabWidget(QtWidgets.QDialog): - def __init__(self, on_create = None, parent = None, winflags = None): - super(TabTabTabWidget, self).__init__(parent = parent) - if winflags is not None: - self.setWindowFlags(winflags) - - self.setMinimumSize(200, 300) - self.setMaximumSize(200, 300) - - # Store callback - self.cb_on_create = on_create - - # Input box - self.input = TabyLineEdit() - - # Node weighting - 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")) - - # List of stuff, and associated model - self.things_model = NodeModel(nodes, weights = self.weights) - self.things = QtWidgets.QListView() - self.things.setModel(self.things_model) - - # Add input and items to layout - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.input) - layout.addWidget(self.things) - - # Remove margins - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - # Update on text change - 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 - - # Create node when enter/tab is pressed, or item is clicked - self.input.returnPressed.connect(self.create) - self.things.clicked.connect(self.create) - - # When esc pressed, close - self.input.cancelled.connect(self.close) - - # Up and down arrow handling - self.input.pressed_arrow.connect(self.move_selection) - - def under_cursor(self): - def clamp(val, mi, ma): - return max(min(val, ma), mi) - - # Get cursor position, and screen dimensions on active screen - cursor = QtGui.QCursor().pos() - screen = QtWidgets.QDesktopWidget().screenGeometry(cursor) - - # Get window position so cursor is just over text input - xpos = cursor.x() - (self.width()/2) - ypos = cursor.y() - 13 - - # Clamp window location to prevent it going offscreen - xpos = clamp(xpos, screen.left(), screen.right() - self.width()) - ypos = clamp(ypos, screen.top(), screen.bottom() - (self.height()-13)) - - # Move window - self.move(xpos, ypos) - - def move_selection(self, where): - if where not in ["first", "up", "down"]: - raise ValueError("where should be either 'first', 'up', 'down', not %r" % ( - where)) - - first = where == "first" - up = where == "up" - down = where == "down" - - if first: - self.things.setCurrentIndex(self.things_model.index(0)) - return - - cur = self.things.currentIndex() - if up: - new = cur.row() - 1 - if new < 0: - new = self.things_model.rowCount() - 1 - elif down: - new = cur.row() + 1 - count = self.things_model.rowCount() - if new > count-1: - new = 0 - - self.things.setCurrentIndex(self.things_model.index(new)) - - def event(self, event): - """Close when window becomes inactive (click outside of window) - """ - if event.type() == QtCore.QEvent.WindowDeactivate: - self.close() - return True - else: - return super(TabTabTabWidget, self).event(event) - - def update(self, text): - """On text change, selects first item and updates filter text - """ - self.things.setCurrentIndex(self.things_model.index(0)) - self.things_model.set_filter(text) - - def show(self): - """Select all the text in the input (which persists between - show()'s) - - Allows typing over previously created text, and [tab][tab] to - create previously created node (instead of the most popular) - """ - - # Load the weights everytime the panel is shown, to prevent - # overwritting weights from other Nuke instances - self.weights.load() - - # Select all text to allow overwriting - self.input.selectAll() - self.input.setFocus() - - super(TabTabTabWidget, self).show() - - def close(self): - """Save weights when closing - """ - self.weights.save() - super(TabTabTabWidget, self).close() - - def create(self): - # Get selected item - selected = self.things.selectedIndexes() - if len(selected) == 0: - return - - thing = self.things_model.getorig(selected) - - # Store the full UI name of the created node, so it is the - # active node on the next [tab]. Prefix it with space, - # to disable substring matching - if thing['text'].startswith(" "): - prev_string = thing['text'] - else: - prev_string = " %s" % thing['text'] - - self.input.setText(prev_string) - - # Create node, increment weight and close - self.cb_on_create(thing = thing) - self.weights.increment(thing['menupath']) - self.close() - - -_tabtabtab_instance = None -def main(): - global _tabtabtab_instance - - if _tabtabtab_instance is not None: - # TODO: Is there a better way of doing this? If a - # TabTabTabWidget is instanced, it goes out of scope at end of - # function and disappers instantly. This seems like a - # reasonable "workaround" - - _tabtabtab_instance.under_cursor() - _tabtabtab_instance.show() - _tabtabtab_instance.raise_() - return - - def on_create(thing): - try: - thing['menuobj'].invoke() - except ImportError: - print "Error creating %s" % thing - - t = TabTabTabWidget(on_create = on_create, winflags = Qt.FramelessWindowHint) - - # Make dialog appear under cursor, as Nuke's builtin one does - t.under_cursor() - - # Show, and make front-most window (mostly for OS X) - t.show() - t.raise_() - - # Keep the TabTabTabWidget alive, but don't keep an extra - # reference to it, otherwise Nuke segfaults on exit. Hacky. - # https://github.com/dbr/tabtabtab-nuke/issues/4 - import weakref - _tabtabtab_instance = weakref.proxy(t) - - -if __name__ == '__main__': - try: - import nuke - m_edit = nuke.menu("Nuke").findItem("Edit") - m_edit.addCommand("Tabtabtab", main, "Tab") - except ImportError: - # For testing outside Nuke - app = QtGui.QApplication(sys.argv) - main() - app.exec_()