..."
29 | },
30 | }
31 | When clicking on a button, the accept() signal is emitted, and the button's text is stored under self.selection
32 | """
33 |
34 | def __init__(self, parent, category="", category_dict=None):
35 | super(KeywordHotbox, self).__init__(parent)
36 | category_dict = category_dict or {}
37 |
38 | self.script_editor = parent
39 | self.setWindowFlags(
40 | QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup) # Without self.windowFlags() first, closes as intended
41 |
42 | if not category or "keywords" not in category_dict:
43 | self.reject()
44 | return
45 |
46 | self.category = category
47 | self.category_dict = category_dict
48 | self.selection = ""
49 |
50 | self.initUI()
51 |
52 | # Move hotbox to appropriate position
53 | self.move(QtGui.QCursor().pos() - QtCore.QPoint((self.width() / 2), -6))
54 | self.installEventFilter(self)
55 |
56 | def initUI(self):
57 |
58 | master_layout = QtWidgets.QVBoxLayout()
59 |
60 | # 1. Main part: Hotbox Buttons
61 | for keyword in self.category_dict["keywords"]:
62 | button = KeywordHotboxButton(keyword, self)
63 | button.clicked.connect(partial(self.pressed, keyword))
64 | master_layout.insertWidget(-1, button)
65 |
66 | # 2. ToolTip etc
67 | if "help" in self.category_dict:
68 | category_help = self.category_dict["help"]
69 | else:
70 | category_help = ""
71 |
72 | if nuke.NUKE_VERSION_MAJOR < 11:
73 | master_layout.setContentsMargins(0, 0, 0, 0)
74 | else:
75 | master_layout.setMargin(0)
76 | master_layout.setSpacing(0)
77 |
78 | self.setToolTip("{}
".format(self.category) + category_help)
79 |
80 | self.setStyleSheet('''QToolTip{
81 | border: 1px solid black;
82 | padding: 10px;
83 | }
84 | ''')
85 | self.setLayout(master_layout)
86 | self.adjustSize()
87 |
88 | def pressed(self, keyword=""):
89 | if keyword != "":
90 | self.selection = keyword
91 | self.accept()
92 |
93 | def focusOutEvent(self, event):
94 | self.close()
95 | QtWidgets.QDialog.focusOutEvent(event)
96 |
97 |
98 | class KeywordHotboxButton(QtWidgets.QLabel):
99 | """
100 | Keyword button for the KeywordHotbox. It's really a label, with a selection color and stuff.
101 | """
102 | clicked = QtCore.Signal()
103 |
104 | def __init__(self, name, parent=None):
105 |
106 | super(KeywordHotboxButton, self).__init__(parent)
107 |
108 | self.parent = parent
109 |
110 | if hasattr(parent, 'script_editor') and hasattr(parent.script_editor, 'knob_scripter'):
111 | self.knobScripter = parent.script_editor.knob_scripter
112 | else:
113 | self.knobScripter = None
114 |
115 | self.name = name
116 | self.highlighted = False
117 | self.defaultStyle = self.style()
118 |
119 | self.setMouseTracking(True)
120 | # self.setTextFormat(QtCore.Qt.RichText)
121 | # self.setWordWrap(True)
122 | self.setText(self.name)
123 | self.setHighlighted(False)
124 |
125 | if self.knobScripter:
126 | self.setFont(self.knobScripter.script_editor_font)
127 | else:
128 | font = QtGui.QFont()
129 | font.setFamily("Monospace")
130 | font.setStyleHint(QtGui.QFont.Monospace)
131 | font.setFixedPitch(True)
132 | font.setPointSize(11)
133 | self.setFont(font)
134 |
135 | def setHighlighted(self, highlighted=False):
136 | """
137 | Define the style of the button for different states
138 | """
139 |
140 | # Selected
141 | if highlighted:
142 | # self.setStyle(QtWidgets.QStyleFactory.create('Plastique')) #background:#e90;
143 | self.setStyleSheet("""
144 | border: 0px solid black;
145 | background:#555;
146 | color:#eeeeee;
147 | padding: 6px 4px;
148 | """)
149 |
150 | # Deselected
151 | else:
152 | # self.setStyle(self.defaultStyle)
153 | self.setStyleSheet("""
154 | border: 0px solid #000;
155 | background:#3e3e3e;
156 | color:#eeeeee;
157 | padding: 6px 4px;
158 | """)
159 |
160 | self.highlighted = highlighted
161 |
162 | def enterEvent(self, event):
163 | """ Mouse hovering """
164 | self.setHighlighted(True)
165 | return True
166 |
167 | def leaveEvent(self, event):
168 | """ Stopped hovering """
169 | self.setHighlighted(False)
170 | return True
171 |
172 | def mouseReleaseEvent(self, event):
173 | """
174 | Execute the buttons' self.function (str)
175 | """
176 | if self.highlighted:
177 | self.clicked.emit()
178 | pass
179 | super(KeywordHotboxButton, self).mouseReleaseEvent(event)
180 |
--------------------------------------------------------------------------------
/KnobScripter/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ Utils: KnobScripter's utility functions
3 |
4 | utils.py contains utility functions that can potentially be helpful for multiple ks modules.
5 |
6 | adrianpueyo.com
7 |
8 | """
9 | import nuke
10 |
11 | from KnobScripter import config
12 | try:
13 | if nuke.NUKE_VERSION_MAJOR < 11:
14 | from PySide import QtGui as QtWidgets
15 | else:
16 | from PySide2 import QtWidgets
17 | except ImportError:
18 | from Qt import QtWidgets
19 |
20 | def remove_comments_and_docstrings(source):
21 | """
22 | Returns 'source' minus comments and docstrings.
23 | Awesome function by Dan McDougall
24 | https://github.com/liftoff/pyminifier
25 | TODO check Unused?
26 | """
27 | import cStringIO, tokenize
28 | io_obj = cStringIO.StringIO(source)
29 | out = ""
30 | prev_toktype = tokenize.INDENT
31 | last_lineno = -1
32 | last_col = 0
33 | for tok in tokenize.generate_tokens(io_obj.readline):
34 | token_type = tok[0]
35 | token_string = tok[1]
36 | start_line, start_col = tok[2]
37 | end_line, end_col = tok[3]
38 | ltext = tok[4]
39 | if start_line > last_lineno:
40 | last_col = 0
41 | if start_col > last_col:
42 | out += (" " * (start_col - last_col))
43 | if token_type == tokenize.COMMENT:
44 | pass
45 | elif token_type == tokenize.STRING:
46 | if prev_toktype != tokenize.INDENT:
47 | if prev_toktype != tokenize.NEWLINE:
48 | if start_col > 0:
49 | out += token_string
50 | else:
51 | out += token_string
52 | prev_toktype = token_type
53 | last_col = end_col
54 | last_lineno = end_line
55 | return out
56 |
57 |
58 | def killPaneMargins(widget_object):
59 | if widget_object:
60 | target_widgets = set()
61 | target_widgets.add(widget_object.parentWidget().parentWidget())
62 | target_widgets.add(widget_object.parentWidget().parentWidget().parentWidget().parentWidget())
63 |
64 | for widget_layout in target_widgets:
65 | try:
66 | widget_layout.layout().setContentsMargins(0, 0, 0, 0)
67 | except:
68 | pass
69 |
70 |
71 | def findSE():
72 | for widget in QtWidgets.QApplication.allWidgets():
73 | if widget.metaObject().className() == 'Nuke::NukeScriptEditor':
74 | return widget
75 |
76 |
77 | def findSEInput(se):
78 | children = se.children()
79 | splitter = [w for w in children if isinstance(w, QtWidgets.QSplitter)]
80 | if not splitter:
81 | return None
82 | splitter = splitter[0]
83 | for widget in splitter.children():
84 | if widget.metaObject().className() == 'Foundry::PythonUI::ScriptInputWidget':
85 | return widget
86 | return None
87 |
88 |
89 | def filepath_version_up(filepath,find_next_available=True):
90 | '''
91 | Return versioned up version of filepath.
92 | @param find_next_available: whether to find the next version that doesn't exist, or simply return the version +1
93 | @return: versioned up filepath or False
94 | '''
95 | import re
96 | import os
97 | filepath_re = r"([_.]v)([\d]+)([._]+)"
98 | version_search = re.search(filepath_re, filepath)
99 | if not version_search:
100 | return False
101 | else:
102 | version_str = version_search.groups()[1]
103 | padding = len(version_str)
104 | version = int(version_str)
105 | while True:
106 | new_path = re.sub(filepath_re, "\g<1>"+str(version+1).zfill(padding)+"\g<3>", filepath)
107 | if not find_next_available or not os.path.exists(new_path):
108 | return new_path
109 | version += 1
110 |
111 |
112 | def findSEConsole(se=None):
113 | if not se:
114 | se = findSE()
115 | children = se.children()
116 | splitter = [w for w in children if isinstance(w, QtWidgets.QSplitter)]
117 | if not splitter:
118 | return None
119 | splitter = splitter[0]
120 | for widget in splitter.children():
121 | if widget.metaObject().className() == 'Foundry::PythonUI::ScriptOutputWidget':
122 | return widget
123 | return None
124 |
125 |
126 | def findSERunBtn(se):
127 | children = se.children()
128 | buttons = [b for b in children if isinstance(b, QtWidgets.QPushButton)]
129 | for button in buttons:
130 | tooltip = button.toolTip()
131 | if "Run the current script" in tooltip:
132 | return button
133 | return None
134 |
135 |
136 | def setSEConsoleChanged():
137 | ''' Sets nuke's SE console textChanged event to change knobscripters too. '''
138 | se_console = findSEConsole()
139 | se_console.textChanged.connect(lambda: consoleChanged(se_console))
140 |
141 |
142 | def consoleChanged(self):
143 | ''' This will be called every time the ScriptEditor Output text is changed '''
144 | for ks in config.all_knobscripters:
145 | try:
146 | console_text = self.document().toPlainText()
147 | omit_se_console_text = ks.omit_se_console_text # The text from the console that will be omitted
148 | ks_output = ks.script_output # The console TextEdit widget
149 | if omit_se_console_text == "":
150 | ks_text = console_text
151 | elif console_text.startswith(omit_se_console_text):
152 | ks_text = str(console_text[len(omit_se_console_text):])
153 | else:
154 | ks_text = console_text
155 | ks.omit_se_console_text = ""
156 | ks_output.setPlainText(ks_text)
157 | ks_output.verticalScrollBar().setValue(ks_output.verticalScrollBar().maximum())
158 | except:
159 | pass
160 |
161 |
162 | def relistAllKnobScripterPanes():
163 | """ Removes from config.all_knobscripters the panes that are closed. """
164 | def topParent(qwidget):
165 | parent = qwidget.parent()
166 | if not parent:
167 | return qwidget
168 | else:
169 | return topParent(parent)
170 | for ks in config.all_knobscripters:
171 | if ks.isPane:
172 | if topParent(ks).metaObject().className() != "Foundry::UI::DockMainWindow":
173 | config.all_knobscripters.remove(ks)
174 |
175 |
176 | def getKnobScripter(knob_scripter=None, alternative=True):
177 | """
178 | Return the given knobscripter if it exists.
179 | Otherwise if alternative == True, find+return another one.
180 | If no knobscripters found, returns None.
181 | """
182 | relistAllKnobScripterPanes()
183 | ks = None
184 | if knob_scripter in config.all_knobscripters:
185 | ks = knob_scripter
186 | return ks
187 | elif len(config.all_knobscripters) and alternative:
188 | for widget in config.all_knobscripters:
189 | if widget.metaObject().className() == 'KnobScripterPane' and widget.isVisible():
190 | ks = widget
191 | if not ks:
192 | ks = config.all_knobscripters[-1]
193 | return ks
194 | else:
195 | nuke.message("No KnobScripters found!")
196 | return None
197 |
198 |
199 | def nk_saved_path():
200 | return nuke.root().name().rsplit("_",1)[0] # Ignoring the version if it happens to be there. Doesn't hurt.
201 |
202 | def clear_layout(layout):
203 | if layout is not None:
204 | while layout.count():
205 | child = layout.takeAt(0)
206 | if child.widget() is not None:
207 | child.widget().deleteLater()
208 | elif child.layout() is not None:
209 | clearLayout(child.layout())
--------------------------------------------------------------------------------
/KnobScripter/dialogs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ Dialogs: Main dialog boxes for KnobScripter
3 |
4 | adrianpueyo.com
5 |
6 | """
7 | import nuke
8 | import re
9 |
10 | try:
11 | if nuke.NUKE_VERSION_MAJOR < 11:
12 | from PySide import QtCore, QtGui, QtGui as QtWidgets
13 | from PySide.QtCore import Qt
14 | else:
15 | from PySide2 import QtWidgets, QtGui, QtCore
16 | from PySide2.QtCore import Qt
17 | except ImportError:
18 | from Qt import QtCore, QtGui, QtWidgets
19 |
20 | def ask(question, parent=None, default_yes = True):
21 | msgBox = QtWidgets.QMessageBox(parent=parent)
22 | msgBox.setText(question)
23 | msgBox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
24 | msgBox.setIcon(QtWidgets.QMessageBox.Question)
25 | msgBox.setWindowFlags(msgBox.windowFlags() | Qt.WindowStaysOnTopHint)
26 | if default_yes:
27 | msgBox.setDefaultButton(QtWidgets.QMessageBox.Yes)
28 | else:
29 | msgBox.setDefaultButton(QtWidgets.QMessageBox.No)
30 | reply = msgBox.exec_()
31 | if reply == QtWidgets.QMessageBox.Yes:
32 | return True
33 | return False
34 |
35 |
36 |
37 | class FileNameDialog(QtWidgets.QDialog):
38 | '''
39 | Dialog for creating new... (mode = "folder", "script" or "knob").
40 | '''
41 | def __init__(self, parent = None, mode = "folder", text = ""):
42 | if parent.isPane:
43 | super(FileNameDialog, self).__init__()
44 | else:
45 | super(FileNameDialog, self).__init__(parent)
46 | self.mode = mode
47 | self.text = text
48 |
49 | title = "Create new {}.".format(self.mode)
50 | self.setWindowTitle(title)
51 |
52 | self.initUI()
53 |
54 | def initUI(self):
55 | # Widgets
56 | self.name_label = QtWidgets.QLabel("Name: ")
57 | self.name_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
58 | self.name_lineEdit = QtWidgets.QLineEdit()
59 | self.name_lineEdit.setText(self.text)
60 | self.name_lineEdit.textChanged.connect(self.nameChanged)
61 |
62 | # Buttons
63 | self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
64 | self.button_box.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(self.text != "")
65 | self.button_box.accepted.connect(self.clickedOk)
66 | self.button_box.rejected.connect(self.clickedCancel)
67 |
68 | # Layout
69 | self.master_layout = QtWidgets.QVBoxLayout()
70 | self.name_layout = QtWidgets.QHBoxLayout()
71 | self.name_layout.addWidget(self.name_label)
72 | self.name_layout.addWidget(self.name_lineEdit)
73 | self.master_layout.addLayout(self.name_layout)
74 | self.master_layout.addWidget(self.button_box)
75 | self.setLayout(self.master_layout)
76 |
77 | self.name_lineEdit.setFocus()
78 | self.setMinimumWidth(250)
79 |
80 | def nameChanged(self):
81 | txt = self.name_lineEdit.text()
82 | m = r"[\w]*$"
83 | if self.mode == "knob": # Knobs can't start with a number...
84 | m = r"[a-zA-Z_]+" + m
85 |
86 | if re.match(m, txt) or txt == "":
87 | self.text = txt
88 | else:
89 | self.name_lineEdit.setText(self.text)
90 |
91 | self.button_box.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(self.text != "")
92 | return
93 |
94 | def clickedOk(self):
95 | self.accept()
96 | return
97 |
98 | def clickedCancel(self):
99 | self.reject()
100 | return
101 |
102 |
103 | class TextInputDialog(QtWidgets.QDialog):
104 | '''
105 | Simple dialog for a text input.
106 | '''
107 | def __init__(self, parent = None, name = "", text = "", title=""):
108 | super(TextInputDialog, self).__init__(parent)
109 |
110 | self.name = name # title of textinput
111 | self.text = text # default content of textinput
112 |
113 | self.setWindowTitle(title)
114 |
115 | self.initUI()
116 |
117 | def initUI(self):
118 | # Widgets
119 | self.name_label = QtWidgets.QLabel(self.name+": ")
120 | self.name_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
121 | self.name_lineEdit = QtWidgets.QLineEdit()
122 | self.name_lineEdit.setText(self.text)
123 | self.name_lineEdit.textChanged.connect(self.nameChanged)
124 |
125 | # Buttons
126 | self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
127 | #self.button_box.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(self.text != "")
128 | self.button_box.accepted.connect(self.clickedOk)
129 | self.button_box.rejected.connect(self.clickedCancel)
130 |
131 | # Layout
132 | self.master_layout = QtWidgets.QVBoxLayout()
133 | self.name_layout = QtWidgets.QHBoxLayout()
134 | self.name_layout.addWidget(self.name_label)
135 | self.name_layout.addWidget(self.name_lineEdit)
136 | self.master_layout.addLayout(self.name_layout)
137 | self.master_layout.addWidget(self.button_box)
138 | self.setLayout(self.master_layout)
139 |
140 | self.name_lineEdit.setFocus()
141 | self.setMinimumWidth(250)
142 |
143 | def nameChanged(self):
144 | self.text = self.name_lineEdit.text()
145 |
146 | def clickedOk(self):
147 | self.accept()
148 | return
149 |
150 | def clickedCancel(self):
151 | self.reject()
152 | return
153 |
154 |
155 | class ChooseNodeDialog(QtWidgets.QDialog):
156 | '''
157 | Dialog for selecting a node by its name. Only admits nodes that exist (including root, preferences...)
158 | '''
159 | def __init__(self, parent = None, name = ""):
160 | if parent.isPane:
161 | super(ChooseNodeDialog, self).__init__()
162 | else:
163 | super(ChooseNodeDialog, self).__init__(parent)
164 |
165 | self.name = name # Name of node (will be "" by default)
166 | self.allNodes = []
167 |
168 | self.setWindowTitle("Enter the node's name...")
169 |
170 | self.initUI()
171 |
172 | def initUI(self):
173 | # Widgets
174 | self.name_label = QtWidgets.QLabel("Name: ")
175 | self.name_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
176 | self.name_lineEdit = QtWidgets.QLineEdit()
177 | self.name_lineEdit.setText(self.name)
178 | self.name_lineEdit.textChanged.connect(self.nameChanged)
179 |
180 | self.allNodes = self.getAllNodes()
181 | completer = QtWidgets.QCompleter(self.allNodes, self)
182 | completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
183 | self.name_lineEdit.setCompleter(completer)
184 |
185 | # Buttons
186 | self.button_box = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
187 | self.button_box.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(nuke.exists(self.name))
188 | self.button_box.accepted.connect(self.clickedOk)
189 | self.button_box.rejected.connect(self.clickedCancel)
190 |
191 | # Layout
192 | self.master_layout = QtWidgets.QVBoxLayout()
193 | self.name_layout = QtWidgets.QHBoxLayout()
194 | self.name_layout.addWidget(self.name_label)
195 | self.name_layout.addWidget(self.name_lineEdit)
196 | self.master_layout.addLayout(self.name_layout)
197 | self.master_layout.addWidget(self.button_box)
198 | self.setLayout(self.master_layout)
199 |
200 | self.name_lineEdit.setFocus()
201 | self.setMinimumWidth(250)
202 |
203 | def getAllNodes(self):
204 | self.allNodes = [n.fullName() for n in nuke.allNodes(recurseGroups=True)] #if parent is in current context??
205 | self.allNodes.extend(["root","preferences"])
206 | return self.allNodes
207 |
208 | def nameChanged(self):
209 | self.name = self.name_lineEdit.text()
210 | self.button_box.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(self.name in self.allNodes)
211 |
212 | def clickedOk(self):
213 | self.accept()
214 | return
215 |
216 | def clickedCancel(self):
217 | self.reject()
218 | return
--------------------------------------------------------------------------------
/KnobScripter/findreplace.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ FindReplaceWidget: Search and Replace widget for a QPlainTextEdit! Designed for KnobScripter
3 |
4 | adrianpueyo.com
5 |
6 | """
7 |
8 | import nuke
9 |
10 | try:
11 | if nuke.NUKE_VERSION_MAJOR < 11:
12 | from PySide import QtCore, QtGui, QtGui as QtWidgets
13 | from PySide.QtCore import Qt
14 | else:
15 | from PySide2 import QtWidgets, QtGui, QtCore
16 | from PySide2.QtCore import Qt
17 | except ImportError:
18 | from Qt import QtCore, QtGui, QtWidgets
19 |
20 |
21 | class FindReplaceWidget(QtWidgets.QWidget):
22 | """
23 | SearchReplace Widget for the knobscripter. FindReplaceWidget(parent = QPlainTextEdit)
24 | """
25 |
26 | def __init__(self, textedit, parent=None):
27 | super(FindReplaceWidget, self).__init__(parent)
28 |
29 | self.editor = textedit
30 |
31 | self.initUI()
32 |
33 | def initUI(self):
34 |
35 | # --------------
36 | # Find Row
37 | # --------------
38 |
39 | # Widgets
40 | self.find_label = QtWidgets.QLabel("Find:")
41 | # self.find_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed,QtWidgets.QSizePolicy.Fixed)
42 | self.find_label.setFixedWidth(50)
43 | self.find_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
44 | self.find_lineEdit = QtWidgets.QLineEdit()
45 | self.find_next_button = QtWidgets.QPushButton("Next")
46 | self.find_next_button.clicked.connect(self.find)
47 | self.find_prev_button = QtWidgets.QPushButton("Previous")
48 | self.find_prev_button.clicked.connect(self.findBack)
49 | self.find_lineEdit.returnPressed.connect(self.find_next_button.click)
50 |
51 | # Layout
52 | self.find_layout = QtWidgets.QHBoxLayout()
53 | self.find_layout.addWidget(self.find_label)
54 | self.find_layout.addWidget(self.find_lineEdit, stretch=1)
55 | self.find_layout.addWidget(self.find_next_button)
56 | self.find_layout.addWidget(self.find_prev_button)
57 |
58 | # --------------
59 | # Replace Row
60 | # --------------
61 |
62 | # Widgets
63 | self.replace_label = QtWidgets.QLabel("Replace:")
64 | # self.replace_label.setSizePolicy(QtWidgets.QSizePolicy.Fixed,QtWidgets.QSizePolicy.Fixed)
65 | self.replace_label.setFixedWidth(50)
66 | self.replace_label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
67 | self.replace_lineEdit = QtWidgets.QLineEdit()
68 | self.replace_button = QtWidgets.QPushButton("Replace")
69 | self.replace_button.clicked.connect(self.replace)
70 | self.replace_all_button = QtWidgets.QPushButton("Replace All")
71 | self.replace_all_button.clicked.connect(lambda: self.replace(rep_all=True))
72 | self.replace_lineEdit.returnPressed.connect(self.replace_button.click)
73 |
74 | # Layout
75 | self.replace_layout = QtWidgets.QHBoxLayout()
76 | self.replace_layout.addWidget(self.replace_label)
77 | self.replace_layout.addWidget(self.replace_lineEdit, stretch=1)
78 | self.replace_layout.addWidget(self.replace_button)
79 | self.replace_layout.addWidget(self.replace_all_button)
80 |
81 | # Info text
82 | self.info_text = QtWidgets.QLabel("")
83 | self.info_text.setVisible(False)
84 | self.info_text.mousePressEvent = lambda x: self.info_text.setVisible(False)
85 | # f = self.info_text.font()
86 | # f.setItalic(True)
87 | # self.info_text.setFont(f)
88 | # self.info_text.clicked.connect(lambda:self.info_text.setVisible(False))
89 |
90 | # Divider line
91 | line = QtWidgets.QFrame()
92 | line.setFrameShape(QtWidgets.QFrame.HLine)
93 | line.setFrameShadow(QtWidgets.QFrame.Sunken)
94 | line.setLineWidth(0)
95 | line.setMidLineWidth(1)
96 | line.setFrameShadow(QtWidgets.QFrame.Sunken)
97 |
98 | # --------------
99 | # Main Layout
100 | # --------------
101 |
102 | self.layout = QtWidgets.QVBoxLayout()
103 | self.layout.addSpacing(4)
104 | self.layout.addWidget(self.info_text)
105 | self.layout.addLayout(self.find_layout)
106 | self.layout.addLayout(self.replace_layout)
107 | self.layout.setSpacing(4)
108 | if nuke.NUKE_VERSION_MAJOR >= 11:
109 | self.layout.setMargin(2)
110 | else:
111 | self.layout.setContentsMargins(2, 2, 2, 2)
112 | self.layout.addSpacing(4)
113 | self.layout.addWidget(line)
114 | self.setLayout(self.layout)
115 | self.setTabOrder(self.find_lineEdit, self.replace_lineEdit)
116 | # self.adjustSize()
117 | # self.setMaximumHeight(180)
118 |
119 | def find(self, find_str="", match_case=True):
120 | if find_str == "":
121 | find_str = self.find_lineEdit.text()
122 |
123 | matches = self.editor.toPlainText().count(find_str)
124 | if not matches or matches == 0:
125 | self.info_text.setText(" No more matches.")
126 | self.info_text.setVisible(True)
127 | return
128 | else:
129 | self.info_text.setVisible(False)
130 |
131 | # Beginning of undo block
132 | cursor = self.editor.textCursor()
133 | cursor.beginEditBlock()
134 |
135 | # Use flags for case match
136 | flags = QtGui.QTextDocument.FindFlags()
137 | if match_case:
138 | flags = flags | QtGui.QTextDocument.FindCaseSensitively
139 |
140 | # Find next
141 | r = self.editor.find(find_str, flags)
142 |
143 | cursor.endEditBlock()
144 |
145 | self.editor.setFocus()
146 | self.editor.show()
147 | return r
148 |
149 | def findBack(self, find_str="", match_case=True):
150 | if find_str == "":
151 | find_str = self.find_lineEdit.text()
152 |
153 | matches = self.editor.toPlainText().count(find_str)
154 | if not matches or matches == 0:
155 | self.info_text.setText(" No more matches.")
156 | self.info_text.setVisible(True)
157 | return
158 | else:
159 | self.info_text.setVisible(False)
160 |
161 | # Beginning of undo block
162 | cursor = self.editor.textCursor()
163 | cursor.beginEditBlock()
164 |
165 | # Use flags for case match
166 | flags = QtGui.QTextDocument.FindFlags()
167 | flags = flags | QtGui.QTextDocument.FindBackward
168 | if match_case:
169 | flags = flags | QtGui.QTextDocument.FindCaseSensitively
170 |
171 | # Find prev
172 | r = self.editor.find(find_str, flags)
173 | cursor.endEditBlock()
174 | self.editor.setFocus()
175 | return r
176 |
177 | def replace(self, find_str="", rep_str="", rep_all=False):
178 | if find_str == "":
179 | find_str = self.find_lineEdit.text()
180 | if rep_str == "":
181 | rep_str = self.replace_lineEdit.text()
182 |
183 | matches = self.editor.toPlainText().count(find_str)
184 | if not matches or matches == 0:
185 | self.info_text.setText(" No more matches.")
186 | self.info_text.setVisible(True)
187 | return
188 | else:
189 | self.info_text.setVisible(False)
190 |
191 | # Beginning of undo block
192 | cursor = self.editor.textCursor()
193 | # cursor_orig_pos = cursor.position()
194 | cursor.beginEditBlock()
195 |
196 | # Use flags for case match
197 | flags = QtGui.QTextDocument.FindFlags()
198 | flags = flags | QtGui.QTextDocument.FindCaseSensitively
199 |
200 | if rep_all:
201 | cursor.movePosition(QtGui.QTextCursor.Start)
202 | self.editor.setTextCursor(cursor)
203 | cursor = self.editor.textCursor()
204 | rep_count = 0
205 | while True:
206 | if not cursor.hasSelection() or cursor.selectedText() != find_str:
207 | self.editor.find(find_str, flags) # Find next
208 | cursor = self.editor.textCursor()
209 | if not cursor.hasSelection():
210 | break
211 | else:
212 | cursor.insertText(rep_str)
213 | rep_count += 1
214 | self.info_text.setText(" Replaced " + str(rep_count) + " matches.")
215 | self.info_text.setVisible(True)
216 | else: # If not "find all"
217 | if not cursor.hasSelection() or cursor.selectedText() != find_str:
218 | self.editor.find(find_str, flags) # Find next
219 | if not cursor.hasSelection() and matches > 0: # If not found but there are matches, start over
220 | cursor.movePosition(QtGui.QTextCursor.Start)
221 | self.editor.setTextCursor(cursor)
222 | self.editor.find(find_str, flags)
223 | else:
224 | cursor.insertText(rep_str)
225 | self.editor.find(rep_str, flags | QtGui.QTextDocument.FindBackward)
226 |
227 | cursor.endEditBlock()
228 | self.replace_lineEdit.setFocus()
229 | return
230 |
--------------------------------------------------------------------------------
/KnobScripter/blinkhighlighter.py:
--------------------------------------------------------------------------------
1 | import nuke
2 |
3 | try:
4 | if nuke.NUKE_VERSION_MAJOR < 11:
5 | from PySide import QtCore, QtGui, QtGui as QtWidgets
6 | from PySide.QtCore import Qt
7 | else:
8 | from PySide2 import QtWidgets, QtGui, QtCore
9 | from PySide2.QtCore import Qt
10 | except ImportError:
11 | from Qt import QtCore, QtGui, QtWidgets
12 |
13 | class KSBlinkHighlighter(QtGui.QSyntaxHighlighter):
14 | '''
15 | Blink code highlighter class!
16 | Modified over Foundry's nukescripts.blinkscripteditor module.
17 | '''
18 |
19 | # TODO open curly braces { and enter should bring the } an extra line down
20 |
21 | def __init__(self, document, style="default"):
22 |
23 | self.selected_text = ""
24 | self.selected_text_prev = ""
25 |
26 | self.styles = self.loadStyles() # Holds a dict for each style
27 | self._style = style # Can be set via setStyle
28 | self._style = "default" # TODO REMOVE
29 | self.setStyle(self._style) # Set default style
30 |
31 | super(KSBlinkHighlighter, self).__init__(document)
32 |
33 | def loadStyles(self):
34 | ''' Loads the different sets of rules '''
35 | styles = dict()
36 |
37 | # LOAD ANY STYLE
38 | default_styles_list = [
39 | {
40 | "title": "default",
41 | "desc": "My adaptation from the default style from Nuke, with some improvements.",
42 | "styles": {
43 | 'keyword': ([122, 136, 53], 'bold'),
44 | 'stringDoubleQuote': ([226, 138, 138]),
45 | 'stringSingleQuote': ([110, 160, 121]),
46 | 'comment': ([188, 179, 84]),
47 | 'multiline_comment': ([188, 179, 84]),
48 | 'type': ([25, 25, 80]),
49 | 'variableKeyword': ([25, 25, 80]),
50 | 'function': ([3, 185, 191]), # only needed till here for blink?
51 | 'number': ([174, 129, 255]),
52 | 'custom': ([255, 170, 0], 'italic'),
53 | 'selected': ([255, 255, 255], 'bold underline'),
54 | 'underline': ([240, 240, 240], 'underline'),
55 | },
56 | "keywords": {},
57 | },
58 | ]
59 |
60 | for style_dict in default_styles_list:
61 | if all(k in style_dict.keys() for k in ["title", "styles"]):
62 | styles[style_dict["title"]] = self.loadStyle(style_dict)
63 |
64 | return styles
65 |
66 | def loadStyle(self, style_dict):
67 | '''
68 | Given a dictionary of styles and keywords, returns the style as a dict
69 | '''
70 |
71 | styles = style_dict["styles"]
72 |
73 | # 1. Base settings
74 | if "base" in styles:
75 | base_format = styles["base"]
76 | else:
77 | base_format = self.format([255, 255, 255])
78 |
79 | for key in styles:
80 | if type(styles[key]) == list:
81 | styles[key] = self.format(styles[key])
82 | elif styles[key][1]:
83 | styles[key] = self.format(styles[key][0], styles[key][1])
84 |
85 | mainKeywords = [
86 | "char", "class", "const", "double", "enum", "explicit",
87 | "friend", "inline", "int", "long", "namespace", "operator",
88 | "private", "protected", "public", "short", "signed",
89 | "static", "struct", "template", "typedef", "typename",
90 | "union", "unsigned", "virtual", "void", "volatile",
91 | "local", "param", "kernel",
92 | ]
93 |
94 | operatorKeywords = [
95 | '=', '==', '!=', '<', '<=', '>', '>=',
96 | '\+', '-', '\*', '/', '//', '\%', '\*\*',
97 | '\+=', '-=', '\*=', '/=', '\%=',
98 | '\^', '\|', '\&', '\~', '>>', '<<', '\+\+'
99 | ]
100 |
101 | variableKeywords = [
102 | "int", "int2", "int3", "int4",
103 | "float", "float2", "float3", "float4", "float3x3", "float4x4", "bool",
104 | ]
105 |
106 | blinkTypes = [
107 | "Image", "eRead", "eWrite", "eReadWrite", "eEdgeClamped", "eEdgeConstant", "eEdgeNull",
108 | "eAccessPoint", "eAccessRanged1D", "eAccessRanged2D", "eAccessRandom",
109 | "eComponentWise", "ePixelWise", "ImageComputationKernel",
110 | ]
111 |
112 | blinkFunctions = [
113 | "define", "defineParam", "process", "init", "setRange", "setAxis", "median", "bilinear",
114 | ]
115 |
116 | singletons = ['true', 'false']
117 |
118 | if 'multiline_comments' in styles:
119 | multiline_delimiter = (QtCore.QRegExp("/\\*"), QtCore.QRegExp("\\*/"), 1, styles['multiline_comments'])
120 | else:
121 | multiline_delimiter = (QtCore.QRegExp("/\\*"), QtCore.QRegExp("\\*/"), 1, base_format)
122 |
123 | # 2. Rules
124 | rules = []
125 |
126 | # Keywords
127 | if 'keyword' in styles:
128 | rules += [(r'\b%s\b' % i, 0, styles['keyword']) for i in mainKeywords]
129 |
130 | # Funcs
131 | if 'function' in styles:
132 | rules += [(r'\b%s\b' % i, 0, styles['function']) for i in blinkFunctions]
133 |
134 | # Types
135 | if 'type' in styles:
136 | rules += [(r'\b%s\b' % i, 0, styles['type']) for i in blinkTypes]
137 |
138 | if 'variableKeyword' in styles:
139 | rules += [(r'\b%s\b' % i, 0, styles['variableKeyword']) for i in variableKeywords]
140 |
141 | # String Literals
142 | if 'stringDoubleQuote' in styles:
143 | rules += [(r"\"([^\"\\\\]|\\\\.)*\"", 0, styles['stringDoubleQuote'])]
144 |
145 | # String single quotes
146 | if 'stringSingleQuote' in styles:
147 | rules += [(r"'([^'\\\\]|\\\\.)*'", 0, styles['stringSingleQuote'])]
148 |
149 | # Comments
150 | if 'comment' in styles:
151 | rules += [(r"//[^\n]*", 0, styles['comment'])]
152 |
153 | # Return all rules
154 | result = {
155 | "rules": [(QtCore.QRegExp(pat), index, fmt) for (pat, index, fmt) in rules],
156 | "multiline_delimiter": multiline_delimiter,
157 | }
158 | return result
159 |
160 | def format(self, rgb, style=''):
161 | '''
162 | Return a QtWidgets.QTextCharFormat with the given attributes.
163 | '''
164 |
165 | color = QtGui.QColor(*rgb)
166 | textFormat = QtGui.QTextCharFormat()
167 | textFormat.setForeground(color)
168 |
169 | if 'bold' in style:
170 | textFormat.setFontWeight(QtGui.QFont.Bold)
171 | if 'italic' in style:
172 | textFormat.setFontItalic(True)
173 | if 'underline' in style:
174 | textFormat.setUnderlineStyle(QtGui.QTextCharFormat.SingleUnderline)
175 |
176 | return textFormat
177 |
178 | def highlightBlock(self, text):
179 | '''
180 | Apply syntax highlighting to the given block of text.
181 | '''
182 |
183 | for expression, nth, format in self.styles[self._style]["rules"]:
184 | index = expression.indexIn(text, 0)
185 |
186 | while index >= 0:
187 | # We actually want the index of the nth match
188 | index = expression.pos(nth)
189 | length = len(expression.cap(nth))
190 | self.setFormat(index, length, format)
191 | index = expression.indexIn(text, index + length)
192 |
193 | self.setCurrentBlockState(0)
194 |
195 | # Multi-line strings etc. based on selected scheme
196 | in_multiline = self.match_multiline_blink(text, *self.styles[self._style]["multiline_delimiter"])
197 |
198 | def match_multiline_blink(self, text, delimiter_start, delimiter_end, in_state, style):
199 | '''
200 | Check whether highlighting requires multiple lines.
201 | '''
202 | # If inside multiline comment, start at 0
203 | if self.previousBlockState() == in_state:
204 | start = 0
205 | add = 0
206 | # Otherwise, look for the delimiter on this line
207 | else:
208 | start = delimiter_start.indexIn(text)
209 | # Move past this match
210 | add = delimiter_start.matchedLength()
211 |
212 | # As long as there's a delimiter match on this line...
213 | while start >= 0:
214 | # Look for the ending delimiter
215 | end = delimiter_end.indexIn(text, start + add)
216 | # Ending delimiter on this line?
217 | if end >= add:
218 | length = end - start + add + delimiter_end.matchedLength()
219 | self.setCurrentBlockState(0)
220 | # No; multi-line string
221 | else:
222 | self.setCurrentBlockState(in_state)
223 | length = len(text) - start + add
224 | # Apply formatting
225 | self.setFormat(start, length, style)
226 | # Look for the next match
227 | start = delimiter_start.indexIn(text, start + length)
228 |
229 | # Return True if still inside a multi-line string, False otherwise
230 | if self.currentBlockState() == in_state:
231 | return True
232 | else:
233 | return False
234 |
235 | def setStyle(self,style=""):
236 | pass
--------------------------------------------------------------------------------
/KnobScripter/content.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ Content: Module containing keywords, snippets and other code content that's useful for KnobScripter and doesn't
3 | come from a dynamic json but instead is part of the bare KnobScripter.
4 |
5 | adrianpueyo.com
6 |
7 | """
8 |
9 | blink_keywords = ["eComponentWise","ePixelWise","ImageComputationKernel","ImageRollingKernel","ImageReductionKernel",
10 | "eRead","eWrite","eReadWrite","kernel",
11 | "eAccessPoint","eAccessRanged1D","eAccessRanged2D","eAccessRandom",
12 | "setAxis($$eX$$)","setRange($$)","defineParam($$paramName, \"label\", defaultValue$$)",
13 | "kMin","kMax","kWhitePoint","kComps","kClamps","bounds",
14 | "ValueType($$image$$)","SampleType($$image$$)",
15 | "float ","float2 ","float3 ","float4 ","float3x3 ","float4x4 ","float[] ",
16 | "int ","int2 ","int3 ","int4 ","int3x3 ",
17 | "process($$int2 pos$$)","init()","param:","local:",
18 | "bilinear($$)","dot($$vec a, vec b$$)","cross","length","normalize",
19 | "sin($$)","cos($$)","tan($$)","asin($$)","acos($$)","atan($$)","atan2($$)",
20 | "exp($$)","log($$)","log2($$)","log10($$)",
21 | "floor($$)","ceil($$)","round($$)","pow($$a, b$$)","sqrt($$)","rsqrt($$)",
22 | "fabs($$)","abs($$)","fmod($$)","modf($$)","sign($$)",
23 | "min($$)","max($$)","clamp($$type a, type min($$), type max($$)","rcp($$)",
24 | "atomicAdd($$)","atomicInc($$)","median($$)",
25 | "rect($$scalar x1, scalar y1, scalar x2, scalar y2$$)","grow($$scalar x, scalar y$$)",
26 | "inside($$vec v$$)","width()","height()",
27 | ]
28 |
29 | blink_keyword_dict = {
30 | "Access Pattern": {
31 | "keywords": ["eAccessPoint", "eAccessRanged1D", "eAccessRanged2D", "eAccessRandom"],
32 | "help": '''This describes how the kernel will access pixels in the image. The options are:
33 |
34 | - eAccessPoint: Access only the current position in the iteration space.
35 | - eAccessRanged1D: Access a one-dimensional range of positions relative to the current position in the iteration space.
36 | - eAccessRanged2D: Access a two-dimensional range of positions relative to the current position in the iteration space.
37 | - eAccessRandom: Access any pixel in the iteration space.
38 |
39 | The default value is eAccessPoint.
40 | '''
41 | },
42 | "Edge Method": {
43 | "keywords": ["eEdgeClamped", "eEdgeConstant", "eEdgeNone"],
44 | "help": '''The edge method for an image defines the behaviour if a kernel function tries to access data outside the image bounds. The options are:
45 |
46 | - eEdgeClamped: The edge values will be repeated outside the image bounds.
47 | - eEdgeConstant: Zero values will be returned outside the image bounds.
48 | - eEdgeNone: Values are undefined outside the image bounds and no within-bounds checks will be done when you access the image. This is the most efficient access method to use when you do not require access outside the bounds, because of the lack of bounds checks.
49 |
50 | The default value is eEdgeNone.
51 | '''
52 | },
53 | "Kernel Granularity": {
54 | "keywords": ["eComponentWise", "ePixelWise"],
55 | "help": '''A kernel can be iterated in either a componentwise or pixelwise manner. Componentwise iteration means that the kernel will be executed once for each component at every point in the iteration space. Pixelwise means it will be called once only for every point in the iteration space. The options for the kernel granularity are:
56 |
57 | - eComponentWise: The kernel processes the image one component at a time. Only the current component's value can be accessed in any of the input images, or written to in the output image.
58 | - ePixelWise: The kernel processes the image one pixel at a time. All component values can be read from and written to.
59 |
60 | '''
61 | },
62 | "Read Spec": {
63 | "keywords": ["eRead", "eWrite", "eReadWrite"],
64 | "help": '''This describes how the data in the image can be accessed. The options are:
65 |
66 | - eRead: Read-only access to the image data. Common for the input image/s.
67 | - eWrite: Write-only access to the image data. Common for the output image.
68 | - eReadWrite: Both read and write access to the image data. Useful when you need to write and read again from the output image.
69 |
70 | '''
71 | },
72 | "Variable Types": {
73 | "keywords": ["int", "int2", "int3", "int4", "float", "float2", "float3", "float4", "float3x3",
74 | "float4x4", "bool"],
75 | "help": '''Both param and local variables can be standard C++ types such as float, int and bool.
76 | Arrays of C++ types are also supported: float[], int[], bool[].
77 | In addition, there are some standard vector types: int2, int3, int4, float2, float3 and float4. For completeness, we also provide the vector types int1 and float1.
78 | Individual components of vector types can be accessed using .x, .y, .z and .w for the first, second, third and fourth components respectively. For example, if you have a variable of a vector type called vec, the first component can be accessed using vec.x.
79 | '''
80 | },
81 | "Kernel Type": {
82 | "keywords": ["ImageComputationKernel", "ImageRollingKernel", "ImageReductionKernel"],
83 | "help": '''Please note only ImageComputationKernel is compatible with the BlinkScript node. Only use the other types if you're writing Blink for a compiled plugin.
84 | There are three types of Blink kernel:
85 |
86 | - ImageComputationKernel: used for image processing, this takes zero or more images as input and produces one or more images as output.
87 | - ImageRollingKernel: also used for image processing, where there is a data dependency between the output at different points in the output space. With an ImageComputationKernel, there are no guarantees about the order in which the output pixels will be filled in. With an ImageRollingKernel, you can choose to "roll" the kernel either horizontally or vertically over the iteration bounds, allowing you to carry data along rows or down columns respectively.
88 | - ImageReductionKernel: used to "reduce" an image down to a value or set of values that represent it, for example to calculate statistics such as the mean or variance of an image.
89 |
90 | '''
91 | },
92 | }
93 |
94 | default_snippets = {
95 | "all": [
96 | [" b","[$$]"], # In nuke panes, most times Nuke doesn't allow the [] keys with is a pain
97 | ["b","[$$]"], # In nuke panes, most times Nuke doesn't allow the [] keys with is a pain
98 | ],
99 | "blink": [
100 | ["img","Image $$src$$;"],
101 | ["kernel","kernel $$SaturationKernel$$ : ImageComputationKernel \n{\n\n}"],
102 | ],
103 | "python": [
104 | ["an","nuke.allNodes($$)"],
105 | ["cn","nuke.createNode(\"$$\")"],
106 | ["cx","xpos()+$_$.screenWidth()/2"],
107 | ["cy","ypos()+$_$.screenHeight()/2"],
108 | ["deselect","[n.setSelected(False) for n in $$nuke.selectedNodes()$$]"],
109 | ["docs","\"\"\"\nThis is an example of Google style.\n\nArgs:\n param1: This is the first param.\n param2: This is a second param.\n\nReturns:\n This is a description of what is returned.\n\nRaises:\n KeyError: Raises an exception.\n\"\"\""],
110 | ["nodename","$Node title$ = nuke.thisNode()\n$Node title$_name = $Node title$.name()"],
111 | ["ntn","nuke.toNode($$)"],
112 | ["p","print($$)"],
113 | ["sn","nuke.selectedNode()"],
114 | ["sns","nuke.selectedNodes()"],
115 | ["tk","nuke.thisKnob()"],
116 | ["tn","nuke.thisNode()"],
117 | ["try","try:\n $$\nexcept:\n pass"],
118 | ["x","xpos()"],
119 | ["xy","$node$_pos = [$node$.xpos(),$node$.ypos()]\n"],
120 | ["y","ypos()"],
121 | ]
122 | }
123 |
124 | # Initialized at runtime
125 | all_snippets = []
126 | code_gallery_dict = {}
--------------------------------------------------------------------------------
/KnobScripter/widgets.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ Widgets: KnobScripter's module for abstract or general class definitions which can be useful elsewhere.
3 |
4 | adrianpueyo.com
5 |
6 | """
7 | import logging
8 | from collections import OrderedDict
9 | import nuke
10 | import os
11 |
12 | try:
13 | if nuke.NUKE_VERSION_MAJOR < 11:
14 | from PySide import QtCore, QtGui, QtGui as QtWidgets
15 | from PySide.QtCore import Qt
16 | else:
17 | from PySide2 import QtWidgets, QtGui, QtCore
18 | from PySide2.QtCore import Qt
19 | except ImportError:
20 | from Qt import QtCore, QtGui, QtWidgets
21 |
22 | from KnobScripter import ksscripteditor, config
23 |
24 |
25 | class GripWidget(QtWidgets.QFrame):
26 | def __init__(self, parent=None, inner_widget=None, resize_x=False, resize_y=True):
27 | super(GripWidget, self).__init__(parent)
28 |
29 | layout = QtWidgets.QVBoxLayout()
30 | layout.addWidget(inner_widget)
31 | layout.setMargin(0)
32 | self.setLayout(layout)
33 |
34 | cursor = None
35 | if resize_x and resize_y:
36 | cursor = Qt.SizeAllCursor
37 | elif resize_x:
38 | cursor = Qt.SplitHCursor
39 | elif resize_y:
40 | cursor = Qt.SplitVCursor
41 |
42 | self.setCursor(QtGui.QCursor(cursor))
43 |
44 | self.parent = parent
45 | self.resize_x = resize_x
46 | self.resize_y = resize_y
47 | self.parent_min_size = (10, 10)
48 |
49 | self.setMouseTracking(True)
50 | self.pressed = False
51 | self.click_pos = None
52 | self.click_offset = None
53 |
54 | def mousePressEvent(self, e):
55 | self.click_pos = self.mapToParent(e.pos())
56 | self.pressed = True
57 | g = self.parent.geometry()
58 | self.click_offset = [g.width() - self.click_pos.x(), g.height() - self.click_pos.y()]
59 | super(GripWidget, self).mousePressEvent(e)
60 |
61 | def mouseReleaseEvent(self, e):
62 | self.pressed = False
63 | super(GripWidget, self).mouseReleaseEvent(e)
64 |
65 | def mouseMoveEvent(self, e):
66 | if self.pressed:
67 | p = self.mapToParent(e.pos())
68 | if self.resize_x:
69 | self.parent.setFixedWidth(max(self.parent_min_size[0], p.x() + self.click_offset[0]))
70 | if self.resize_y:
71 | self.parent.setFixedHeight(max(self.parent_min_size[1], p.y() + self.click_offset[1]))
72 |
73 |
74 | class HLine(QtWidgets.QFrame):
75 | def __init__(self):
76 | super(HLine, self).__init__()
77 | self.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken)
78 | self.setLineWidth(1)
79 | self.setMidLineWidth(0)
80 | self.setLayout(None)
81 |
82 |
83 | class ClickableWidget(QtWidgets.QFrame):
84 | clicked = QtCore.Signal()
85 |
86 | def __init__(self, parent=None):
87 | super(ClickableWidget, self).__init__(parent)
88 | self.setMouseTracking(True)
89 | self.highlighted = False
90 |
91 | def setHighlighted(self, highlighted=False):
92 | self.highlighted = highlighted
93 |
94 | def enterEvent(self, event):
95 | """ Mouse hovering """
96 | self.setHighlighted(True)
97 | return QtWidgets.QFrame.enterEvent(self, event)
98 |
99 | def leaveEvent(self, event):
100 | """ Stopped hovering """
101 | self.setHighlighted(False)
102 | return QtWidgets.QFrame.leaveEvent(self, event)
103 |
104 | def mouseReleaseEvent(self, event):
105 | """ Emit clicked """
106 | super(ClickableWidget, self).mouseReleaseEvent(event)
107 | if event.button() == Qt.LeftButton:
108 | if self.highlighted:
109 | self.clicked.emit()
110 | pass
111 |
112 |
113 | class Arrow(QtWidgets.QFrame):
114 | def __init__(self, expanded=False, parent=None):
115 | super(Arrow, self).__init__(parent)
116 | self.padding = (4, 2)
117 | self.setFixedSize(12 + self.padding[0], 12 + self.padding[1])
118 |
119 | self.expanded = expanded
120 |
121 | px, py = self.padding
122 | self._arrow_down = [QtCore.QPointF(0 + px, 2.0 + py), QtCore.QPointF(10.0 + px, 2.0 + py),
123 | QtCore.QPointF(5.0 + px, 7.0 + py)]
124 | self._arrow_right = [QtCore.QPointF(2.0 + px, 0.0 + py), QtCore.QPointF(7.0 + px, 5.0 + py),
125 | QtCore.QPointF(2.0 + px, 10.0 + py)]
126 | self._arrowPoly = None
127 | self.setExpanded(expanded)
128 |
129 | def setExpanded(self, expanded=True):
130 | if expanded:
131 | self._arrowPoly = self._arrow_down
132 | else:
133 | self._arrowPoly = self._arrow_right
134 | self.expanded = expanded
135 |
136 | def paintEvent(self, event):
137 | painter = QtGui.QPainter()
138 | painter.begin(self)
139 | painter.setBrush(QtGui.QColor(192, 192, 192))
140 | painter.setPen(QtGui.QColor(64, 64, 64))
141 | painter.drawPolygon(self._arrowPoly)
142 | painter.end()
143 | return QtWidgets.QFrame.paintEvent(self, event)
144 |
145 |
146 | class ToggableGroup(QtWidgets.QFrame):
147 | """ Abstract QFrame with an arrow, a title area and a toggable content layout. """
148 |
149 | def __init__(self, parent=None, title="", collapsed=False):
150 | super(ToggableGroup, self).__init__(parent)
151 |
152 | self.collapsed = collapsed
153 |
154 | # Widgets and layouts
155 | self.arrow = Arrow(parent=self)
156 |
157 | # Layout
158 | # 1. Top Layout
159 | # Left (clickable) part, for the title
160 | self.top_clickable_widget = ClickableWidget()
161 | self.top_clickable_layout = QtWidgets.QHBoxLayout()
162 | self.top_clickable_layout.setSpacing(6)
163 | self.top_clickable_widget.setLayout(self.top_clickable_layout)
164 | # self.top_clickable_widget.setStyleSheet(".ClickableWidget{margin-top: 3px;background:transparent}")
165 | # self.top_clickable_widget.setStyleSheet("background:#000;float:left;")
166 | self.top_clickable_widget.clicked.connect(self.toggleCollapsed)
167 |
168 | # Right (non-clickable) part, for buttons or extras
169 | self.top_right_layout = QtWidgets.QHBoxLayout()
170 |
171 | self.top_clickable_layout.addWidget(self.arrow)
172 | self.title_label = QtWidgets.QLabel()
173 | self.title_label.setStyleSheet("line-height:50%;")
174 | self.title_label.setTextInteractionFlags(Qt.NoTextInteraction)
175 | self.title_label.setWordWrap(True)
176 |
177 | self.top_clickable_widget.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
178 | self.setTitle(title)
179 | self.top_clickable_layout.addWidget(self.title_label)
180 | self.top_clickable_layout.addSpacing(1)
181 | self.top_clickable_layout.setAlignment(Qt.AlignVCenter)
182 |
183 | # Together
184 | self.top_layout = QtWidgets.QHBoxLayout()
185 | self.top_layout.addWidget(self.top_clickable_widget)
186 | self.top_layout.addLayout(self.top_right_layout)
187 |
188 | # 2. Main content area
189 | self.content_widget = QtWidgets.QFrame()
190 | self.content_widget.setObjectName("content-widget")
191 | self.content_widget.setStyleSheet("#content-widget{margin:6px 0px 5px 24px;}")
192 | self.content_layout = QtWidgets.QVBoxLayout()
193 | self.content_widget.setLayout(self.content_layout)
194 | # self.content_widget.setSizePolicy(QtWidgets.QSizePolicy.Expanding,QtWidgets.QSizePolicy.Expanding)
195 |
196 | # 3. Vertical layout of 1 and 2
197 | master_layout = QtWidgets.QVBoxLayout()
198 | master_layout.addLayout(self.top_layout)
199 | master_layout.addWidget(self.content_widget)
200 |
201 | self.setLayout(master_layout)
202 | self.setCollapsed(self.collapsed)
203 |
204 | master_layout.setMargin(0)
205 | self.content_layout.setMargin(0)
206 | self.content_layout.setSizeConstraint(self.content_layout.SetNoConstraint)
207 | self.setMinimumHeight(10)
208 | self.top_clickable_layout.setMargin(0)
209 |
210 | def setTitle(self, text=""):
211 | self.title_label.setText(text)
212 |
213 | def toggleCollapsed(self):
214 | self.collapsed = not self.collapsed
215 | self.setCollapsed(self.collapsed)
216 |
217 | def setCollapsed(self, collapsed=True):
218 | self.collapsed = collapsed
219 | self.arrow.setExpanded(not collapsed)
220 | self.content_widget.setVisible(not collapsed)
221 | logging.debug("Collapsed:" + str(collapsed))
222 |
223 |
224 | class ToggableCodeGroup(ToggableGroup):
225 | """ ToggableGroup adapted for having a code editor """
226 |
227 | def __init__(self, parent=None):
228 | self.prev_height = None
229 | super(ToggableCodeGroup, self).__init__(parent=parent)
230 | self.parent = parent
231 |
232 | # Add content
233 | self.script_editor = ksscripteditor.KSScriptEditor()
234 | self.script_editor.setMinimumHeight(20)
235 |
236 | self.content_layout.addWidget(self.script_editor)
237 | self.content_layout.setSpacing(1)
238 |
239 | self.grip_line = GripWidget(self, inner_widget=HLine())
240 | self.grip_line.setStyleSheet("GripWidget:hover{border: 1px solid #DDD;}")
241 | self.grip_line.parent_min_size = (100, 100)
242 | self.content_layout.addWidget(self.grip_line)
243 |
244 | def setCollapsed(self, collapsed=True):
245 | if collapsed:
246 | self.prev_height = self.height()
247 | self.setMinimumHeight(0)
248 | else:
249 | if self.prev_height:
250 | self.setFixedHeight(self.prev_height)
251 |
252 | super(ToggableCodeGroup, self).setCollapsed(collapsed)
253 |
254 |
255 | class RadioSelector(QtWidgets.QWidget):
256 | radio_selected = QtCore.Signal(object)
257 |
258 | def __init__(self, item_list=None, orientation=0, parent=None):
259 | """
260 | item_list: list of strings
261 | orientation = 0 (h) or 1 (v)
262 | """
263 | super(RadioSelector, self).__init__(parent)
264 | self.item_list = item_list
265 | self.button_list = OrderedDict()
266 | for item in item_list:
267 | self.button_list[item] = QtWidgets.QRadioButton(item)
268 |
269 | if orientation == 0:
270 | self.layout = QtWidgets.QHBoxLayout()
271 | else:
272 | self.layout = QtWidgets.QVBoxLayout()
273 |
274 | self.button_group = QtWidgets.QButtonGroup(self)
275 | for i, btn in enumerate(self.button_list):
276 | self.button_group.addButton(self.button_list[btn], i)
277 | self.layout.addWidget(self.button_list[btn])
278 | self.button_group.buttonClicked.connect(self.button_clicked)
279 |
280 | self.layout.addStretch(1)
281 |
282 | self.setLayout(self.layout)
283 | self.layout.setMargin(0)
284 |
285 | def button_clicked(self, button):
286 | self.radio_selected.emit(str(button.text()))
287 |
288 | def set_button(self, text, emit=False):
289 | text = text.lower()
290 | item_list_lower = [i.lower() for i in self.item_list]
291 | if text in item_list_lower:
292 | btn = self.button_group.button(item_list_lower.index(text))
293 | btn.setChecked(True)
294 | if emit:
295 | self.radio_selected.emit(btn.text())
296 | else:
297 | logging.debug("Couldn't set radio button text.")
298 |
299 | def selected_text(self):
300 | return str(self.button_group.button(self.button_group.checkedId()).text())
301 |
302 |
303 | class APToolButton(QtWidgets.QToolButton):
304 | """ Given the png name and sizes, makes a tool button """
305 |
306 | def __init__(self, icon=None, icon_size=None, btn_size=None, parent=None):
307 | super(APToolButton, self).__init__(parent=parent)
308 |
309 | self.icon_path = None
310 | self.set_icon(icon)
311 | icon_size = icon_size or config.prefs["qt_icon_size"]
312 | btn_size = btn_size or config.prefs["qt_btn_size"]
313 | self.setIconSize(QtCore.QSize(icon_size, icon_size))
314 | self.setFixedSize(QtCore.QSize(btn_size, btn_size))
315 |
316 | def set_icon(self, icon_filename=None, add_extension=True, full_path=False):
317 | """ Set the button's icon (i.e. icon_search.png) """
318 | if icon_filename is None:
319 | self.setIcon(None)
320 | return
321 | elif add_extension and not icon_filename.endswith(".png"):
322 | icon_filename = icon_filename + ".png"
323 |
324 | if full_path:
325 | self.icon_path = icon_filename
326 | else:
327 | self.icon_path = os.path.join(config.ICONS_DIR, icon_filename)
328 |
329 | self.setIcon(QtGui.QIcon(self.icon_path))
330 |
--------------------------------------------------------------------------------
/KnobScripter/pythonhighlighter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ Python Higlighter: KnobScripter's QSyntaxHighlighter adapted for python code.
3 |
4 | Adapted from an original version by Wouter Gilsing. His comments:
5 | Modified, simplified version of some code found I found when researching:
6 | wiki.python.org/moin/PyQt/Python%20syntax%20highlighting
7 | They did an awesome job, so credits to them. I only needed to make some modifications to make it fit my needs.
8 |
9 | adrianpueyo.com
10 |
11 | """
12 |
13 | import nuke
14 |
15 | try:
16 | if nuke.NUKE_VERSION_MAJOR < 11:
17 | from PySide import QtCore, QtGui, QtGui as QtWidgets
18 | from PySide.QtCore import Qt
19 | else:
20 | from PySide2 import QtWidgets, QtGui, QtCore
21 | from PySide2.QtCore import Qt
22 | except ImportError:
23 | from Qt import QtCore, QtGui, QtWidgets
24 |
25 |
26 | class KSPythonHighlighter(QtGui.QSyntaxHighlighter):
27 | """
28 | Adapted from an original version by Wouter Gilsing. His comments:
29 | Modified, simplified version of some code found I found when researching:
30 | wiki.python.org/moin/PyQt/Python%20syntax%20highlighting
31 | They did an awesome job, so credits to them. I only needed to make some
32 | modifications to make it fit my needs for KS.
33 | """
34 |
35 | def __init__(self, document, style="monokai"):
36 |
37 | self.selected_text = ""
38 | self.selected_text_prev = ""
39 |
40 | self.blocked = False
41 |
42 | self.styles = self.loadStyles() # Holds a dict for each style
43 | self._style = style # Can be set via setStyle
44 | self.setStyle(self._style) # Set default style
45 | # self.updateStyle() # Load ks color scheme
46 |
47 | super(KSPythonHighlighter, self).__init__(document)
48 |
49 | def loadStyles(self):
50 | """ Loads the different sets of rules """
51 | styles = dict()
52 |
53 | # LOAD ANY STYLE
54 | default_styles_list = [
55 | {
56 | "title": "nuke",
57 | "styles": {
58 | 'base': self.format([255, 255, 255]),
59 | 'keyword': self.format([238, 117, 181], 'bold'),
60 | 'operator': self.format([238, 117, 181], 'bold'),
61 | 'number': self.format([174, 129, 255]),
62 | 'singleton': self.format([174, 129, 255]),
63 | 'string': self.format([242, 136, 135]),
64 | 'comment': self.format([143, 221, 144]),
65 | },
66 | "keywords": {},
67 | },
68 | {
69 | "title": "monokai",
70 | "styles": {
71 | 'base': self.format([255, 255, 255]),
72 | 'keyword': self.format([237, 36, 110]),
73 | 'operator': self.format([237, 36, 110]),
74 | 'string': self.format([237, 229, 122]),
75 | 'comment': self.format([125, 125, 125]),
76 | 'number': self.format([165, 120, 255]),
77 | 'singleton': self.format([165, 120, 255]),
78 | 'function': self.format([184, 237, 54]),
79 | 'argument': self.format([255, 170, 10], 'italic'),
80 | 'class': self.format([184, 237, 54]),
81 | 'callable': self.format([130, 226, 255]),
82 | 'error': self.format([130, 226, 255], 'italic'),
83 | 'underline': self.format([240, 240, 240], 'underline'),
84 | 'selected': self.format([255, 255, 255], 'bold underline'),
85 | 'custom': self.format([200, 200, 200], 'italic'),
86 | 'blue': self.format([130, 226, 255], 'italic'),
87 | 'self': self.format([255, 170, 10], 'italic'),
88 | },
89 | "keywords": {
90 | 'custom': ['nuke'],
91 | 'blue': ['def', 'class', 'int', 'str', 'float',
92 | 'bool', 'list', 'dict', 'set', ],
93 | 'base': [],
94 | 'self': ['self'],
95 | },
96 | }
97 | ]
98 | # TODO separate the format before the loadstyle thing. should be done here before looping.
99 | for style_dict in default_styles_list:
100 | if all(k in style_dict.keys() for k in ["title", "styles"]):
101 | styles[style_dict["title"]] = self.loadStyle(style_dict)
102 |
103 | return styles
104 |
105 | def loadStyle(self, style_dict):
106 | """
107 | Given a dictionary of styles and keywords, returns the style as a dict
108 | """
109 |
110 | styles = style_dict["styles"].copy()
111 |
112 | # 1. Base settings
113 | if "base" in styles:
114 | base_format = styles["base"]
115 | else:
116 | base_format = self.format([255, 255, 255])
117 |
118 | main_keywords = [
119 | 'and', 'assert', 'break', 'continue',
120 | 'del', 'elif', 'else', 'except', 'exec', 'finally',
121 | 'for', 'from', 'global', 'if', 'import', 'in',
122 | 'is', 'lambda', 'not', 'or', 'pass', 'print',
123 | 'raise', 'return', 'try', 'while', 'yield', 'with', 'as'
124 | ]
125 |
126 | error_keywords = ['AssertionError', 'AttributeError', 'EOFError', 'FloatingPointError',
127 | 'FloatingPointError', 'GeneratorExit', 'ImportError', 'IndexError',
128 | 'KeyError', 'KeyboardInterrupt', 'MemoryError', 'NameError',
129 | 'NotImplementedError', 'OSError', 'OverflowError', 'ReferenceError',
130 | 'RuntimeError', 'StopIteration', 'SyntaxError', 'IndentationError',
131 | 'TabError', 'SystemError', 'SystemExit', 'TypeError', 'UnboundLocalError',
132 | 'UnicodeError', 'UnicodeEncodeError', 'UnicodeDecodeError', 'UnicodeTranslateError',
133 | 'ValueError', 'ZeroDivisionError',
134 | ]
135 |
136 | base_keywords = [',']
137 |
138 | operator_keywords = [
139 | '=', '==', '!=', '<', '<=', '>', '>=',
140 | '\+', '-', '\*', '/', '//', '\%', '\*\*',
141 | '\+=', '-=', '\*=', '/=', '\%=',
142 | '\^', '\|', '\&', '\~', '>>', '<<'
143 | ]
144 |
145 | singletons = ['True', 'False', 'None']
146 |
147 | if 'comment' in styles:
148 | tri_single = (QtCore.QRegExp("'''"), 1, styles['comment'])
149 | tri_double = (QtCore.QRegExp('"""'), 2, styles['comment'])
150 | else:
151 | tri_single = (QtCore.QRegExp("'''"), 1, base_format)
152 | tri_double = (QtCore.QRegExp('"""'), 2, base_format)
153 |
154 | # 2. Rules
155 | rules = []
156 |
157 | if "argument" in styles:
158 | # Everything inside parentheses
159 | rules += [(r"def [\w]+[\s]*\((.*)\)", 1, styles['argument'])]
160 | # Now restore unwanted stuff...
161 | rules += [(i, 0, base_format) for i in base_keywords]
162 | rules += [(r"[^\(\w),.][\s]*[\w]+", 0, base_format)]
163 |
164 | if "callable" in styles:
165 | rules += [(r"\b([\w]+)[\s]*[(]", 1, styles['callable'])]
166 |
167 | if "keyword" in styles:
168 | rules += [(r'\b%s\b' % i, 0, styles['keyword']) for i in main_keywords]
169 |
170 | if "error" in styles:
171 | rules += [(r'\b%s\b' % i, 0, styles['error']) for i in error_keywords]
172 |
173 | if "operator" in styles:
174 | rules += [(i, 0, styles['operator']) for i in operator_keywords]
175 |
176 | if "singleton" in styles:
177 | rules += [(r'\b%s\b' % i, 0, styles['singleton']) for i in singletons]
178 |
179 | if "number" in styles:
180 | rules += [(r'\b[0-9]+\b', 0, styles['number'])]
181 |
182 | # Function definitions
183 | if "function" in styles:
184 | rules += [(r"def[\s]+([\w\.]+)", 1, styles['function'])]
185 |
186 | # Class definitions
187 | if "class" in styles:
188 | rules += [(r"class[\s]+([\w\.]+)", 1, styles['class'])]
189 | # Class argument (which is also a class so must be same color)
190 | rules += [(r"class[\s]+[\w\.]+[\s]*\((.*)\)", 1, styles['class'])]
191 |
192 | # Function arguments
193 | if "argument" in styles:
194 | rules += [(r"def[\s]+[\w]+[\s]*\(([\w]+)", 1, styles['argument'])]
195 |
196 | # Custom keywords
197 | if "keywords" in style_dict.keys():
198 | keywords = style_dict["keywords"]
199 | for k in keywords.keys():
200 | if k in styles:
201 | rules += [(r'\b%s\b' % i, 0, styles[k]) for i in keywords[k]]
202 |
203 | if "string" in styles:
204 | # Double-quoted string, possibly containing escape sequences
205 | rules += [(r'"[^"\\]*(\\.[^"\\]*)*"', 0, styles['string'])]
206 | # Single-quoted string, possibly containing escape sequences
207 | rules += [(r"'[^'\\]*(\\.[^'\\]*)*'", 0, styles['string'])]
208 |
209 | # Comments from '#' until a newline
210 | if "comment" in styles:
211 | rules += [(r'#[^\n]*', 0, styles['comment'])]
212 |
213 | # 3. Resulting dictionary
214 | result = {
215 | "rules": [(QtCore.QRegExp(pat), index, fmt) for (pat, index, fmt) in rules],
216 | # Build a QRegExp for each pattern
217 | "tri_single": tri_single,
218 | "tri_double": tri_double,
219 | }
220 |
221 | return result
222 |
223 | @staticmethod
224 | def format(rgb, style=''):
225 | """
226 | Return a QtWidgets.QTextCharFormat with the given attributes.
227 | """
228 |
229 | color = QtGui.QColor(*rgb)
230 | text_format = QtGui.QTextCharFormat()
231 | text_format.setForeground(color)
232 |
233 | if 'bold' in style:
234 | text_format.setFontWeight(QtGui.QFont.Bold)
235 | if 'italic' in style:
236 | text_format.setFontItalic(True)
237 | if 'underline' in style:
238 | text_format.setUnderlineStyle(QtGui.QTextCharFormat.SingleUnderline)
239 |
240 | return text_format
241 |
242 | def highlightBlock(self, text):
243 | """
244 | Apply syntax highlighting to the given block of text.
245 | """
246 |
247 | for expression, nth, text_format in self.styles[self._style]["rules"]:
248 | index = expression.indexIn(text, 0)
249 |
250 | while index >= 0:
251 | # We actually want the index of the nth match
252 | index = expression.pos(nth)
253 | length = len(expression.cap(nth))
254 | try:
255 | self.setFormat(index, length, text_format)
256 | except:
257 | return False
258 | index = expression.indexIn(text, index + length)
259 |
260 | self.setCurrentBlockState(0)
261 |
262 | # Multi-line strings etc. based on selected scheme
263 | in_multiline = self.match_multiline(text, *self.styles[self._style]["tri_single"])
264 | if not in_multiline:
265 | in_multiline = self.match_multiline(text, *self.styles[self._style]["tri_double"])
266 |
267 | # TODO if there's a selection, highlight same occurrences in the full document.
268 | # If no selection but something highlighted, unhighlight full document. (do it thru regex or sth)
269 |
270 | def setStyle(self, style_name="nuke"):
271 | if style_name in self.styles.keys():
272 | self._style = style_name
273 | else:
274 | raise Exception("Style {} not found.".format(str(style_name)))
275 |
276 | def match_multiline(self, text, delimiter, in_state, style):
277 | """
278 | Check whether highlighting requires multiple lines.
279 | """
280 | # If inside triple-single quotes, start at 0
281 | if self.previousBlockState() == in_state:
282 | start = 0
283 | add = 0
284 | # Otherwise, look for the delimiter on this line
285 | else:
286 | start = delimiter.indexIn(text)
287 | # Move past this match
288 | add = delimiter.matchedLength()
289 |
290 | # As long as there's a delimiter match on this line...
291 | while start >= 0:
292 | # Look for the ending delimiter
293 | end = delimiter.indexIn(text, start + add)
294 | # Ending delimiter on this line?
295 | if end >= add:
296 | length = end - start + add + delimiter.matchedLength()
297 | self.setCurrentBlockState(0)
298 | # No; multi-line string
299 | else:
300 | self.setCurrentBlockState(in_state)
301 | length = len(text) - start + add
302 | # Apply formatting
303 | self.setFormat(start, length, style)
304 | # Look for the next match
305 | start = delimiter.indexIn(text, start + length)
306 |
307 | # Return True if still inside a multi-line string, False otherwise
308 | if self.currentBlockState() == in_state:
309 | return True
310 | else:
311 | return False
312 |
313 | @property
314 | def style(self):
315 | return self._style
316 |
317 | @style.setter
318 | def style(self, style_name="nuke"):
319 | self.setStyle(style_name)
320 |
--------------------------------------------------------------------------------
/KnobScripter/snippets.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ This module provides all the functionality relative to KnobScripter's Snippets.
3 |
4 | Main classes:
5 | * AppendSnippetPanel: Convenient widget to append a snippet to the current dict.
6 | * SnippetsWidget: Snippet Edit panel, where you can create/delete/edit/save snippets.
7 | * SnippetsItem: ToggableGroup adapted to editing a specific Snippet.
8 |
9 | Main functions:
10 | * load_snippets_dict: Loads all available snippets as a dictionary.
11 | * load_all_snippets: Loads snippets recursively. Deprecated.
12 | * save_snippets_dict: Saves a given dictionary as snippets.
13 | * append_snippet: Appends a given snippet to the dictionary and saves.
14 |
15 | adrianpueyo.com
16 |
17 | """
18 |
19 | import nuke
20 | import json
21 | import os
22 | import re
23 | import logging
24 | from functools import partial
25 |
26 | try:
27 | if nuke.NUKE_VERSION_MAJOR < 11:
28 | from PySide import QtCore, QtGui, QtGui as QtWidgets
29 | from PySide.QtCore import Qt
30 | else:
31 | from PySide2 import QtWidgets, QtGui, QtCore
32 | from PySide2.QtCore import Qt
33 | except ImportError:
34 | from Qt import QtCore, QtGui, QtWidgets
35 |
36 | from KnobScripter import ksscripteditor, config, dialogs, utils, widgets, content
37 |
38 |
39 | def load_snippets_dict(path=None):
40 | """
41 | Load the snippets from json path as a dict. Return dict()
42 | if default_snippets == True and no snippets file found, loads default library of snippets.
43 | """
44 | if not path:
45 | path = config.snippets_txt_path
46 | if not os.path.isfile(path):
47 | logging.debug("Path doesn't exist: " + path)
48 | return content.default_snippets
49 | else:
50 | try:
51 | with open(path, "r") as f:
52 | snippets = json.load(f)
53 | return snippets
54 | except:
55 | logging.debug("Couldn't open file: {}.\nLoading default snippets instead.".format(path))
56 | return content.default_snippets
57 |
58 |
59 | def save_snippets_dict(snippets_dict, path=None):
60 | """ Perform a json dump of the snippets into the path """
61 | if not path:
62 | path = config.snippets_txt_path
63 | with open(path, "w") as f:
64 | json.dump(snippets_dict, f, sort_keys=True, indent=4)
65 | content.all_snippets = snippets_dict
66 |
67 |
68 | def append_snippet(code, shortcode="", path=None, lang=None):
69 | """ Load the snippets file as a dict and append a snippet """
70 | if code == "":
71 | return False
72 | if not path:
73 | path = config.snippets_txt_path
74 | if not lang:
75 | lang = "python"
76 | lang = lang.lower()
77 | all_snippets = load_snippets_dict(path)
78 | if shortcode == "":
79 | return False
80 | if lang not in all_snippets:
81 | all_snippets[lang] = []
82 | all_snippets[lang].append([shortcode, code])
83 | save_snippets_dict(all_snippets, path)
84 |
85 |
86 | class AppendSnippetPanel(QtWidgets.QDialog):
87 | def __init__(self, parent=None, code=None, shortcode=None, path=None, lang="python"):
88 | super(AppendSnippetPanel, self).__init__(parent)
89 |
90 | self.lang = lang
91 | shortcode = shortcode or ""
92 | self.path = path or config.snippets_txt_path
93 | self.existing_snippets = load_snippets_dict(self.path)
94 | if not self.existing_snippets:
95 | return
96 | self.existing_shortcodes = self.existing_snippets.keys()
97 |
98 | # Layout
99 | self.layout = QtWidgets.QVBoxLayout()
100 |
101 | # Code language
102 | self.lang_selector = widgets.RadioSelector(["Python", "Blink", "All"])
103 |
104 | self.lang_selector.radio_selected.connect(self.change_lang)
105 |
106 | # Shortcode
107 | self.shortcode_lineedit = QtWidgets.QLineEdit(shortcode)
108 | f = self.shortcode_lineedit.font()
109 | f.setWeight(QtGui.QFont.Bold)
110 | self.shortcode_lineedit.setFont(f)
111 |
112 | # Code
113 | self.script_editor = ksscripteditor.KSScriptEditor()
114 |
115 | # self.script_editor.set_code_language(lang)
116 | self.script_editor.setPlainText(code)
117 | se_policy = self.script_editor.sizePolicy()
118 | se_policy.setVerticalStretch(1)
119 | self.script_editor.setSizePolicy(se_policy)
120 |
121 | # Warnings
122 | self.warnings_label = QtWidgets.QLabel("Please set a code and a shortcode.")
123 | self.warnings_label.setStyleSheet("color: #D65; font-style: italic;")
124 | self.warnings_label.setWordWrap(True)
125 | self.warnings_label.mouseReleaseEvent = lambda x: self.warnings_label.hide()
126 |
127 | # Buttons
128 | self.button_box = QtWidgets.QDialogButtonBox(
129 | QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel)
130 | self.button_box.accepted.connect(self.save_pressed)
131 | self.button_box.rejected.connect(self.cancel_pressed)
132 |
133 | # Form layout
134 | self.form = QtWidgets.QFormLayout()
135 | self.form.addRow("Language: ", self.lang_selector)
136 | self.form.addRow("Shortcode: ", self.shortcode_lineedit)
137 | self.form.addRow("Code: ", self.script_editor)
138 | self.form.addRow("", self.warnings_label)
139 | self.warnings_label.hide()
140 | self.form.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
141 |
142 | self.layout.addLayout(self.form)
143 | self.layout.addWidget(self.button_box)
144 | self.setLayout(self.layout)
145 |
146 | # Init values
147 | self.setWindowTitle("Add Snippet")
148 | self.lang_selector.set_button(self.lang)
149 | self.script_editor.set_code_language(self.lang)
150 | self.shortcode_lineedit.setFocus()
151 | self.shortcode_lineedit.selectAll()
152 |
153 | def change_lang(self, lang):
154 | self.script_editor.set_code_language(str(lang.lower()))
155 |
156 | def save_pressed(self):
157 | shortcode = self.shortcode_lineedit.text()
158 | code = self.script_editor.toPlainText()
159 | lang = self.lang_selector.selected_text()
160 | if code == "" or shortcode == "":
161 | self.warnings_label.show()
162 | return False
163 | if shortcode in self.existing_shortcodes:
164 | msg = "A snippet with the given code already exists. Do you wish to overwrite it?"
165 | if not dialogs.ask(msg, self, default_yes=False):
166 | return False
167 | logging.debug(
168 | "Snippet to be saved \nLang:\n{0}\nShortcode:\n{1}\nCode:\n{2}\n------".format(lang, shortcode, code))
169 | append_snippet(code, shortcode, lang=lang)
170 | all_snippets = load_snippets_dict()
171 | try:
172 | content.all_snippets = all_snippets
173 | except Exception as e:
174 | logging.debug(e)
175 | self.accept()
176 |
177 | def cancel_pressed(self):
178 | if self.script_editor.toPlainText() != "":
179 | msg = "Do you wish to discard the changes?"
180 | if not dialogs.ask(msg, self, default_yes=False):
181 | return False
182 | self.reject()
183 |
184 |
185 | class SnippetsWidget(QtWidgets.QWidget):
186 | """ Widget containing snippet editors, lang selector and other functionality. """
187 |
188 | def __init__(self, knob_scripter="", _parent=QtWidgets.QApplication.activeWindow()):
189 | super(SnippetsWidget, self).__init__(_parent)
190 | self.knob_scripter = knob_scripter
191 | self.code_language = "python"
192 | self.snippets_built = False
193 |
194 | self.initUI()
195 | self.build_snippets(lang=self.code_language)
196 |
197 | def initUI(self):
198 | self.layout = QtWidgets.QVBoxLayout()
199 |
200 | # 1. Filters (language etc)
201 | self.filter_widget = QtWidgets.QFrame()
202 | filter_layout = QtWidgets.QHBoxLayout()
203 | code_language_label = QtWidgets.QLabel("Language:")
204 | filter_layout.addWidget(code_language_label)
205 | # TODO Compatible with expressions and TCL knobs too
206 | self.lang_selector = widgets.RadioSelector(["Python", "Blink", "All"])
207 | self.lang_selector.radio_selected.connect(self.change_lang)
208 | filter_layout.addWidget(self.lang_selector)
209 | filter_layout.addStretch()
210 | self.reload_button = QtWidgets.QPushButton("Reload")
211 | self.reload_button.clicked.connect(self.reload)
212 | filter_layout.setMargin(0)
213 | filter_layout.addWidget(self.reload_button)
214 |
215 | self.filter_widget.setLayout(filter_layout)
216 | self.layout.addWidget(self.filter_widget)
217 | self.layout.addWidget(widgets.HLine())
218 |
219 | # 2. Scroll Area
220 | # 2.1. Inner scroll content
221 | self.scroll_content = QtWidgets.QWidget()
222 | self.scroll_layout = QtWidgets.QVBoxLayout()
223 | self.scroll_layout.setMargin(0)
224 | self.scroll_layout.addStretch()
225 | self.scroll_content.setLayout(self.scroll_layout)
226 | self.scroll_content.setContentsMargins(0, 0, 8, 0)
227 |
228 | # 2.2. External Scroll Area
229 | self.scroll = QtWidgets.QScrollArea()
230 | self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
231 | self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
232 | self.scroll.setWidgetResizable(True)
233 | self.scroll.setWidget(self.scroll_content)
234 | self.scroll.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
235 |
236 | self.layout.addWidget(self.scroll)
237 |
238 | # 3. Lower buttons
239 | self.lower_layout = QtWidgets.QHBoxLayout()
240 |
241 | self.add_snippet_btn = widgets.APToolButton("add_filled")
242 | self.add_snippet_btn.setToolTip("Add new snippet")
243 | self.add_snippet_btn.clicked.connect(self.add_snippet)
244 |
245 | self.sort_az_btn = widgets.APToolButton("sort_az", icon_size=22)
246 | self.sort_az_btn.setToolTip("Sort snippets A-Z")
247 | self.sort_az_btn.clicked.connect(self.sort_snippets)
248 | self.sort_za_btn = widgets.APToolButton("sort_za", icon_size=22)
249 | self.sort_za_btn.setToolTip("Sort snippets Z-A")
250 | self.sort_za_btn.clicked.connect(lambda: self.sort_snippets(reverse=True))
251 | self.v_expand_btn = widgets.APToolButton("v_expand", icon_size=22)
252 | self.v_expand_btn.setToolTip("Expand all snippets")
253 | self.v_expand_btn.clicked.connect(self.expand_snippets)
254 | self.v_collapse_btn = widgets.APToolButton("v_collapse", icon_size=22)
255 | self.v_collapse_btn.setToolTip("Collapse all snippets")
256 | self.v_collapse_btn.clicked.connect(self.collapse_snippets)
257 | self.save_snippets_btn = widgets.APToolButton("save_all")
258 | self.save_snippets_btn.setToolTip("Save all snippets")
259 | self.save_snippets_btn.clicked.connect(self.save_all_snippets)
260 | self.snippets_help_btn = widgets.APToolButton("help_filled")
261 | self.snippets_help_btn.setToolTip("Help")
262 | self.snippets_help_btn.clicked.connect(self.snippets_help)
263 |
264 | self.lower_layout.addWidget(self.add_snippet_btn)
265 | self.lower_layout.addSpacing(12)
266 | self.lower_layout.addWidget(self.sort_az_btn)
267 | self.lower_layout.addWidget(self.sort_za_btn)
268 | self.lower_layout.addSpacing(12)
269 | self.lower_layout.addWidget(self.v_expand_btn)
270 | self.lower_layout.addWidget(self.v_collapse_btn)
271 | self.lower_layout.addStretch()
272 | self.lower_layout.addWidget(self.save_snippets_btn)
273 | self.lower_layout.addWidget(self.snippets_help_btn)
274 |
275 | self.layout.addWidget(widgets.HLine())
276 | self.layout.addLayout(self.lower_layout)
277 |
278 | self.setLayout(self.layout)
279 |
280 | def reload(self):
281 | """ Force a rebuild of the widgets in the current filter status. """
282 | self.build_snippets()
283 |
284 | def build_snippets(self, lang=None):
285 | lang = lang or self.code_language
286 | lang = lang.lower()
287 | self.code_language = lang
288 |
289 | # Clear scroll area
290 | utils.clear_layout(self.scroll_layout)
291 | snippets_dict = load_snippets_dict()
292 | # Build widgets as needed
293 | for language in snippets_dict:
294 | # print("language: "+language)
295 | for snippet in snippets_dict[language]:
296 | if isinstance(snippet, list):
297 | self.add_snippet(snippet[0], snippet[1], lang=str(language))
298 | self.scroll_layout.addStretch()
299 | self.change_lang(self.code_language)
300 | self.snippets_built = True
301 |
302 | def change_lang(self, lang, force_reload=True):
303 | """ Set the code language, clear the scroll layout and rebuild it as needed. """
304 | lang = str(lang).lower()
305 |
306 | if force_reload == False and lang == self.code_language:
307 | logging.debug("KS: Doing nothing because the language was already selected.")
308 | return False
309 |
310 | self.lang_selector.set_button(lang)
311 | self.code_language = lang
312 | logging.debug("Setting code language to " + lang)
313 |
314 | for snippets_item in self.all_snippets_items():
315 | snippets_item.setHidden(snippets_item.lang != self.code_language)
316 | return
317 |
318 | def all_snippets_items(self):
319 | """ Return a list of all SnippetItems. """
320 | all_widgets = (self.scroll_layout.itemAt(i).widget() for i in range(self.scroll_layout.count()))
321 | snippets_items = []
322 | for w in all_widgets:
323 | if isinstance(w, SnippetsItem):
324 | snippets_items.append(w)
325 | return snippets_items
326 |
327 | def add_snippet(self, key=None, code=None, lang=None):
328 | """ Create a new snippet field and focus on it. """
329 | key = key or ""
330 | code = code or ""
331 | lang = lang or self.code_language
332 | snippets_item = SnippetsItem(key, code, lang, self)
333 | snippets_item.btn_insert.clicked.connect(partial(self.insert_code, snippets_item))
334 | snippets_item.btn_duplicate.clicked.connect(partial(self.duplicate_snippet, snippets_item))
335 | snippets_item.btn_delete.clicked.connect(partial(self.delete_snippet, snippets_item))
336 | # snippets_item.setTitle("Key:")
337 | self.scroll_layout.insertWidget(0, snippets_item)
338 | snippets_item.key_lineedit.setFocus()
339 |
340 | def insert_code(self, snippet_item):
341 | """ Insert the code contained in snippet_item in the knobScripter's texteditmain. """
342 | self.knob_scripter = utils.getKnobScripter(self.knob_scripter)
343 | if self.knob_scripter:
344 | code = snippet_item.script_editor.toPlainText()
345 | self.knob_scripter.script_editor.addSnippetText(code)
346 |
347 | def duplicate_snippet(self, snippet_item):
348 | self.add_snippet(snippet_item.key_lineedit.text(), snippet_item.script_editor.toPlainText(), self.code_language)
349 |
350 | @staticmethod
351 | def delete_snippet(snippet_item):
352 | snippet_item.deleteLater()
353 |
354 | def sort_snippets(self, reverse=False):
355 | def code_key(snippets_item):
356 | return snippets_item.key_lineedit.text()
357 |
358 | snippets_items = sorted(self.all_snippets_items(), key=code_key, reverse=reverse)
359 |
360 | for w in reversed(snippets_items):
361 | self.scroll_layout.removeWidget(w)
362 | self.scroll_layout.insertWidget(0, w)
363 |
364 | def expand_snippets(self):
365 | for w in self.all_snippets_items():
366 | w.setCollapsed(False)
367 |
368 | def collapse_snippets(self):
369 | for w in self.all_snippets_items():
370 | w.setCollapsed(True)
371 |
372 | def save_all_snippets(self):
373 | # 1. Build snippet dict
374 | snippet_dict = {}
375 | for snippets_item in self.all_snippets_items():
376 | lang = snippets_item.lang
377 | key = snippets_item.key_lineedit.text()
378 | code = snippets_item.script_editor.toPlainText()
379 | if lang not in snippet_dict:
380 | snippet_dict[lang] = []
381 | if "" not in [key, code]:
382 | snippet_dict[lang].append([key, code])
383 | # 2. Notify...
384 | msg = "Are you sure you want to save all snippets?\nAny snippets deleted will be lost."
385 | if dialogs.ask(msg):
386 | # 3. Save!
387 | save_snippets_dict(snippet_dict)
388 |
389 | @staticmethod
390 | def snippets_help():
391 | # TODO make proper help... link to pdf or video?
392 | nuke.message("Snippets are a convenient way to save pieces of code you need to use over and over. "
393 | "By setting a code and shortcode, every time you write the shortcode on the script editor and "
394 | "press tab, the full code will be added. it also includes other convenient features. "
395 | "Please refer to the docs for more information.")
396 |
397 |
398 | class SnippetsItem(widgets.ToggableCodeGroup):
399 | """ widgets.ToggableGroup adapted specifically for a snippet item. """
400 |
401 | def __init__(self, key="", code="", lang="python", parent=None):
402 | super(SnippetsItem, self).__init__(parent=parent)
403 | self.parent = parent
404 | self.lang = lang
405 |
406 | self.title_label.setParent(None)
407 |
408 | # Add QLineEdit
409 | self.key_lineedit = QtWidgets.QLineEdit()
410 | self.key_lineedit.setMinimumWidth(20)
411 | self.key_lineedit.setStyleSheet("background:#222222;")
412 | f = self.key_lineedit.font()
413 | f.setWeight(QtGui.QFont.Bold)
414 | self.key_lineedit.setFont(f)
415 | self.key_lineedit.setText(str(key))
416 | self.top_clickable_layout.addWidget(self.key_lineedit)
417 |
418 | # Add buttons
419 | self.btn_insert = widgets.APToolButton("download")
420 | self.btn_insert.setToolTip("Insert code into KnobScripter editor")
421 | self.btn_duplicate = widgets.APToolButton("duplicate")
422 | self.btn_duplicate.setToolTip("Duplicate snippet")
423 | self.btn_delete = widgets.APToolButton("delete")
424 | self.btn_delete.setToolTip("Delete snippet")
425 |
426 | self.top_right_layout.addWidget(self.btn_insert)
427 | self.top_right_layout.addWidget(self.btn_duplicate)
428 | self.top_right_layout.addWidget(self.btn_delete)
429 |
430 | # Set code
431 | self.script_editor.set_code_language(lang.lower())
432 | self.script_editor.setPlainText(str(code))
433 |
434 | lines = self.script_editor.document().blockCount()
435 | lineheight = self.script_editor.fontMetrics().height()
436 |
437 | self.setFixedHeight(80 + lineheight * min(lines - 1, 4))
438 | self.grip_line.parent_min_size = (100, 80)
439 |
440 | self.setTabOrder(self.key_lineedit, self.script_editor)
441 |
--------------------------------------------------------------------------------
/KnobScripter/prefs.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ KnobScripter Prefs: Preferences widget (PrefsWidget) and utility function to load all preferences.
3 |
4 | The load_prefs function will load all preferences relative to the KnobScripter, both stored
5 | as variables in the config.py module and saved in the KS preferences json file.
6 |
7 | adrianpueyo.com
8 |
9 | """
10 |
11 | import json
12 | import os
13 | import nuke
14 |
15 | from KnobScripter.info import __version__, __author__, __date__
16 | from KnobScripter import config, widgets, utils
17 |
18 | try:
19 | if nuke.NUKE_VERSION_MAJOR < 11:
20 | from PySide import QtCore, QtGui, QtGui as QtWidgets
21 | from PySide.QtCore import Qt
22 | else:
23 | from PySide2 import QtWidgets, QtGui, QtCore
24 | from PySide2.QtCore import Qt
25 | except ImportError:
26 | from Qt import QtCore, QtGui, QtWidgets
27 |
28 |
29 | def load_prefs():
30 | """ Load prefs json file and overwrite config.prefs """
31 | # Setup paths
32 | config.ks_directory = os.path.join(os.path.expanduser("~"), ".nuke", config.prefs["ks_directory"])
33 | config.py_scripts_dir = os.path.join(config.ks_directory, config.prefs["ks_py_scripts_directory"])
34 | config.blink_dir = os.path.join(config.ks_directory, config.prefs["ks_blink_directory"])
35 | config.codegallery_user_txt_path = os.path.join(config.ks_directory, config.prefs["ks_codegallery_file"])
36 | config.snippets_txt_path = os.path.join(config.ks_directory, config.prefs["ks_snippets_file"])
37 | config.prefs_txt_path = os.path.join(config.ks_directory, config.prefs["ks_prefs_file"])
38 | config.py_state_txt_path = os.path.join(config.ks_directory, config.prefs["ks_py_state_file"])
39 | config.knob_state_txt_path = os.path.join(config.ks_directory, config.prefs["ks_knob_state_file"])
40 |
41 | # Setup config font
42 | config.script_editor_font = QtGui.QFont()
43 | config.script_editor_font.setStyleHint(QtGui.QFont.Monospace)
44 | config.script_editor_font.setFixedPitch(True)
45 | config.script_editor_font.setFamily("Monospace")
46 | config.script_editor_font.setPointSize(10)
47 |
48 | if not os.path.isfile(config.prefs_txt_path):
49 | return None
50 | else:
51 | with open(config.prefs_txt_path, "r") as f:
52 | prefs = json.load(f)
53 | for pref in prefs:
54 | config.prefs[pref] = prefs[pref]
55 | config.script_editor_font.setFamily(config.prefs["se_font_family"])
56 | config.script_editor_font.setPointSize(config.prefs["se_font_size"])
57 | return prefs
58 |
59 | def clear_knob_state_history():
60 | if not nuke.ask("Are you sure you want to clear all history of knob states?"):
61 | return
62 |
63 | # Per instance? Probably not
64 | # for ks in config.all_knobscripters:
65 | # if hasattr(ks, 'current_node_state_dict'):
66 | # ks.current_node_state_dict = {}
67 |
68 | # In memory
69 | config.knob_state_dict = {}
70 | # In file
71 | with open(config.knob_state_txt_path, "w") as f:
72 | json.dump({}, f)
73 |
74 | def clear_py_state_history():
75 | if not nuke.ask("Are you sure you want to clear all history of .py states?"):
76 | return
77 | # In memory
78 | config.py_state_dict = {}
79 | with open(config.py_state_txt_path, "w") as f:
80 | json.dump({}, f)
81 |
82 | class PrefsWidget(QtWidgets.QWidget):
83 | def __init__(self, knob_scripter="", _parent=QtWidgets.QApplication.activeWindow()):
84 | super(PrefsWidget, self).__init__(_parent)
85 | self.knob_scripter = knob_scripter
86 | self.initUI()
87 | self.refresh_prefs()
88 |
89 | def initUI(self):
90 | self.layout = QtWidgets.QVBoxLayout()
91 |
92 | # 1. Title (name, version)
93 | self.title_widget = QtWidgets.QWidget()
94 | self.title_layout = QtWidgets.QHBoxLayout()
95 | self.title_layout.setMargin(0)
96 | title_label = QtWidgets.QLabel("KnobScripter v" + __version__)
97 | title_label.setStyleSheet("font-weight:bold;color:#CCCCCC;font-size:20px;")
98 | built_label = QtWidgets.QLabel('Built {0}'.format(__date__))
99 | built_label.setStyleSheet("color:#555;font-size:9px;padding-top:10px;")
100 | subtitle_label = QtWidgets.QLabel("Script editor for python and callback knobs")
101 | subtitle_label.setStyleSheet("color:#999")
102 | line1 = widgets.HLine()
103 |
104 | img_ap = QtWidgets.QLabel()
105 | pixmap = QtGui.QPixmap(os.path.join(config.ICONS_DIR, "ap_tools.png"))
106 | img_ap.setPixmap(pixmap)
107 | img_ap.resize(pixmap.width(), pixmap.height())
108 | img_ap.setStyleSheet("padding-top: 3px;")
109 |
110 |
111 | signature = QtWidgets.QLabel(''
112 | 'adrianpueyo.com, 2016-{0}'.format(__date__.split(" ")[-1]))
113 |
114 | signature.setOpenExternalLinks(True)
115 | # signature.setStyleSheet('''color:#555;font-size:9px;padding-left: {}px;'''.format(pixmap.width()+4))
116 | signature.setStyleSheet('''color:#555;font-size:9px;''')
117 | signature.setAlignment(QtCore.Qt.AlignLeft)
118 |
119 | img_ks = QtWidgets.QLabel()
120 | pixmap = QtGui.QPixmap(os.path.join(config.ICONS_DIR, "knob_scripter.png"))
121 | img_ks.setPixmap(pixmap)
122 | img_ks.resize(pixmap.width(), pixmap.height())
123 |
124 | # self.title_layout.addWidget(img_ks)
125 | self.title_layout.addWidget(img_ap)
126 | self.title_layout.addSpacing(2)
127 | self.title_layout.addWidget(title_label)
128 | self.title_layout.addWidget(built_label)
129 | self.title_layout.addStretch()
130 | self.title_widget.setLayout(self.title_layout)
131 |
132 | self.layout.addWidget(self.title_widget)
133 | self.layout.addWidget(signature)
134 | self.layout.addWidget(line1)
135 |
136 | # 2. Scroll Area
137 | # 2.1. Inner scroll content
138 | self.scroll_content = QtWidgets.QWidget()
139 | self.scroll_layout = QtWidgets.QVBoxLayout()
140 | self.scroll_layout.setMargin(0)
141 |
142 | self.scroll_content.setLayout(self.scroll_layout)
143 | self.scroll_content.setContentsMargins(0, 0, 8, 0)
144 |
145 | # 2.2. External Scroll Area
146 | self.scroll = QtWidgets.QScrollArea()
147 | self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
148 | self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
149 | self.scroll.setWidgetResizable(True)
150 | self.scroll.setWidget(self.scroll_content)
151 | self.scroll.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
152 |
153 | self.layout.addWidget(self.scroll)
154 |
155 | # 3. Build prefs inside scroll layout
156 | self.form_layout = QtWidgets.QFormLayout()
157 | self.scroll_layout.addLayout(self.form_layout)
158 | self.scroll_layout.addStretch()
159 |
160 | # 3.1. General
161 | self.form_layout.addRow("General", QtWidgets.QWidget())
162 | # Font
163 | self.font_box = QtWidgets.QFontComboBox()
164 | self.font_box.currentFontChanged.connect(self.font_changed)
165 | self.form_layout.addRow("Font:", self.font_box)
166 |
167 | # Font size
168 | self.font_size_box = QtWidgets.QSpinBox()
169 | self.font_size_box.setMinimum(6)
170 | self.font_size_box.setMaximum(100)
171 | self.font_size_box.setFixedHeight(24)
172 | self.font_size_box.valueChanged.connect(self.font_size_changed)
173 | self.form_layout.addRow("Font size:", self.font_size_box)
174 |
175 | # Window size
176 | self.window_size_box = QtWidgets.QFrame()
177 | self.window_size_box.setContentsMargins(0, 0, 0, 0)
178 | window_size_layout = QtWidgets.QHBoxLayout()
179 | window_size_layout.setMargin(0)
180 | self.window_size_w_box = QtWidgets.QSpinBox()
181 | self.window_size_w_box.setValue(config.prefs["ks_default_size"][0])
182 | self.window_size_w_box.setMinimum(200)
183 | self.window_size_w_box.setMaximum(4000)
184 | self.window_size_w_box.setFixedHeight(24)
185 | self.window_size_w_box.setToolTip("Default window width in pixels")
186 | window_size_layout.addWidget(self.window_size_w_box)
187 | window_size_layout.addWidget(QtWidgets.QLabel("x"))
188 | self.window_size_h_box = QtWidgets.QSpinBox()
189 | self.window_size_h_box.setValue(config.prefs["ks_default_size"][1])
190 | self.window_size_h_box.setMinimum(100)
191 | self.window_size_h_box.setMaximum(2000)
192 | self.window_size_h_box.setFixedHeight(24)
193 | self.window_size_h_box.setToolTip("Default window height in pixels")
194 | window_size_layout.addWidget(self.window_size_h_box)
195 | self.window_size_box.setLayout(window_size_layout)
196 | self.form_layout.addRow("Floating window:", self.window_size_box)
197 |
198 | self.grab_dimensions_button = QtWidgets.QPushButton("Grab current dimensions")
199 | self.grab_dimensions_button.clicked.connect(self.grab_dimensions)
200 | self.form_layout.addRow("", self.grab_dimensions_button)
201 |
202 | # Save knob editor state
203 | self.knob_editor_state_box = QtWidgets.QFrame()
204 | self.knob_editor_state_box.setContentsMargins(0, 0, 0, 0)
205 | knob_editor_state_layout = QtWidgets.QHBoxLayout()
206 | knob_editor_state_layout.setMargin(0)
207 | self.save_knob_editor_state_combobox = QtWidgets.QComboBox()
208 | self.save_knob_editor_state_combobox.setToolTip("Save script editor state on knobs? "
209 | "(which knob is open in editor, cursor pos, scroll values)\n"
210 | " - Save in memory = active session only\n"
211 | " - Save to disk = active between sessions")
212 | self.save_knob_editor_state_combobox.addItem("Do not save", 0)
213 | self.save_knob_editor_state_combobox.addItem("Save in memory", 1)
214 | self.save_knob_editor_state_combobox.addItem("Save to disk", 2)
215 | knob_editor_state_layout.addWidget(self.save_knob_editor_state_combobox)
216 | self.clear_knob_history_button = QtWidgets.QPushButton("Clear history")
217 | self.clear_knob_history_button.clicked.connect(clear_knob_state_history)
218 | knob_editor_state_layout.addWidget(self.clear_knob_history_button)
219 | self.knob_editor_state_box.setLayout(knob_editor_state_layout)
220 | self.form_layout.addRow("Knob Editor State:", self.knob_editor_state_box)
221 |
222 | # Save .py editor state
223 | self.py_editor_state_box = QtWidgets.QFrame()
224 | self.py_editor_state_box.setContentsMargins(0, 0, 0, 0)
225 | py_editor_state_layout = QtWidgets.QHBoxLayout()
226 | py_editor_state_layout.setMargin(0)
227 | self.save_py_editor_state_combobox = QtWidgets.QComboBox()
228 | self.save_py_editor_state_combobox.setToolTip("Save script editor state on .py scripts? "
229 | "(which script is open in editor, cursor pos, scroll values)\n"
230 | " - Save in memory = active session only\n"
231 | " - Save to disk = active between sessions")
232 | self.save_py_editor_state_combobox.addItem("Do not save", 0)
233 | self.save_py_editor_state_combobox.addItem("Save in memory", 1)
234 | self.save_py_editor_state_combobox.addItem("Save to disk", 2)
235 | py_editor_state_layout.addWidget(self.save_py_editor_state_combobox)
236 | self.clear_py_history_button = QtWidgets.QPushButton("Clear history")
237 | self.clear_py_history_button.clicked.connect(clear_py_state_history)
238 | py_editor_state_layout.addWidget(self.clear_py_history_button)
239 | self.py_editor_state_box.setLayout(py_editor_state_layout)
240 | self.form_layout.addRow(".py Editor State:", self.py_editor_state_box)
241 |
242 |
243 | # 3.2. Python
244 | self.form_layout.addRow(" ", None)
245 | self.form_layout.addRow("Python", QtWidgets.QWidget())
246 |
247 | # Tab spaces
248 | self.tab_spaces_combobox = QtWidgets.QComboBox()
249 | self.tab_spaces_combobox.addItem("2", 2)
250 | self.tab_spaces_combobox.addItem("4", 4)
251 | self.tab_spaces_combobox.currentIndexChanged.connect(self.tab_spaces_changed)
252 | self.form_layout.addRow("Tab spaces:", self.tab_spaces_combobox)
253 |
254 | # Color scheme
255 | self.python_color_scheme_combobox = QtWidgets.QComboBox()
256 | self.python_color_scheme_combobox.addItem("nuke", "nuke")
257 | self.python_color_scheme_combobox.addItem("monokai", "monokai")
258 | self.python_color_scheme_combobox.currentIndexChanged.connect(self.color_scheme_changed)
259 | self.form_layout.addRow("Color scheme:", self.python_color_scheme_combobox)
260 |
261 | # Run in context
262 | self.run_in_context_checkbox = QtWidgets.QCheckBox("Run in context")
263 | self.run_in_context_checkbox.setToolTip("Default mode for running code in context (when in node mode).")
264 | # self.run_in_context_checkbox.stateChanged.connect(self.run_in_context_changed)
265 | self.form_layout.addRow("", self.run_in_context_checkbox)
266 |
267 | # Show labels
268 | self.show_knob_labels_checkbox = QtWidgets.QCheckBox("Show knob labels")
269 | self.show_knob_labels_checkbox.setToolTip("Display knob labels on the knob dropdown\n"
270 | "Otherwise, show the internal name only.")
271 | self.form_layout.addRow("", self.show_knob_labels_checkbox)
272 |
273 | # 3.3. Blink
274 | self.form_layout.addRow(" ", None)
275 | self.form_layout.addRow("Blink", QtWidgets.QWidget())
276 |
277 | # Color scheme
278 | # self.blink_color_scheme_combobox = QtWidgets.QComboBox()
279 | # self.blink_color_scheme_combobox.addItem("nuke default")
280 | # self.blink_color_scheme_combobox.addItem("adrians flavour")
281 | # self.form_layout.addRow("Tab spaces:", self.blink_color_scheme_combobox)
282 | self.autosave_on_compile_checkbox = QtWidgets.QCheckBox("Auto-save to disk on compile")
283 | self.autosave_on_compile_checkbox.setToolTip("Set the default value for Auto-save to disk on compile.")
284 | self.form_layout.addRow("", self.autosave_on_compile_checkbox)
285 |
286 | # 4. Lower buttons?
287 | self.lower_buttons_layout = QtWidgets.QHBoxLayout()
288 | self.lower_buttons_layout.addStretch()
289 |
290 | self.save_prefs_button = QtWidgets.QPushButton("Save")
291 | self.save_prefs_button.clicked.connect(self.save_prefs)
292 | self.lower_buttons_layout.addWidget(self.save_prefs_button)
293 | self.apply_prefs_button = QtWidgets.QPushButton("Apply")
294 | self.apply_prefs_button.clicked.connect(self.apply_prefs)
295 | self.lower_buttons_layout.addWidget(self.apply_prefs_button)
296 | self.cancel_prefs_button = QtWidgets.QPushButton("Cancel")
297 | self.cancel_prefs_button.clicked.connect(self.cancel_prefs)
298 | self.lower_buttons_layout.addWidget(self.cancel_prefs_button)
299 |
300 | self.layout.addLayout(self.lower_buttons_layout)
301 | self.setLayout(self.layout)
302 |
303 | def font_size_changed(self):
304 | config.script_editor_font.setPointSize(self.font_size_box.value())
305 | for ks in config.all_knobscripters:
306 | if hasattr(ks, 'script_editor'):
307 | ks.script_editor.setFont(config.script_editor_font)
308 |
309 | def font_changed(self):
310 | self.font = self.font_box.currentFont().family()
311 | config.script_editor_font.setFamily(self.font)
312 | for ks in config.all_knobscripters:
313 | if hasattr(ks, 'script_editor'):
314 | ks.script_editor.setFont(config.script_editor_font)
315 |
316 | def tab_spaces_changed(self):
317 | config.prefs["se_tab_spaces"] = self.tab_spaces_combobox.currentData()
318 | for ks in config.all_knobscripters:
319 | if hasattr(ks, 'highlighter'):
320 | ks.highlighter.rehighlight()
321 | return
322 |
323 | def color_scheme_changed(self):
324 | config.prefs["code_style_python"] = self.python_color_scheme_combobox.currentData()
325 | for ks in config.all_knobscripters:
326 | if hasattr(ks, 'script_editor'):
327 | if ks.script_editor.code_language == "python":
328 | ks.script_editor.highlighter.setStyle(config.prefs["code_style_python"])
329 | ks.script_editor.highlighter.rehighlight()
330 | return
331 |
332 | def grab_dimensions(self):
333 | self.knob_scripter = utils.getKnobScripter(self.knob_scripter)
334 | self.window_size_w_box.setValue(self.knob_scripter.width())
335 | self.window_size_h_box.setValue(self.knob_scripter.height())
336 |
337 | def refresh_prefs(self):
338 | """ Reload the json prefs, apply them on config.prefs, and repopulate the knobs """
339 | load_prefs()
340 |
341 | self.font_box.setCurrentFont(QtGui.QFont(config.prefs["se_font_family"]))
342 | self.font_size_box.setValue(config.prefs["se_font_size"])
343 |
344 | self.window_size_w_box.setValue(config.prefs["ks_default_size"][0])
345 | self.window_size_h_box.setValue(config.prefs["ks_default_size"][1])
346 |
347 | self.show_knob_labels_checkbox.setChecked(config.prefs["ks_show_knob_labels"] is True)
348 | self.run_in_context_checkbox.setChecked(config.prefs["ks_run_in_context"] is True)
349 |
350 | self.save_knob_editor_state_combobox.setCurrentIndex(config.prefs["ks_save_knob_state"])
351 | self.save_py_editor_state_combobox.setCurrentIndex(config.prefs["ks_save_py_state"])
352 |
353 | i = self.python_color_scheme_combobox.findData(config.prefs["code_style_python"])
354 | if i != -1:
355 | self.python_color_scheme_combobox.setCurrentIndex(i)
356 |
357 | i = self.tab_spaces_combobox.findData(config.prefs["se_tab_spaces"])
358 | if i != -1:
359 | self.tab_spaces_combobox.setCurrentIndex(i)
360 |
361 | self.autosave_on_compile_checkbox.setChecked(config.prefs["ks_blink_autosave_on_compile"])
362 |
363 | def get_prefs_dict(self):
364 | """ Return a dictionary with the prefs from the current knob state """
365 | ks_prefs = {
366 | "ks_default_size": [self.window_size_w_box.value(), self.window_size_h_box.value()],
367 | "ks_run_in_context": self.run_in_context_checkbox.isChecked(),
368 | "ks_show_knob_labels": self.show_knob_labels_checkbox.isChecked(),
369 | "ks_blink_autosave_on_compile": self.autosave_on_compile_checkbox.isChecked(),
370 | "ks_save_knob_state": self.save_knob_editor_state_combobox.currentData(),
371 | "ks_save_py_state": self.save_py_editor_state_combobox.currentData(),
372 | "code_style_python": self.python_color_scheme_combobox.currentData(),
373 | "se_font_family": self.font_box.currentFont().family(),
374 | "se_font_size": self.font_size_box.value(),
375 | "se_tab_spaces": self.tab_spaces_combobox.currentData(),
376 | }
377 | return ks_prefs
378 |
379 | def save_config(self, prefs=None):
380 | """ Save the given prefs dict in config.prefs """
381 | if not prefs:
382 | prefs = self.get_prefs_dict()
383 | for pref in prefs:
384 | config.prefs[pref] = prefs[pref]
385 | config.script_editor_font.setFamily(config.prefs["se_font_family"])
386 | config.script_editor_font.setPointSize(config.prefs["se_font_size"])
387 |
388 | def save_prefs(self):
389 | """ Save current prefs on json, config, and apply on KnobScripters """
390 | # 1. Save json
391 | ks_prefs = self.get_prefs_dict()
392 | with open(config.prefs_txt_path, "w") as f:
393 | json.dump(ks_prefs, f, sort_keys=True, indent=4)
394 | nuke.message("Preferences saved!")
395 |
396 | # 2. Save config
397 | self.save_config(ks_prefs)
398 |
399 | # 3. Apply on KnobScripters
400 | self.apply_prefs()
401 |
402 | def apply_prefs(self):
403 | """ Apply the current knob values to the KnobScripters """
404 | self.save_config()
405 | for ks in config.all_knobscripters:
406 | ks.script_editor.setFont(config.script_editor_font)
407 | ks.script_editor.tab_spaces = config.prefs["se_tab_spaces"]
408 | ks.script_editor.highlighter.rehighlight()
409 | ks.runInContext = config.prefs["ks_run_in_context"]
410 | ks.runInContextAct.setChecked(config.prefs["ks_run_in_context"])
411 | ks.show_labels = config.prefs["ks_show_knob_labels"]
412 | ks.blink_autoSave_act.setChecked(config.prefs["ks_blink_autosave_on_compile"])
413 | # TODO Apply the "ks_save_py_state" and "ks_save_knob_state" here too
414 | if ks.nodeMode:
415 | ks.refreshClicked()
416 |
417 | def cancel_prefs(self):
418 | """ Revert to saved json prefs """
419 | # 1. Reload json and populate knobs
420 | self.refresh_prefs()
421 | # 2. Apply values to KnobScripters
422 | self.apply_prefs()
423 | # 3. If this is a floating panel, close it??
424 |
--------------------------------------------------------------------------------
/KnobScripter/codegallery.py:
--------------------------------------------------------------------------------
1 | import nuke
2 | import os
3 | import logging
4 | import json
5 | from functools import partial
6 |
7 | try:
8 | if nuke.NUKE_VERSION_MAJOR < 11:
9 | from PySide import QtCore, QtGui, QtGui as QtWidgets
10 | from PySide.QtCore import Qt
11 | else:
12 | from PySide2 import QtWidgets, QtGui, QtCore
13 | from PySide2.QtCore import Qt
14 | except ImportError:
15 | from Qt import QtCore, QtGui, QtWidgets
16 |
17 | from KnobScripter import utils, snippets, widgets, config, content, ksscripteditor
18 |
19 | code_gallery_dict = {
20 | "blink": [
21 | {
22 | "title": "Kernel skeleton",
23 | "desc": "Basic code structure for starting a Blink kernel.",
24 | "cat": ["Base codes"],
25 | "code": """\nkernel KernelName : ImageComputationKernel\n{\n Image src;\n Image dst;\n\n param:\n\n\n local:\n\n\n void init() {\n\n }\n\n void process(int2 pos) {\n dst() = src();\n }\n};\n""",
26 | "editor_height": 40,
27 | },
28 | {
29 | "title": "Process function",
30 | "desc": "Example template for the main processing function in Blink.",
31 | "cat": ["Base codes"],
32 | "code": """void process() {\n // Read the input image\n SampleType(src) input = src();\n\n // Isolate the RGB components\n float3 srcPixel(input.x, input.y, input.z);\n\n // Calculate luma\n float luma = srcPixel.x * coefficients.x\n + srcPixel.y * coefficients.y\n + srcPixel.z * coefficients.z;\n // Apply saturation\n float3 saturatedPixel = (srcPixel - luma) * saturation + luma;\n\n // Write the result to the output image\n dst() = float4(saturatedPixel.x, saturatedPixel.y, saturatedPixel.z, input.w);\n }"""
33 | },
34 | {
35 | "title": "Longer text? what would happen exactly? lets try it like right now yes yes yes yes yes ",
36 | "desc": "Example template for the main processing function in Blink. this is the same but with a way longer description to see what happens... lets see!!!!.",
37 | "cat": ["Base codes", "Example"],
38 | "code": """void process() {\n // Read the input image\n SampleType(src) input = src();\n\n // Isolate the RGB components\n float3 srcPixel(input.x, input.y, input.z);\n\n // Calculate luma\n float luma = srcPixel.x * coefficients.x\n + srcPixel.y * coefficients.y\n + srcPixel.z * coefficients.z;\n // Apply saturation\n float3 saturatedPixel = (srcPixel - luma) * saturation + luma;\n\n // Write the result to the output image\n dst() = float4(saturatedPixel.x, saturatedPixel.y, saturatedPixel.z, input.w);\n }"""
39 | },
40 | ],
41 | "python": [
42 | {
43 | "title": "print statement",
44 | "desc": "Simple print statement...",
45 | "cat": ["Base codes"],
46 | "code": """print("2")""",
47 | },
48 | ],
49 | }
50 |
51 |
52 | def get_categories(code_dict=None):
53 | """ Return a list of available categories for the specified code_dict (or the default one if not specified). """
54 | code_dict = code_dict or load_code_gallery_dict(config.codegallery_user_txt_path)
55 | categories = []
56 | for lang in code_dict:
57 | for code_item in code_dict[lang]:
58 | if "cat" in code_item.keys():
59 | cat = code_item["cat"]
60 | if isinstance(cat, list):
61 | categories.extend(cat)
62 | return list(set(categories))
63 |
64 | def load_all_code_gallery_dicts():
65 | """ Return a dictionary that contains the code gallery dicts from all different paths. """
66 | # TODO This function!!!! to also include the other paths, not only the user specified...
67 | user_dict = config.code_gallery_files
68 | full_dict = dict()
69 | for file in config.code_gallery_files+[config.codegallery_user_txt_path]:
70 | file_dict = load_code_gallery_dict(file)
71 | logging.debug(file)
72 | for key in file_dict.keys():
73 | if key not in full_dict.keys():
74 | full_dict[key] = []
75 | for single_code_dict in file_dict[key]:
76 | full_dict[key].append(single_code_dict)
77 | logging.debug(full_dict)
78 | return full_dict
79 |
80 | def load_code_gallery_dict(path=None):
81 | '''
82 | Load the codes from the user json path as a dict. Return dict()
83 | '''
84 | #return code_gallery_dict #TEMPORARY
85 |
86 | if not path:
87 | path = config.codegallery_user_txt_path
88 | if not os.path.isfile(path):
89 | logging.debug("Path doesn't exist: "+path)
90 | return dict()
91 | else:
92 | try:
93 | with open(path, "r") as f:
94 | code_dict = json.load(f)
95 | return code_dict
96 | except:
97 | logging.debug("Couldn't open file: {}.\nLoading empty dict instead.".format(path))
98 | return dict()
99 |
100 | def save_code_gallery_dict(code_dict, path=None):
101 | ''' Perform a json dump of the code gallery into the path. '''
102 | if not path:
103 | path = config.codegallery_user_txt_path
104 | with open(path, "w") as f:
105 | json.dump(code_dict, f, sort_keys=True, indent=4)
106 | content.code_gallery_dict = code_dict
107 |
108 | def append_code(code, title=None, desc=None, categories = None, path=None, lang="python"):
109 | """ Load the codegallery file as a dict and append a code. """
110 | if code == "":
111 | return False
112 | path = path or config.codegallery_user_txt_path
113 | title = title or ""
114 | desc = desc or ""
115 | categories = categories or get_categories()
116 | lang = lang.lower()
117 | all_codes = load_code_gallery_dict(path)
118 | if code == "":
119 | return False
120 | if lang not in all_codes:
121 | all_codes[lang] = []
122 | single_code_dict = dict()
123 | single_code_dict["title"] = title
124 | single_code_dict["desc"] = desc
125 | single_code_dict["cat"] = categories
126 | single_code_dict["code"] = code
127 | all_codes[lang].append(single_code_dict)
128 | save_code_gallery_dict(all_codes, path)
129 |
130 |
131 | class AppendCodePanel(QtWidgets.QDialog):
132 | def __init__(self, parent=None, code=None, title=None, desc=None, cat=None, lang="python", path=None):
133 | super(AppendCodePanel, self).__init__(parent)
134 |
135 | self.lang = lang
136 | title = title or ""
137 | desc = desc or ""
138 | cat = cat or []
139 | self.path = path or config.codegallery_user_txt_path
140 | self.existing_code_dict = load_code_gallery_dict(self.path)
141 | self.existing_categories = get_categories(self.existing_code_dict)
142 |
143 | # Layout
144 | self.layout = QtWidgets.QVBoxLayout()
145 |
146 | # Code language
147 | self.lang_selector = widgets.RadioSelector(["Python", "Blink", "All"])
148 | self.lang_selector.radio_selected.connect(self.change_lang)
149 |
150 | # Title
151 | self.title_lineedit = QtWidgets.QLineEdit(title)
152 | f = self.title_lineedit.font()
153 | f.setWeight(QtGui.QFont.Bold)
154 | self.title_lineedit.setFont(f)
155 |
156 | # Description
157 | self.description_lineedit = QtWidgets.QLineEdit(title)
158 |
159 | # Category
160 | self.category_combobox = QtWidgets.QComboBox()
161 | self.category_combobox.setEditable(True)
162 | self.category_combobox.setSizePolicy(QtWidgets.QSizePolicy.Expanding,QtWidgets.QSizePolicy.Expanding)
163 | #self.category_combobox.lineEdit().setText("")
164 | self.category_combobox.addItem("","")
165 | for cat in self.existing_categories:
166 | self.category_combobox.addItem(str(cat), str(cat))
167 |
168 | # Code
169 | self.script_editor = ksscripteditor.KSScriptEditor()
170 | self.script_editor.setPlainText(code)
171 | se_policy = self.script_editor.sizePolicy()
172 | se_policy.setVerticalStretch(1)
173 | self.script_editor.setSizePolicy(se_policy)
174 |
175 | # Warnings
176 | self.warnings_label = QtWidgets.QLabel("Please set a code and title.")
177 | self.warnings_label.setStyleSheet("color: #D65; font-style: italic;")
178 | self.warnings_label.setWordWrap(True)
179 | self.warnings_label.mouseReleaseEvent = lambda x: self.warnings_label.hide()
180 |
181 | # Buttons
182 | self.button_box = QtWidgets.QDialogButtonBox(
183 | QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel)
184 | self.button_box.accepted.connect(self.save_pressed)
185 | self.button_box.rejected.connect(self.cancel_pressed)
186 |
187 | # Form layout
188 | self.form = QtWidgets.QFormLayout()
189 | self.form.addRow("Language: ", self.lang_selector)
190 | self.form.addRow("Title: ", self.title_lineedit)
191 | self.form.addRow("Description: ", self.description_lineedit)
192 | self.form.addRow("Category: ", self.category_combobox)
193 | self.form.addRow("Code: ", self.script_editor)
194 | self.form.addRow("", self.warnings_label)
195 | self.warnings_label.hide()
196 | self.form.setFieldGrowthPolicy(QtWidgets.QFormLayout.ExpandingFieldsGrow)
197 |
198 | self.layout.addLayout(self.form)
199 | self.layout.addWidget(self.button_box)
200 | self.setLayout(self.layout)
201 |
202 | # Init values
203 | self.setWindowTitle("Add Code to Code Gallery")
204 | self.lang_selector.set_button(self.lang)
205 | self.script_editor.set_code_language(self.lang)
206 | self.title_lineedit.setFocus()
207 | self.title_lineedit.selectAll()
208 |
209 | def change_lang(self, lang):
210 | self.script_editor.set_code_language(str(lang.lower()))
211 |
212 | def save_pressed(self):
213 | title = self.title_lineedit.text()
214 | description = self.description_lineedit.text()
215 | categories_str = self.category_combobox.lineEdit().text()
216 | categories = [c.strip() for c in categories_str.split(",")]
217 | categories = [c for c in categories if len(c)]
218 | code = self.script_editor.toPlainText()
219 | lang = self.lang_selector.selected_text()
220 | if "" in [code,title]:
221 | self.warnings_label.show()
222 | return False
223 | logging.debug(
224 | "Code to be saved \nLang:\n{0}\nTitle:\n{1}\nDescription:\n{2}\nCategory:\n{3}\nCode:\n{4}\n------".format(lang, title, description, categories, code))
225 | append_code(code, title, description, categories, lang=lang)
226 | code_gallery_dict = load_code_gallery_dict()
227 | try:
228 | content.code_gallery_dict = code_gallery_dict
229 | except Exception as e:
230 | logging.debug(e)
231 | self.accept()
232 |
233 | def cancel_pressed(self):
234 | if self.script_editor.toPlainText() != "":
235 | msg = "Do you wish to discard the changes?"
236 | if not dialogs.ask(msg, self, default_yes=False):
237 | return False
238 | self.reject()
239 |
240 |
241 | class CodeGalleryWidget(QtWidgets.QWidget):
242 | def __init__(self, knob_scripter="", _parent=QtWidgets.QApplication.activeWindow(), lang="python"):
243 | super(CodeGalleryWidget, self).__init__(_parent)
244 |
245 | self.knob_scripter = knob_scripter
246 | self.code_language = lang
247 |
248 | self.initUI()
249 | self.change_lang(self.code_language)
250 |
251 | def initUI(self):
252 | self.layout = QtWidgets.QVBoxLayout()
253 |
254 | # 1. Filters (language etc)
255 | self.filter_widget = QtWidgets.QFrame()
256 | filter_layout = QtWidgets.QHBoxLayout()
257 | code_language_label = QtWidgets.QLabel("Language:")
258 | filter_layout.addWidget(code_language_label)
259 | # TODO Compatible with expressions and TCL knobs too!!
260 | self.lang_selector = widgets.RadioSelector(["Python", "Blink", "All"])
261 | self.lang_selector.radio_selected.connect(self.change_lang)
262 | filter_layout.addWidget(self.lang_selector)
263 | filter_layout.addStretch()
264 | self.reload_button = QtWidgets.QPushButton("Reload")
265 | self.reload_button.clicked.connect(self.reload)
266 | filter_layout.setMargin(0)
267 | filter_layout.addWidget(self.reload_button)
268 |
269 | self.filter_widget.setLayout(filter_layout)
270 | self.layout.addWidget(self.filter_widget)
271 | self.layout.addWidget(widgets.HLine())
272 |
273 | # 2. Scroll Area
274 | # 2.1. Inner scroll content
275 | self.scroll_content = QtWidgets.QWidget()
276 | self.scroll_layout = QtWidgets.QVBoxLayout()
277 | self.scroll_layout.setMargin(0)
278 | self.scroll_layout.addStretch()
279 | self.scroll_content.setLayout(self.scroll_layout)
280 | self.scroll_content.setContentsMargins(0, 0, 8, 0)
281 |
282 | self.change_lang(self.code_language, force_reload=True)
283 |
284 | # 2.2. External Scroll Area
285 | self.scroll = QtWidgets.QScrollArea()
286 | self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
287 | self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
288 | self.scroll.setWidgetResizable(True)
289 | self.scroll.setWidget(self.scroll_content)
290 | self.scroll.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding)
291 |
292 | self.layout.addWidget(self.scroll)
293 |
294 | # 3. Lower buttons
295 | self.lower_layout = QtWidgets.QHBoxLayout()
296 |
297 | self.add_code_btn = widgets.APToolButton("add_filled")
298 | self.add_code_btn.setToolTip("Add new code")
299 | self.add_code_btn.clicked.connect(self.add_code)
300 |
301 | self.v_expand_btn = widgets.APToolButton("v_expand", icon_size=22)
302 | self.v_expand_btn.setToolTip("Expand all codes")
303 | self.v_expand_btn.clicked.connect(self.expand_codes)
304 | self.v_collapse_btn = widgets.APToolButton("v_collapse", icon_size=22)
305 | self.v_collapse_btn.setToolTip("Collapse all codes")
306 | self.v_collapse_btn.clicked.connect(self.collapse_codes)
307 |
308 | self.help_btn = widgets.APToolButton("help_filled")
309 | self.help_btn.setToolTip("Help")
310 | self.help_btn.clicked.connect(self.show_help)
311 |
312 | self.lower_layout.addWidget(self.add_code_btn)
313 | self.lower_layout.addSpacing(12)
314 | self.lower_layout.addWidget(self.v_expand_btn)
315 | self.lower_layout.addWidget(self.v_collapse_btn)
316 | self.lower_layout.addStretch()
317 | self.lower_layout.addWidget(self.help_btn)
318 |
319 | self.layout.addWidget(widgets.HLine())
320 | self.layout.addLayout(self.lower_layout)
321 |
322 | self.setLayout(self.layout)
323 |
324 | def reload(self):
325 | """ Force a rebuild of the widgets in the current filter status. """
326 | lang = self.lang_selector.selected_text()
327 | self.change_lang(lang, force_reload=True)
328 |
329 | def change_lang(self, lang, force_reload=False):
330 | """ Set the code language, clear the scroll layout and rebuild it as needed. """
331 | lang = lang.lower()
332 |
333 | if force_reload == False and lang == self.code_language:
334 | logging.debug("KS: Doing nothing because the language was already selected.")
335 | return False
336 | elif force_reload:
337 | pass
338 |
339 |
340 | self.lang_selector.set_button(lang)
341 | self.code_language = lang
342 | logging.debug("Setting code language to " + lang)
343 |
344 | # Clear scroll area
345 | utils.clear_layout(self.scroll_layout)
346 |
347 | code_gallery_dict = load_all_code_gallery_dicts()
348 |
349 | # Build widgets as needed
350 | if lang == "all":
351 | for lang in code_gallery_dict.keys():
352 | tg = widgets.ToggableGroup(self)
353 | tg.setTitle("{}".format(lang.capitalize()))
354 | self.build_gallery_group(code_gallery_dict[lang], tg.content_layout, lang=lang)
355 | self.scroll_layout.insertWidget(-1, tg)
356 | self.scroll_layout.addSpacing(10)
357 | elif lang in code_gallery_dict:
358 | self.build_gallery_group(code_gallery_dict[lang], self.scroll_layout, lang=lang)
359 | self.scroll_layout.addStretch()
360 |
361 | def build_gallery_group(self, code_list, layout, lang="python"):
362 | """ Given a list of code gallery items, it builds the widgets in the given layout """
363 | # 1. Get available categories
364 | categories = []
365 | for code in code_list:
366 | for cat in code["cat"]:
367 | categories.append(cat)
368 | categories = list(set(categories))
369 |
370 | # 2. Build gallery items
371 | for cat in categories:
372 | tg = widgets.ToggableGroup(self)
373 | tg.setTitle("{}".format(cat))
374 | for code in code_list:
375 | if cat in code["cat"]:
376 | cgi = self.code_gallery_item(code, lang=lang)
377 | tg.content_layout.addWidget(cgi)
378 |
379 | layout.insertWidget(-1, tg)
380 | layout.addSpacing(4)
381 |
382 | def code_gallery_item(self, code, lang="python"):
383 | """ Given a code dict, returns the corresponding code gallery widget. """
384 | if not all(i in code for i in ["title", "code"]):
385 | return False
386 | cgi = CodeGalleryItem(self)
387 |
388 | # 1. Title/description
389 | title = "{0}".format(code["title"])
390 | if "desc" in code:
391 | title += "
{}".format(code["desc"])
392 | cgi.setTitle(title)
393 |
394 | cgi.btn_insert_code.clicked.connect(partial(self.insert_code, cgi))
395 | cgi.btn_save_snippet.clicked.connect(partial(self.save_snippet, cgi))
396 |
397 | # 2. Content
398 | cgi.script_editor.set_code_language(lang.lower())
399 | # cgi.script_editor.setFont(config.script_editor_font)
400 | cgi.script_editor.setPlainText(code["code"])
401 |
402 | if "editor_height" in code:
403 | cgi.setFixedHeight(cgi.top_layout.sizeHint().height() + 40 + code["editor_height"])
404 | else:
405 | cgi.setFixedHeight(cgi.top_layout.sizeHint().height() + 140)
406 |
407 | return cgi
408 |
409 | def add_code(self):
410 | """ Bring up a panel to add a new code to the Code Gallery. """
411 | codepanel = AppendCodePanel(self, lang=self.code_language)
412 | codepanel.show()
413 |
414 | def insert_code(self, code_gallery_item):
415 | """ Insert the code contained in code_gallery_item in the knobScripter's texteditmain. """
416 | self.knob_scripter = utils.getKnobScripter(self.knob_scripter)
417 | if self.knob_scripter:
418 | code = code_gallery_item.script_editor.toPlainText()
419 | self.knob_scripter.script_editor.addSnippetText(code)
420 |
421 | def save_snippet(self, code_gallery_item, shortcode=""):
422 | """ Save the current code as a snippet (by introducing a shortcode) """
423 | # while...
424 | code = code_gallery_item.script_editor.toPlainText()
425 | lang = code_gallery_item.script_editor.code_language
426 | asp = snippets.AppendSnippetPanel(self, code, shortcode, lang=lang)
427 | asp.show()
428 |
429 | def all_code_groups(self):
430 | """ Return a list of all Code Gallery Groups. """
431 | all_scroll_widgets = (self.scroll_layout.itemAt(i).widget() for i in range(self.scroll_layout.count()))
432 | gallery_groups = []
433 | for g in all_scroll_widgets:
434 | if isinstance(g, widgets.ToggableGroup):
435 | gallery_groups.append(g)
436 | return gallery_groups
437 |
438 | def all_codegallery_items(self, code_groups=None):
439 | """ Return a list of all CodeGalleryItems. """
440 | if not code_groups:
441 | code_groups = self.all_code_groups()
442 |
443 | codegallery_items = []
444 | for g in code_groups:
445 | all_subwidgets = (g.content_layout.itemAt(i).widget() for i in range(g.content_layout.count()))
446 | for w in all_subwidgets:
447 | if isinstance(w, CodeGalleryItem):
448 | codegallery_items.append(w)
449 | return codegallery_items
450 |
451 | def expand_codes(self):
452 | code_groups = self.all_code_groups()
453 | for w in code_groups + self.all_codegallery_items(code_groups):
454 | w.setCollapsed(False)
455 |
456 | def collapse_codes(self):
457 | code_groups = self.all_code_groups()
458 | for w in code_groups + self.all_codegallery_items(code_groups):
459 | w.setCollapsed(True)
460 |
461 | def show_help(self):
462 | # TODO make proper help... link to pdf or video?
463 | nuke.message("The Code Gallery is a convenient place for code reference. It allows yourself or your studio "
464 | "to have a gallery of useful pieces of code, categorized and accompanied by a title and short "
465 | "description. \n\n"
466 | "Please refer to the docs for more information.")
467 |
468 |
469 | class CodeGalleryItem(widgets.ToggableCodeGroup):
470 | """ widgets.ToggableGroup adapted specifically for a code gallery item. """
471 |
472 | def __init__(self, parent=None):
473 | super(CodeGalleryItem, self).__init__(parent=parent)
474 | self.parent = parent
475 |
476 | # Add buttons
477 | btn1_text = "Insert code"
478 | self.btn_insert_code = QtWidgets.QPushButton(btn1_text)
479 | self.btn_insert_code.setMaximumWidth(self.btn_insert_code.fontMetrics().boundingRect(btn1_text).width() + 14)
480 |
481 | btn2_text = "Save snippet"
482 | self.btn_save_snippet = QtWidgets.QPushButton(btn2_text)
483 | self.btn_save_snippet.setMaximumWidth(self.btn_save_snippet.fontMetrics().boundingRect(btn2_text).width() + 14)
484 |
485 | self.top_right_layout.addWidget(self.btn_insert_code)
486 | self.top_right_layout.addWidget(self.btn_save_snippet)
487 |
--------------------------------------------------------------------------------
/KnobScripter/ksscripteditor.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ Base script editor class for KnobScripter.
3 |
4 | The KSScriptEditor is a QPlainTextEdit adapted for scripting: it provides a line number area,
5 | and extended functionality for duplicating or moving lines.
6 | Wouter Gilsing built an incredibly useful python script editor for his Hotbox Manager (v1.5).
7 | Credit to him: http://www.woutergilsing.com/
8 | Starting from his code, I changed the style and added extra functionality.
9 |
10 | adrianpueyo.com
11 |
12 | """
13 |
14 | import nuke
15 | import re
16 | import logging
17 |
18 | try:
19 | if nuke.NUKE_VERSION_MAJOR < 11:
20 | from PySide import QtCore, QtGui, QtGui as QtWidgets
21 | from PySide.QtCore import Qt
22 | else:
23 | from PySide2 import QtWidgets, QtGui, QtCore
24 | from PySide2.QtCore import Qt
25 | except ImportError:
26 | from Qt import QtCore, QtGui, QtWidgets
27 |
28 | from KnobScripter import config, blinkhighlighter, pythonhighlighter
29 |
30 |
31 | class KSScriptEditor(QtWidgets.QPlainTextEdit):
32 | """ Base Script Editor Widget
33 |
34 | Wouter Gilsing built an incredibly useful python script editor for his Hotbox Manager (v1.5).
35 | Credit to him: http://www.woutergilsing.com/
36 | Starting from his code, I changed the style and added extra functionality.
37 | """
38 |
39 | def __init__(self, knob_scripter=""):
40 | super(KSScriptEditor, self).__init__()
41 |
42 | self.knobScripter = knob_scripter
43 | self.selected_text = ""
44 |
45 | self.highlighter = None
46 | self.code_language = None
47 |
48 | # Setup line numbers
49 | self.tab_spaces = config.prefs["se_tab_spaces"]
50 |
51 | self.lineColor = None
52 | self.lineNumberAreaColor = None
53 | self.lineNumberColor = None
54 | self.currentLineNumberColor = None
55 | self.setColorStyle()
56 | self.setFont(config.script_editor_font)
57 |
58 | self.lineNumberArea = KSLineNumberArea(self)
59 | self.blockCountChanged.connect(self.updateLineNumberAreaWidth)
60 | self.updateRequest.connect(self.updateLineNumberArea)
61 | self.updateLineNumberAreaWidth()
62 |
63 | # Highlight line
64 | self.cursorPositionChanged.connect(self.highlightCurrentLine)
65 |
66 | def lineNumberAreaWidth(self):
67 | digits = 1
68 | max_num = max(1, self.blockCount())
69 | while max_num >= 10:
70 | max_num /= 10
71 | digits += 1
72 |
73 | space = 7 + self.fontMetrics().horizontalAdvance('9') * digits
74 | return space
75 |
76 | def updateLineNumberAreaWidth(self):
77 | self.setViewportMargins(self.lineNumberAreaWidth(), 0, 0, 0)
78 |
79 | def updateLineNumberArea(self, rect, dy):
80 |
81 | if dy:
82 | self.lineNumberArea.scroll(0, dy)
83 | else:
84 | self.lineNumberArea.update(0, rect.y(), self.lineNumberArea.width(), rect.height())
85 |
86 | if rect.contains(self.viewport().rect()):
87 | self.updateLineNumberAreaWidth()
88 |
89 | def resizeEvent(self, event):
90 | QtWidgets.QPlainTextEdit.resizeEvent(self, event)
91 |
92 | cr = self.contentsRect()
93 | self.lineNumberArea.setGeometry(QtCore.QRect(cr.left(), cr.top(), self.lineNumberAreaWidth(), cr.height()))
94 |
95 | # def toPlainText(self):
96 | # return utils.string(QtWidgets.QPlainTextEdit.toPlainText(self))
97 |
98 | def lineNumberAreaPaintEvent(self, event):
99 |
100 | if self.isReadOnly():
101 | return
102 |
103 | painter = QtGui.QPainter(self.lineNumberArea)
104 | painter.fillRect(event.rect(), self.lineNumberAreaColor) # Number bg
105 |
106 | block = self.firstVisibleBlock()
107 | block_number = block.blockNumber()
108 | top = int(self.blockBoundingGeometry(block).translated(self.contentOffset()).top())
109 | bottom = top + int(self.blockBoundingRect(block).height())
110 | current_line = self.document().findBlock(self.textCursor().position()).blockNumber()
111 |
112 | painter.setPen(self.palette().color(QtGui.QPalette.Text))
113 |
114 | painter_font = config.script_editor_font
115 | if self.knobScripter != "":
116 | painter_font.setPointSize(config.prefs["se_font_size"])
117 | painter.setFont(painter_font)
118 |
119 | while block.isValid() and top <= event.rect().bottom():
120 |
121 | text_color = self.lineNumberColor # Numbers
122 |
123 | if block_number == current_line and self.hasFocus():
124 | text_color = self.currentLineNumberColor # Number highlighted
125 |
126 | painter.setPen(text_color)
127 |
128 | number = "%s" % str(block_number + 1)
129 | painter.drawText(-3, top, self.lineNumberArea.width(), self.fontMetrics().height(), QtCore.Qt.AlignRight,
130 | number)
131 |
132 | # Move to the next block
133 | block = block.next()
134 | top = bottom
135 | bottom = top + int(self.blockBoundingRect(block).height())
136 | block_number += 1
137 |
138 | def keyPressEvent(self, event):
139 | """
140 | Custom actions for specific keystrokes
141 | """
142 | key = event.key()
143 | ctrl = bool(event.modifiers() & Qt.ControlModifier)
144 | # alt = bool(event.modifiers() & Qt.AltModifier)
145 | shift = bool(event.modifiers() & Qt.ShiftModifier)
146 | pre_scroll = self.verticalScrollBar().value()
147 | # modifiers = QtWidgets.QApplication.keyboardModifiers()
148 | # ctrl = (modifiers == Qt.ControlModifier)
149 | # shift = (modifiers == Qt.ShiftModifier)
150 |
151 | up_arrow = 16777235
152 | down_arrow = 16777237
153 |
154 | # if Tab convert to Space
155 | if key == 16777217:
156 | self.indentation('indent')
157 |
158 | # if Shift+Tab remove indent
159 | elif key == 16777218:
160 | self.indentation('unindent')
161 |
162 | # if BackSpace try to snap to previous indent level
163 | elif key == 16777219:
164 | if not self.unindentBackspace():
165 | QtWidgets.QPlainTextEdit.keyPressEvent(self, event)
166 | else:
167 | # COOL BEHAVIORS SIMILAR TO SUBLIME GO NEXT!
168 | cursor = self.textCursor()
169 | cpos = cursor.position()
170 | apos = cursor.anchor()
171 | text_before_cursor = self.toPlainText()[:min(cpos, apos)]
172 | text_after_cursor = self.toPlainText()[max(cpos, apos):]
173 | text_all = self.toPlainText()
174 | to_line_start = text_before_cursor[::-1].find("\n")
175 | if to_line_start == -1:
176 | linestart_pos = 0 # Position of the start of the line that includes the cursor selection start
177 | else:
178 | linestart_pos = len(text_before_cursor) - to_line_start
179 |
180 | to_line_end = text_after_cursor.find("\n")
181 | if to_line_end == -1:
182 | lineend_pos = len(text_all) # Position of the end of the line that includes the cursor selection end
183 | else:
184 | lineend_pos = max(cpos, apos) + to_line_end
185 |
186 | text_before_lines = text_all[:linestart_pos]
187 | text_after_lines = text_all[lineend_pos:]
188 | if len(text_after_lines) and text_after_lines.startswith("\n"):
189 | text_after_lines = text_after_lines[1:]
190 | text_lines = text_all[linestart_pos:lineend_pos]
191 |
192 | if cursor.hasSelection():
193 | selection = cursor.selection().toPlainText()
194 | else:
195 | selection = ""
196 | if key == Qt.Key_ParenLeft and (len(selection) > 0 or re.match(r"[\s)}\];]+", text_after_cursor) or not len(
197 | text_after_cursor)): # (
198 | cursor.insertText("(" + selection + ")")
199 | cursor.setPosition(apos + 1, QtGui.QTextCursor.MoveAnchor)
200 | cursor.setPosition(cpos + 1, QtGui.QTextCursor.KeepAnchor)
201 | self.setTextCursor(cursor)
202 | elif key == Qt.Key_ParenRight and text_after_cursor.startswith(")"): # )
203 | cursor.movePosition(QtGui.QTextCursor.NextCharacter)
204 | self.setTextCursor(cursor)
205 | elif key in [94, Qt.Key_BracketLeft] \
206 | and (len(selection) > 0 or re.match(r"[\s)}\];]+", text_after_cursor)
207 | or not len(text_after_cursor)): # [
208 | cursor.insertText("[" + selection + "]")
209 | cursor.setPosition(apos + 1, QtGui.QTextCursor.MoveAnchor)
210 | cursor.setPosition(cpos + 1, QtGui.QTextCursor.KeepAnchor)
211 | self.setTextCursor(cursor)
212 | elif key in [Qt.Key_BracketRight, 43, 93] and text_after_cursor.startswith("]"): # ]
213 | cursor.movePosition(QtGui.QTextCursor.NextCharacter)
214 | self.setTextCursor(cursor)
215 | elif key == Qt.Key_BraceLeft and (len(selection) > 0 or re.match(r"[\s)}\];]+", text_after_cursor)
216 | or not len(text_after_cursor)): # {
217 | cursor.insertText("{" + selection + "}")
218 | cursor.setPosition(apos + 1, QtGui.QTextCursor.MoveAnchor)
219 | cursor.setPosition(cpos + 1, QtGui.QTextCursor.KeepAnchor)
220 | self.setTextCursor(cursor)
221 | elif key in [199, Qt.Key_BraceRight] and text_after_cursor.startswith("}"): # }
222 | cursor.movePosition(QtGui.QTextCursor.NextCharacter)
223 | self.setTextCursor(cursor)
224 | elif key == 34: # "
225 | if len(selection) > 0:
226 | cursor.insertText('"' + selection + '"')
227 | cursor.setPosition(apos + 1, QtGui.QTextCursor.MoveAnchor)
228 | cursor.setPosition(cpos + 1, QtGui.QTextCursor.KeepAnchor)
229 | elif text_after_cursor.startswith('"') and '"' in text_before_cursor.split("\n")[-1]:
230 | cursor.movePosition(QtGui.QTextCursor.NextCharacter)
231 | elif not re.match(r"(?:[\s)\]]+|$)", text_after_cursor): # If chars after cursor, act normal
232 | QtWidgets.QPlainTextEdit.keyPressEvent(self, event)
233 | elif not re.search(r"[\s.({\[,]$",
234 | text_before_cursor) and text_before_cursor != "": # Chars before cursor: act normal
235 | QtWidgets.QPlainTextEdit.keyPressEvent(self, event)
236 | else:
237 | cursor.insertText('"' + selection + '"')
238 | cursor.setPosition(apos + 1, QtGui.QTextCursor.MoveAnchor)
239 | cursor.setPosition(cpos + 1, QtGui.QTextCursor.KeepAnchor)
240 | self.setTextCursor(cursor)
241 | elif key == 39: # '
242 | if len(selection) > 0:
243 | cursor.insertText("'" + selection + "'")
244 | cursor.setPosition(apos + 1, QtGui.QTextCursor.MoveAnchor)
245 | cursor.setPosition(cpos + 1, QtGui.QTextCursor.KeepAnchor)
246 | elif text_after_cursor.startswith("'") and "'" in text_before_cursor.split("\n")[-1]:
247 | cursor.movePosition(QtGui.QTextCursor.NextCharacter)
248 | elif not re.match(r"(?:[\s)\]]+|$)", text_after_cursor): # If chars after cursor, act normal
249 | QtWidgets.QPlainTextEdit.keyPressEvent(self, event)
250 | elif not re.search(r"[\s.({\[,]$",
251 | text_before_cursor) and text_before_cursor != "": # Chars before cursor: act normal
252 | QtWidgets.QPlainTextEdit.keyPressEvent(self, event)
253 | else:
254 | cursor.insertText("'" + selection + "'")
255 | cursor.setPosition(apos + 1, QtGui.QTextCursor.MoveAnchor)
256 | cursor.setPosition(cpos + 1, QtGui.QTextCursor.KeepAnchor)
257 | self.setTextCursor(cursor)
258 | elif key == 35 and len(selection): # # (yes, a hash)
259 | # If there's a selection, insert a hash at the start of each line.. how?
260 | if selection != "":
261 | selection_split = selection.split("\n")
262 | if all(i.startswith("#") for i in selection_split):
263 | selection_commented = "\n".join([s[1:] for s in selection_split]) # Uncommented
264 | else:
265 | selection_commented = "#" + "\n#".join(selection_split)
266 | cursor.insertText(selection_commented)
267 | if apos > cpos:
268 | cursor.setPosition(apos + len(selection_commented) - len(selection),
269 | QtGui.QTextCursor.MoveAnchor)
270 | cursor.setPosition(cpos, QtGui.QTextCursor.KeepAnchor)
271 | else:
272 | cursor.setPosition(apos, QtGui.QTextCursor.MoveAnchor)
273 | cursor.setPosition(cpos + len(selection_commented) - len(selection),
274 | QtGui.QTextCursor.KeepAnchor)
275 | self.setTextCursor(cursor)
276 |
277 | elif key == 68 and ctrl and shift: # Ctrl+Shift+D, to duplicate text or line/s
278 |
279 | if not len(selection):
280 | self.setPlainText(text_before_lines + text_lines + "\n" + text_lines + "\n" + text_after_lines)
281 | cursor.setPosition(apos + len(text_lines) + 1, QtGui.QTextCursor.MoveAnchor)
282 | cursor.setPosition(cpos + len(text_lines) + 1, QtGui.QTextCursor.KeepAnchor)
283 | self.setTextCursor(cursor)
284 | self.verticalScrollBar().setValue(pre_scroll)
285 | self.scrollToCursor()
286 | else:
287 | if text_before_cursor.endswith("\n") and not selection.startswith("\n"):
288 | cursor.insertText(selection + "\n" + selection)
289 | cursor.setPosition(apos + len(selection) + 1, QtGui.QTextCursor.MoveAnchor)
290 | cursor.setPosition(cpos + len(selection) + 1, QtGui.QTextCursor.KeepAnchor)
291 | else:
292 | cursor.insertText(selection + selection)
293 | cursor.setPosition(apos + len(selection), QtGui.QTextCursor.MoveAnchor)
294 | cursor.setPosition(cpos + len(selection), QtGui.QTextCursor.KeepAnchor)
295 | self.setTextCursor(cursor)
296 |
297 | elif key == up_arrow and ctrl and shift and len(
298 | text_before_lines): # Ctrl+Shift+Up, to move the selected line/s up
299 | prev_line_start_distance = text_before_lines[:-1][::-1].find("\n")
300 | if prev_line_start_distance == -1:
301 | prev_line_start_pos = 0 # Position of the start of the previous line
302 | else:
303 | prev_line_start_pos = len(text_before_lines) - 1 - prev_line_start_distance
304 | prev_line = text_before_lines[prev_line_start_pos:]
305 |
306 | text_before_prev_line = text_before_lines[:prev_line_start_pos]
307 |
308 | if prev_line.endswith("\n"):
309 | prev_line = prev_line[:-1]
310 |
311 | if len(text_after_lines):
312 | text_after_lines = "\n" + text_after_lines
313 |
314 | self.setPlainText(text_before_prev_line + text_lines + "\n" + prev_line + text_after_lines)
315 | cursor.setPosition(apos - len(prev_line) - 1, QtGui.QTextCursor.MoveAnchor)
316 | cursor.setPosition(cpos - len(prev_line) - 1, QtGui.QTextCursor.KeepAnchor)
317 | self.setTextCursor(cursor)
318 | self.verticalScrollBar().setValue(pre_scroll)
319 | self.scrollToCursor()
320 | return
321 |
322 | elif key == down_arrow and ctrl and shift: # Ctrl+Shift+Up, to move the selected line/s up
323 | if not len(text_after_lines):
324 | text_after_lines = ""
325 | next_line_end_distance = text_after_lines.find("\n")
326 | if next_line_end_distance == -1:
327 | next_line_end_pos = len(text_all)
328 | else:
329 | next_line_end_pos = next_line_end_distance
330 | next_line = text_after_lines[:next_line_end_pos]
331 | text_after_next_line = text_after_lines[next_line_end_pos:]
332 |
333 | self.setPlainText(text_before_lines + next_line + "\n" + text_lines + text_after_next_line)
334 | cursor.setPosition(apos + len(next_line) + 1, QtGui.QTextCursor.MoveAnchor)
335 | cursor.setPosition(cpos + len(next_line) + 1, QtGui.QTextCursor.KeepAnchor)
336 | self.setTextCursor(cursor)
337 | self.verticalScrollBar().setValue(pre_scroll)
338 | self.scrollToCursor()
339 | return
340 |
341 | elif key == up_arrow and not len(text_before_lines): # If up key and nothing happens, go to start
342 | if not shift:
343 | cursor.setPosition(0, QtGui.QTextCursor.MoveAnchor)
344 | self.setTextCursor(cursor)
345 | else:
346 | cursor.setPosition(0, QtGui.QTextCursor.KeepAnchor)
347 | self.setTextCursor(cursor)
348 |
349 | elif key == down_arrow and not len(text_after_lines): # If up key and nothing happens, go to start
350 | if not shift:
351 | cursor.setPosition(len(text_all), QtGui.QTextCursor.MoveAnchor)
352 | self.setTextCursor(cursor)
353 | else:
354 | cursor.setPosition(len(text_all), QtGui.QTextCursor.KeepAnchor)
355 | self.setTextCursor(cursor)
356 |
357 | # if enter or return, match indent level
358 | elif key in [16777220, 16777221]:
359 | self.indentNewLine()
360 |
361 | # If ctrl + +, increase font size
362 | elif ctrl and key == Qt.Key_Plus:
363 | font = self.font()
364 | font.setPointSize(-(-font.pointSize() // 0.9))
365 | self.setFont(font)
366 | # If ctrl + -, decrease font size
367 | elif ctrl and key == Qt.Key_Minus:
368 | font = self.font()
369 | font.setPointSize(font.pointSize() // 1.1)
370 | self.setFont(font)
371 |
372 | else:
373 | QtWidgets.QPlainTextEdit.keyPressEvent(self, event)
374 |
375 | self.scrollToCursor()
376 |
377 | def scrollToCursor(self):
378 | self.cursor = self.textCursor()
379 | self.cursor.movePosition(
380 | QtGui.QTextCursor.NoMove) # Does nothing, but makes the scroll go to the right place...
381 | self.setTextCursor(self.cursor)
382 |
383 | def getCursorInfo(self):
384 |
385 | self.cursor = self.textCursor()
386 |
387 | self.firstChar = self.cursor.selectionStart()
388 | self.lastChar = self.cursor.selectionEnd()
389 |
390 | self.noSelection = False
391 | if self.firstChar == self.lastChar:
392 | self.noSelection = True
393 |
394 | self.originalPosition = self.cursor.position()
395 | self.cursorBlockPos = self.cursor.positionInBlock()
396 |
397 | def unindentBackspace(self):
398 | """
399 | #snap to previous indent level
400 | """
401 | self.getCursorInfo()
402 |
403 | if not self.noSelection or self.cursorBlockPos == 0:
404 | return False
405 |
406 | # check text in front of cursor
407 | text_in_front = self.document().findBlock(self.firstChar).text()[:self.cursorBlockPos]
408 |
409 | # check whether solely spaces
410 | if text_in_front != ' ' * self.cursorBlockPos:
411 | return False
412 |
413 | # snap to previous indent level
414 | spaces = len(text_in_front)
415 |
416 | for space in range(int(spaces - int(float(spaces - 1) // self.tab_spaces) * self.tab_spaces - 1)):
417 | self.cursor.deletePreviousChar()
418 |
419 | def indentNewLine(self):
420 | # In case selection covers multiple line, make it one line first
421 | self.insertPlainText('')
422 | self.getCursorInfo()
423 |
424 | # Check how many spaces after cursor
425 | text = self.document().findBlock(self.firstChar).text()
426 | text_in_front = text[:self.cursorBlockPos]
427 |
428 | if len(text_in_front) == 0:
429 | self.insertPlainText('\n')
430 | return
431 |
432 | indent_level = 0
433 | for i in text_in_front:
434 | if i == ' ':
435 | indent_level += 1
436 | else:
437 | break
438 |
439 | indent_level //= self.tab_spaces
440 |
441 | # find out whether text_in_front's last character was a ':'
442 | # if that's the case add another indent.
443 | # ignore any spaces at the end, however also
444 | # make sure text_in_front is not just an indent
445 | if text_in_front.count(' ') != len(text_in_front):
446 | while text_in_front[-1] == ' ':
447 | text_in_front = text_in_front[:-1]
448 |
449 | if text_in_front[-1] == ':':
450 | indent_level += 1
451 |
452 | # new line
453 | self.insertPlainText('\n')
454 | # match indent
455 | self.insertPlainText(' ' * int(self.tab_spaces * indent_level))
456 |
457 | def indentation(self, mode):
458 |
459 | pre_scroll = self.verticalScrollBar().value()
460 | self.getCursorInfo()
461 |
462 | # if nothing is selected and mode is set to indent, simply insert as many
463 | # space as needed to reach the next indentation level.
464 | if self.noSelection and mode == 'indent':
465 | remaining_spaces = self.tab_spaces - (self.cursorBlockPos % self.tab_spaces)
466 | self.insertPlainText(' ' * remaining_spaces)
467 | return
468 |
469 | selected_blocks = self.findBlocks(self.firstChar, self.lastChar)
470 | before_blocks = self.findBlocks(last=self.firstChar - 1, exclude=selected_blocks)
471 | after_blocks = self.findBlocks(first=self.lastChar + 1, exclude=selected_blocks)
472 |
473 | before_blocks_text = self.blocks2list(before_blocks)
474 | selected_blocks_text = self.blocks2list(selected_blocks, mode)
475 | after_blocks_text = self.blocks2list(after_blocks)
476 |
477 | combined_text = '\n'.join(before_blocks_text + selected_blocks_text + after_blocks_text)
478 |
479 | # make sure the line count stays the same
480 | original_block_count = len(self.toPlainText().split('\n'))
481 | combined_text = '\n'.join(combined_text.split('\n')[:original_block_count])
482 |
483 | self.clear()
484 | self.setPlainText(combined_text)
485 |
486 | if self.noSelection:
487 | self.cursor.setPosition(self.lastChar)
488 |
489 | # check whether the the orignal selection was from top to bottom or vice versa
490 | else:
491 | if self.originalPosition == self.firstChar:
492 | first = self.lastChar
493 | last = self.firstChar
494 | first_block_snap = QtGui.QTextCursor.EndOfBlock
495 | last_block_snap = QtGui.QTextCursor.StartOfBlock
496 | else:
497 | first = self.firstChar
498 | last = self.lastChar
499 | first_block_snap = QtGui.QTextCursor.StartOfBlock
500 | last_block_snap = QtGui.QTextCursor.EndOfBlock
501 |
502 | self.cursor.setPosition(first)
503 | self.cursor.movePosition(first_block_snap, QtGui.QTextCursor.MoveAnchor)
504 | self.cursor.setPosition(last, QtGui.QTextCursor.KeepAnchor)
505 | self.cursor.movePosition(last_block_snap, QtGui.QTextCursor.KeepAnchor)
506 |
507 | self.setTextCursor(self.cursor)
508 | self.verticalScrollBar().setValue(pre_scroll)
509 |
510 | def findBlocks(self, first=0, last=None, exclude=None):
511 | exclude = exclude or []
512 | blocks = []
513 | if last is None:
514 | last = self.document().characterCount()
515 | for pos in range(first, last + 1):
516 | block = self.document().findBlock(pos)
517 | if block not in blocks and block not in exclude:
518 | blocks.append(block)
519 | return blocks
520 |
521 | def blocks2list(self, blocks, mode=None):
522 | text = []
523 | for block in blocks:
524 | block_text = block.text()
525 | if mode == 'unindent':
526 | if block_text.startswith(' ' * self.tab_spaces):
527 | block_text = block_text[self.tab_spaces:]
528 | self.lastChar -= self.tab_spaces
529 | elif block_text.startswith(' '):
530 | block_text = block_text[1:]
531 | self.lastChar -= 1
532 |
533 | elif mode == 'indent':
534 | block_text = ' ' * self.tab_spaces + block_text
535 | self.lastChar += self.tab_spaces
536 |
537 | text.append(block_text)
538 |
539 | return text
540 |
541 | def highlightCurrentLine(self):
542 | """
543 | Highlight currently selected line
544 | """
545 | extra_selections = []
546 |
547 | selection = QtWidgets.QTextEdit.ExtraSelection()
548 |
549 | selection.format.setBackground(self.lineColor)
550 | selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True)
551 | selection.cursor = self.textCursor()
552 | selection.cursor.clearSelection()
553 |
554 | extra_selections.append(selection)
555 |
556 | self.setExtraSelections(extra_selections)
557 | self.scrollToCursor()
558 |
559 | @staticmethod
560 | def format(rgb, style=''):
561 | """
562 | Return a QtWidgets.QTextCharFormat with the given attributes.
563 | """
564 | color = QtGui.QColor(*rgb)
565 | text_format = QtGui.QTextCharFormat()
566 | text_format.setForeground(color)
567 |
568 | if 'bold' in style:
569 | text_format.setFontWeight(QtGui.QFont.Bold)
570 | if 'italic' in style:
571 | text_format.setFontItalic(True)
572 | if 'underline' in style:
573 | text_format.setUnderlineStyle(QtGui.QTextCharFormat.SingleUnderline)
574 |
575 | return text_format
576 |
577 | def setColorStyle(self, style=None):
578 | """
579 | Change bg and text color configurations regarding the editor style. This doesn't change the syntax highlighter
580 | """
581 | styles = config.script_editor_styles
582 |
583 | if not style:
584 | style = config.prefs["se_style"]
585 |
586 | if style not in styles:
587 | return False
588 |
589 | self.setStyleSheet(styles[style]["stylesheet"])
590 | self.lineColor = QtGui.QColor(*styles[style]["selected_line_color"])
591 | self.lineNumberAreaColor = QtGui.QColor(*styles[style]["lineNumberAreaColor"])
592 | self.lineNumberColor = QtGui.QColor(*styles[style]["lineNumberColor"])
593 | self.currentLineNumberColor = QtGui.QColor(*styles[style]["currentLineNumberColor"])
594 | self.highlightCurrentLine()
595 | self.scrollToCursor()
596 | return True
597 |
598 | def set_code_language(self, lang="python"):
599 | """ Sets the appropriate highlighter and styles """
600 |
601 | if lang is None and self.highlighter:
602 | self.highlighter.setDocument(None)
603 | self.highlighter = None
604 | self.code_language = None
605 |
606 | if isinstance(lang, str):
607 | if lang != self.code_language:
608 | lang = lang.lower()
609 | if self.highlighter:
610 | self.highlighter.setDocument(None)
611 | self.highlighter = None
612 | if lang == "blink":
613 | self.highlighter = blinkhighlighter.KSBlinkHighlighter(self.document())
614 | self.highlighter.setStyle(config.prefs["code_style_blink"])
615 | self.setColorStyle("blink_default")
616 | elif lang == "python":
617 | self.highlighter = pythonhighlighter.KSPythonHighlighter(self.document())
618 | self.highlighter.setStyle(config.prefs["code_style_python"])
619 | self.setColorStyle("default")
620 | else:
621 | self.setColorStyle("default")
622 | self.code_language = None
623 | return
624 | self.code_language = lang
625 | else:
626 | logging.debug("Lang type not valid: " + str(type(lang)))
627 |
628 |
629 | class KSLineNumberArea(QtWidgets.QWidget):
630 | def __init__(self, script_editor):
631 | super(KSLineNumberArea, self).__init__(script_editor)
632 |
633 | self.scriptEditor = script_editor
634 | self.setStyleSheet("text-align: center;")
635 |
636 | def paintEvent(self, event):
637 | self.scriptEditor.lineNumberAreaPaintEvent(event)
638 | return
639 |
--------------------------------------------------------------------------------
/KnobScripter/ksscripteditormain.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """ KnobScripter's Main Script Editor: Version of KSScriptEditor with extended functionality.
3 |
4 | The KSScriptEditorMain is an extension of KSScriptEditor (QPlainTextEdit) which includes
5 | snippet functionality, auto-completions, suggestions and other features useful to have
6 | only in the main script editor, the one in the actual KnobScripter.
7 |
8 | adrianpueyo.com
9 |
10 | """
11 |
12 | import nuke
13 | import re
14 | import sys
15 |
16 | try:
17 | if nuke.NUKE_VERSION_MAJOR < 11:
18 | from PySide import QtCore, QtGui, QtGui as QtWidgets
19 | from PySide.QtCore import Qt
20 | else:
21 | from PySide2 import QtWidgets, QtGui, QtCore
22 | from PySide2.QtCore import Qt
23 | except ImportError:
24 | from Qt import QtCore, QtGui, QtWidgets
25 |
26 |
27 | from KnobScripter.ksscripteditor import KSScriptEditor
28 | from KnobScripter import keywordhotbox, content, dialogs
29 |
30 | def best_ending_match(text, match_list):
31 | '''
32 | If the text ends with a key in the match_list, it returns the key and value.
33 | match_list example: [["ban","banana"],["ap","apple"],["or","orange"]]
34 | If there are several matches, returns the longest one.
35 | Except if one starts with space, in which case return the other.
36 | False if no matches.
37 | '''
38 | ending_matches = []
39 |
40 | # 1. Find which items from match_list are found
41 | for item in match_list:
42 | if item[0].startswith(" "):
43 | match = re.search(item[0] + r"$", text)
44 | else:
45 | match = re.search(r"[\s.(){}\[\],;:=+-]" + item[0] + r"$", text)
46 | if match or text == item[0]:
47 | ending_matches.append(item)
48 | if not len(ending_matches):
49 | return False
50 |
51 | # 2. If multiple matches, decide which is the best one
52 | # Order by length
53 | ending_matches = sorted(ending_matches, key = lambda a: len(a[0]))
54 |
55 | return ending_matches[-1]
56 |
57 | def get_last_word(text):
58 | '''
59 | Return the last word (azAZ09_) appearing in the text or False.
60 | '''
61 | s = re.split(r"[\W]",text)
62 | if len(s):
63 | return s[-1]
64 | else:
65 | return False
66 |
67 |
68 | class KSScriptEditorMain(KSScriptEditor):
69 | '''
70 | Modified KSScriptEditor to include snippets, tab menu, etc.
71 | '''
72 |
73 | def __init__(self, knob_scripter, output=None, parent=None):
74 | super(KSScriptEditorMain, self).__init__(knob_scripter)
75 | self.knobScripter = knob_scripter
76 | self.script_output = output
77 | self.nukeCompleter = None
78 | self.currentNukeCompletion = None
79 |
80 | ########
81 | # FROM NUKE's SCRIPT EDITOR START
82 | ########
83 | self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
84 |
85 | # Setup Nuke Python completer
86 | self.nukeCompleter = QtWidgets.QCompleter(self)
87 | self.nukeCompleter.setWidget(self)
88 | self.nukeCompleter.setCompletionMode(QtWidgets.QCompleter.UnfilteredPopupCompletion)
89 | self.nukeCompleter.setCaseSensitivity(Qt.CaseSensitive)
90 | try:
91 | self.nukeCompleter.setModel(QtGui.QStringListModel())
92 | except:
93 | self.nukeCompleter.setModel(QtCore.QStringListModel())
94 |
95 | self.nukeCompleter.activated.connect(self.insertNukeCompletion)
96 | self.nukeCompleter.highlighted.connect(self.completerHighlightChanged)
97 | ########
98 | # FROM NUKE's SCRIPT EDITOR END
99 | ########
100 |
101 | def placeholderToEnd(self, text, placeholder):
102 | '''Returns distance (int) from the first ocurrence of the placeholder, to the end of the string with placeholders removed'''
103 | search = re.search(placeholder, text)
104 | if not search:
105 | return -1
106 | from_start = search.start()
107 | total = len(re.sub(placeholder, "", text))
108 | to_end = total - from_start
109 | return to_end
110 |
111 | def addSnippetText(self, snippet_text, last_word = None):
112 | ''' Adds the selected text as a snippet (taking care of $$, $name$ etc) to the script editor.
113 | If last_word arg supplied, it replaces $_$ for that word.
114 | '''
115 | cursor_placeholder_find = r"(? 1:
149 | cursor_len = positions[1] - positions[0] - 2
150 |
151 | text = re.sub(cursor_placeholder_find, "", text)
152 | self.cursor.insertText(text)
153 | if placeholder_to_end >= 0:
154 | for i in range(placeholder_to_end):
155 | self.cursor.movePosition(QtGui.QTextCursor.PreviousCharacter)
156 | for i in range(cursor_len):
157 | self.cursor.movePosition(QtGui.QTextCursor.NextCharacter, QtGui.QTextCursor.KeepAnchor)
158 | self.setTextCursor(self.cursor)
159 |
160 | def mouseDoubleClickEvent(self, event):
161 | ''' On doublelick on a word, suggestions might show up. i.e. eRead/eWrite, etc. '''
162 | KSScriptEditor.mouseDoubleClickEvent(self, event)
163 | selected_text = self.textCursor().selection().toPlainText()
164 |
165 | # 1. Doubleclick on blink!
166 | if self.knobScripter.code_language == "blink":
167 | # 1.1. Define all blink keywords
168 | blink_keyword_dict = content.blink_keyword_dict
169 | # 1.2. If there's a match, show the hotbox!
170 | category = self.findCategory(selected_text, blink_keyword_dict) # Returns something like "Access Method"
171 | if category:
172 | keyword_hotbox = keywordhotbox.KeywordHotbox(self, category, blink_keyword_dict[category])
173 | if keyword_hotbox.exec_() == QtWidgets.QDialog.Accepted:
174 | self.textCursor().insertText(keyword_hotbox.selection)
175 |
176 | def keyPressEvent(self, event):
177 |
178 | ctrl = bool(event.modifiers() & Qt.ControlModifier)
179 | alt = bool(event.modifiers() & Qt.AltModifier)
180 | shift = bool(event.modifiers() & Qt.ShiftModifier)
181 | key = event.key()
182 |
183 | # ADAPTED FROM NUKE's SCRIPT EDITOR:
184 | # Get completer state
185 | self.nukeCompleterShowing = self.nukeCompleter.popup().isVisible()
186 |
187 | # BEFORE ANYTHING ELSE, IF SPECIAL MODIFIERS SIMPLY IGNORE THE REST
188 | if not self.nukeCompleterShowing and (ctrl or shift or alt):
189 | # Bypassed!
190 | if key not in [Qt.Key_Return, Qt.Key_Enter, Qt.Key_Tab]:
191 | KSScriptEditor.keyPressEvent(self, event)
192 | return
193 |
194 | # If the python completer is showing
195 | if self.nukeCompleterShowing:
196 | tc = self.textCursor()
197 | # If we're hitting enter, do completion
198 | if key in [Qt.Key_Return, Qt.Key_Enter, Qt.Key_Tab]:
199 | if not self.currentNukeCompletion:
200 | self.nukeCompleter.setCurrentRow(0)
201 | self.currentNukeCompletion = self.nukeCompleter.currentCompletion()
202 | # print str(self.nukeCompleter.completionModel[0])
203 | self.insertNukeCompletion(self.currentNukeCompletion)
204 | self.nukeCompleter.popup().hide()
205 | self.nukeCompleterShowing = False
206 | # If you're hitting right or escape, hide the popup
207 | elif key == Qt.Key_Right or key == Qt.Key_Escape:
208 | self.nukeCompleter.popup().hide()
209 | self.nukeCompleterShowing = False
210 | # If you hit tab, escape or ctrl-space, hide the completer
211 | elif key == Qt.Key_Tab or key == Qt.Key_Escape or (ctrl and key == Qt.Key_Space):
212 | self.currentNukeCompletion = ""
213 | self.nukeCompleter.popup().hide()
214 | self.nukeCompleterShowing = False
215 | # If none of the above, update the completion model
216 | else:
217 | QtWidgets.QPlainTextEdit.keyPressEvent(self, event)
218 | # Edit completion model
219 | colNum = tc.columnNumber()
220 | posNum = tc.position()
221 | inputText = self.toPlainText()
222 | inputTextSplit = inputText.splitlines()
223 | runningLength = 0
224 | currentLine = None
225 | for line in inputTextSplit:
226 | length = len(line)
227 | runningLength += length
228 | if runningLength >= posNum:
229 | currentLine = line
230 | break
231 | runningLength += 1
232 | if currentLine:
233 | completionPart = currentLine.split(" ")[-1]
234 | if "(" in completionPart:
235 | completionPart = completionPart.split("(")[-1]
236 | self.completeNukePartUnderCursor(completionPart)
237 | return
238 |
239 | if type(event) == QtGui.QKeyEvent:
240 | if key == Qt.Key_Escape: # Close the knobscripter...
241 | if not type( self.parent().parent() ) == nuke.KnobScripterPane:
242 | self.knobScripter.close()
243 | elif not ctrl and not alt and not shift and event.key() == Qt.Key_Tab: # If only tab
244 | self.placeholder = "$$"
245 | # 1. Set the cursor
246 | self.cursor = self.textCursor()
247 |
248 | # 2. Save text before and after
249 | cpos = self.cursor.position()
250 | text_before_cursor = self.toPlainText()[:cpos]
251 | line_before_cursor = text_before_cursor.split('\n')[-1]
252 | # Remove tabs too, so it doesn't count as active space
253 | while line_before_cursor.startswith(" "*max(1,self.tab_spaces)):
254 | line_before_cursor = line_before_cursor[self.tab_spaces:]
255 | text_after_cursor = self.toPlainText()[cpos:]
256 |
257 | # Abort mission if there's a tab or nothing before
258 | if any([text_before_cursor.endswith(_) for _ in ["\t","\n"]]) or not len(line_before_cursor.strip()):
259 | KSScriptEditor.keyPressEvent(self, event)
260 | return
261 |
262 | # If cursor has selection, abort mission if it spawns multiple lines or includes start of line + spaces
263 | # Otherwise, open floating panel to input a text to encapsulate with () the selection! New v3.0
264 | if self.cursor.hasSelection():
265 | cursor_text = self.cursor.selectedText()
266 | if u"\u2029" not in cursor_text and len(line_before_cursor): # Newline "\n" is auto converted to u"\u2029"
267 | panel = dialogs.TextInputDialog(self.knobScripter, name="Wrap with", text="", title="Wrap selection.")
268 | if panel.exec_():
269 | # Accepted
270 | cpos = self.cursor.position()
271 | apos = self.cursor.anchor()
272 | cpos = min(cpos,apos)
273 | new_text = "{0}({1})".format(panel.text,cursor_text)
274 | self.cursor.insertText(new_text)
275 | self.cursor.setPosition(cpos , QtGui.QTextCursor.MoveAnchor)
276 | self.cursor.setPosition(cpos + len(new_text), QtGui.QTextCursor.KeepAnchor)
277 | self.setTextCursor(self.cursor)
278 | return
279 |
280 |
281 | # 3. Check coincidences in snippets dicts
282 | try: # Meaning snippet found
283 | snippets_lang = []
284 | snippets_all = []
285 | if self.knobScripter.code_language in content.all_snippets:
286 | snippets_lang = content.all_snippets[self.knobScripter.code_language]
287 | if "all" in content.all_snippets:
288 | snippets_all = content.all_snippets["all"]
289 | snippets_list = snippets_lang + snippets_all
290 | match_key, match_snippet = best_ending_match(line_before_cursor, snippets_list)
291 | for i in range(len(match_key)):
292 | self.cursor.deletePreviousChar()
293 | new_line_before_cursor = text_before_cursor[:-len(match_key)].split('\n')[-1]
294 |
295 | # Next we'll be able to check what's the last word before the cursor
296 | word_before_cursor = None
297 | if new_line_before_cursor.endswith("."):
298 | word_before_cursor = get_last_word(new_line_before_cursor[:-1].strip())
299 | self.addSnippetText(match_snippet,last_word = word_before_cursor) # Add the appropriate snippet and move the cursor
300 | except: # Meaning snippet not found...
301 | # 3.1. Go with nuke/python completer
302 | if self.knobScripter.code_language in ["python","blink"]:
303 | # ADAPTED FROM NUKE's SCRIPT EDITOR:
304 | tc = self.textCursor()
305 | allCode = self.toPlainText()
306 | colNum = tc.columnNumber()
307 | posNum = tc.position()
308 |
309 | # ...and if there's text in the editor
310 | if len(allCode.split()) > 0:
311 | # There is text in the editor
312 | currentLine = tc.block().text()
313 |
314 | # If you're not at the end of the line just add a tab (maybe not???)
315 | if colNum < len(currentLine):
316 | # If there's text right after the cursor, don't autocomplete
317 | #if currentLine[colNum] not in [',', '<', ' ' ,')','.','[']:
318 | if re.match(r'[\w]',currentLine[colNum:]):
319 | KSScriptEditor.keyPressEvent(self, event)
320 | return
321 | # Else show the completer
322 | else:
323 | completionPart = currentLine[:colNum].split(" ")[-1]
324 | if "(" in completionPart:
325 | completionPart = completionPart.split("(")[-1]
326 |
327 | self.completeNukePartUnderCursor(completionPart)
328 |
329 | return
330 |
331 | # If you are at the end of the line,
332 | else:
333 | # If there's nothing to the right of you add a tab
334 | if currentLine[colNum - 1:] == "" or currentLine.endswith(" "):
335 | KSScriptEditor.keyPressEvent(self, event)
336 | return
337 | # Else update completionPart and show the completer
338 | completionPart = currentLine.split(" ")[-1]
339 | if "(" in completionPart:
340 | completionPart = completionPart.split("(")[-1]
341 |
342 | self.completeNukePartUnderCursor(completionPart)
343 | return
344 |
345 | KSScriptEditor.keyPressEvent(self, event)
346 | else:
347 | KSScriptEditor.keyPressEvent(self, event)
348 | elif event.key() in [Qt.Key_Enter, Qt.Key_Return]:
349 | modifiers = QtWidgets.QApplication.keyboardModifiers()
350 | if modifiers == QtCore.Qt.ControlModifier:
351 | # Ctrl + Enter! Python or blink?
352 | if self.knobScripter.code_language == "python":
353 | self.runScript()
354 | else:
355 | self.knobScripter.blinkSaveRecompile()
356 | else:
357 | KSScriptEditor.keyPressEvent(self, event)
358 | else:
359 | KSScriptEditor.keyPressEvent(self, event)
360 |
361 | def getPyObjects(self, text):
362 | ''' Returns a list containing all the functions, classes and variables found within the selected python text (code) '''
363 | matches = []
364 | # 1: Remove text inside triple quotes (leaving the quotes)
365 | text_clean = '""'.join(text.split('"""')[::2])
366 | text_clean = '""'.join(text_clean.split("'''")[::2])
367 |
368 | # 2: Remove text inside of quotes (leaving the quotes) except if \"
369 | lines = text_clean.split("\n")
370 | text_clean = ""
371 | for line in lines:
372 | line_clean = '""'.join(line.split('"')[::2])
373 | line_clean = '""'.join(line_clean.split("'")[::2])
374 | line_clean = line_clean.split("#")[0]
375 | text_clean += line_clean + "\n"
376 |
377 | # 3. Split into segments (lines plus ";")
378 | segments = re.findall(r"[^\n;]+", text_clean)
379 |
380 | # 4. Go case by case.
381 | for s in segments:
382 | # Declared vars
383 | matches += re.findall(r"([\w.]+)(?=[,\s\w]*=[^=]+$)", s)
384 | # Def functions and arguments
385 | function = re.findall(r"[\s]*def[\s]+([\w.]+)[\s]*\([\s]*", s)
386 | if len(function):
387 | matches += function
388 | args = re.split(r"[\s]*def[\s]+([\w.]+)[\s]*\([\s]*", s)
389 | if len(args) > 1:
390 | args = args[-1]
391 | matches += re.findall(r"(? Returns category (str)
402 | Looks for keyword in keyword_dict and returns the relevant category name or None
403 | '''
404 | for category in keyword_dict:
405 | if keyword in keyword_dict[category]["keywords"]:
406 | return category
407 | return None
408 |
409 | # Nuke script editor's modules completer
410 | def completionsForcompletionPart(self, completionPart):
411 | if self.knobScripter.code_language == "python":
412 | return self.pythonCompletions(completionPart)
413 | elif self.knobScripter.code_language == "blink":
414 | return self.blinkCompletions(completionPart)
415 |
416 | def pythonCompletions(self,completionPart):
417 | def findModules(searchString):
418 | sysModules = sys.modules
419 | globalModules = globals()
420 | allModules = dict(sysModules, **globalModules)
421 | allKeys = list(set(list(globals().keys()) + list(sys.modules.keys())))
422 | allKeysSorted = [x for x in sorted(set(allKeys))]
423 |
424 | if searchString == '':
425 | matching = []
426 | for x in allModules:
427 | if x.startswith(searchString):
428 | matching.append(x)
429 | return matching
430 | else:
431 | try:
432 | if sys.modules.has_key(searchString):
433 | return dir(sys.modules['%s' % searchString])
434 | elif globals().has_key(searchString):
435 | return dir(globals()['%s' % searchString])
436 | else:
437 | return []
438 | except:
439 | return None
440 |
441 | completerText = completionPart
442 |
443 | # Get text before last dot
444 | moduleSearchString = '.'.join(completerText.split('.')[:-1])
445 |
446 | # Get text after last dot
447 | fragmentSearchString = completerText.split('.')[-1] if completerText.split('.')[
448 | -1] != moduleSearchString else ''
449 |
450 | # Get all the modules that match module search string
451 | allModules = findModules(moduleSearchString)
452 |
453 | # If no modules found, do a dir
454 | if not allModules:
455 | if len(moduleSearchString.split('.')) == 1:
456 | matchedModules = []
457 | else:
458 | try:
459 | trimmedModuleSearchString = '.'.join(moduleSearchString.split('.')[:-1])
460 | matchedModules = [x for x in dir(
461 | getattr(sys.modules[trimmedModuleSearchString], moduleSearchString.split('.')[-1])) if
462 | '__' not in x and x.startswith(fragmentSearchString)]
463 | except:
464 | matchedModules = []
465 | else:
466 | matchedModules = [x for x in allModules if '__' not in x and x.startswith(fragmentSearchString)]
467 |
468 | selfObjects = list(set(self.getPyObjects(self.toPlainText())))
469 | for i in selfObjects:
470 | if i.startswith(completionPart):
471 | matchedModules.append(i)
472 |
473 | return matchedModules
474 |
475 | def blinkCompletions(self, completionPart):
476 | blink_keywords = content.blink_keywords
477 | matchedModules = []
478 | for i in blink_keywords:
479 | if i.startswith(completionPart):
480 | matchedModules.append(i)
481 | return matchedModules
482 |
483 | def completeNukePartUnderCursor(self, completionPart):
484 |
485 | completionPart = completionPart.lstrip().rstrip()
486 | completionList = self.completionsForcompletionPart(completionPart)
487 | if len(completionList) == 0:
488 | return
489 | self.nukeCompleter.model().setStringList(completionList)
490 | self.nukeCompleter.setCompletionPrefix(completionPart)
491 |
492 | if self.nukeCompleter.popup().isVisible():
493 | rect = self.cursorRect()
494 | rect.setWidth(self.nukeCompleter.popup().sizeHintForColumn(
495 | 0) + self.nukeCompleter.popup().verticalScrollBar().sizeHint().width())
496 | self.nukeCompleter.complete(rect)
497 | return
498 |
499 | # Make it visible
500 | if len(completionList) == 1:
501 | self.insertNukeCompletion(completionList[0])
502 | else:
503 | rect = self.cursorRect()
504 | rect.setWidth(self.nukeCompleter.popup().sizeHintForColumn(
505 | 0) + self.nukeCompleter.popup().verticalScrollBar().sizeHint().width())
506 | self.nukeCompleter.complete(rect)
507 |
508 | return
509 |
510 | def insertNukeCompletion(self, completion):
511 | """ Insert the appropriate text into the script editor. """
512 | if completion:
513 | # If python, insert text... If blink, insert as snippet?
514 | completionPart = self.nukeCompleter.completionPrefix()
515 | if len(completionPart.split('.')) == 0:
516 | completionPartFragment = completionPart
517 | else:
518 | completionPartFragment = completionPart.split('.')[-1]
519 |
520 | textToInsert = completion[len(completionPartFragment):]
521 | tc = self.textCursor()
522 | if self.code_language == "python":
523 | tc.insertText(textToInsert)
524 | elif self.code_language == "blink":
525 | self.addSnippetText(textToInsert)
526 | return
527 |
528 | def completerHighlightChanged(self, highlighted):
529 | self.currentNukeCompletion = highlighted
530 |
531 | def runScript(self):
532 | cursor = self.textCursor()
533 | if cursor.hasSelection():
534 | code = cursor.selection().toPlainText()
535 | else:
536 | code = self.toPlainText()
537 |
538 | if code == "":
539 | return
540 |
541 | if nuke.NUKE_VERSION_MAJOR >= 13 and self.knobScripter.nodeMode and self.knobScripter.runInContext:
542 | # The simple and nice approach for run in context!! Doesn't work with Nuke 12...
543 | run_context = "root"
544 | # If node mode and run in context (experimental) selected in preferences, run the code in its proper context!
545 | # if self.knobScripter.nodeMode and self.knobScripter.runInContext:
546 | nodeName = self.knobScripter.node.fullName()
547 | knobName = self.knobScripter.current_knob_dropdown.itemData(
548 | self.knobScripter.current_knob_dropdown.currentIndex())
549 | if nuke.exists(nodeName) and knobName in nuke.toNode(nodeName).knobs():
550 | run_context = "{}.{}".format(nodeName, knobName)
551 | code = 'exec("""{}""")'.format(code.replace('\\', '\\\\'))
552 | # Run the code! Much cleaner in this way:
553 | nuke.runIn(run_context, code)
554 |
555 | else:
556 | nukeSEInput = self.knobScripter.nukeSEInput
557 | # If node mode and run in context (experimental) selected in preferences, run the code in its proper context!
558 | if self.knobScripter.nodeMode and self.knobScripter.runInContext:
559 | # 1. change thisNode, thisKnob...
560 | nodeName = self.knobScripter.node.fullName()
561 | knobName = self.knobScripter.current_knob_dropdown.itemData(
562 | self.knobScripter.current_knob_dropdown.currentIndex())
563 | if nuke.exists(nodeName) and knobName in nuke.toNode(nodeName).knobs():
564 | code = code.replace("nuke.thisNode()", "nuke.toNode('{}')".format(nodeName))
565 | code = code.replace("nuke.thisKnob()", "nuke.toNode('{}').knob('{}')".format(nodeName, knobName))
566 | # 2. If group, wrap all with: with nuke.toNode(fullNameOfGroup) and then indent every single line!
567 | # at least by one space. replace "\n" with "\n "
568 | if self.knobScripter.node.Class() in ["Group", "LiveGroup", "Root"]:
569 | code = code.replace("\n", "\n ")
570 | code = "with nuke.toNode('{}'):\n {}".format(nodeName, code)
571 |
572 | # Store original ScriptEditor status
573 | nukeSECursor = nukeSEInput.textCursor()
574 | origSelection = nukeSECursor.selectedText()
575 | oldAnchor = nukeSECursor.anchor()
576 | oldPosition = nukeSECursor.position()
577 |
578 | # Add the code to be executed and select it
579 | nukeSEInput.insertPlainText(code)
580 |
581 | if oldAnchor < oldPosition:
582 | newAnchor = oldAnchor
583 | newPosition = nukeSECursor.position()
584 | else:
585 | newAnchor = nukeSECursor.position()
586 | newPosition = oldPosition
587 |
588 | nukeSECursor.setPosition(newAnchor, QtGui.QTextCursor.MoveAnchor)
589 | nukeSECursor.setPosition(newPosition, QtGui.QTextCursor.KeepAnchor)
590 | nukeSEInput.setTextCursor(nukeSECursor)
591 |
592 | # Run the code!
593 | self.knobScripter.nukeSERunBtn.click()
594 |
595 | # Revert ScriptEditor to original
596 | nukeSEInput.insertPlainText(origSelection)
597 | nukeSECursor.setPosition(oldAnchor, QtGui.QTextCursor.MoveAnchor)
598 | nukeSECursor.setPosition(oldPosition, QtGui.QTextCursor.KeepAnchor)
599 | nukeSEInput.setTextCursor(nukeSECursor)
600 |
601 |
--------------------------------------------------------------------------------