├── .gitignore
├── LICENSE.md
├── README.md
├── audio
└── audio_analysis_channel.tox
├── color_palette.tox
├── command_panel.md
├── command_panel.py
├── command_panel.tox
├── common.py
├── component-editor.toe
├── components.tox
├── console_logger.tox
├── control
├── ControlMappingExt.py
├── codev2.tox
├── control_binder.tox
├── control_mapper.tox
├── control_target.md
├── control_target.tox
├── device_control_display.tox
├── device_display.tox
└── mftwister.tox
├── corner_rect.tox
├── file_logger.tox
├── hexagonal_grid.tox
├── lib
├── _stubs
│ ├── ArcBallExt.py
│ ├── CallbacksExt.py
│ ├── ListerExt.py
│ ├── PopDialogExt.py
│ ├── PopMenuExt.py
│ ├── TDCallbacksExt.py
│ ├── TDCodeGen.py
│ ├── TDFunctions.py
│ ├── TDJSON.py
│ ├── TDStoreTools.py
│ ├── TreeListerExt.py
│ ├── Updater.py
│ └── __init__.py
├── logs.py
└── tdcomponents_resolve.py
├── loggly_logger.tox
├── mapper.tox
├── midimap.py
├── mixer
├── MixerSourcesExt.py
├── mixer_sources.tox
├── source_track.tox
├── track_mixer.tox
├── videoSenderCore.tox
└── video_sender.tox
├── multi_logger.tox
├── rack.toe
├── rack
├── component_editor
│ ├── ComponentEditorExt.py
│ └── component_editor.tox
├── component_picker
│ ├── ComponentPickerExt.py
│ └── component_picker.tox
├── editor
│ ├── EditorExt.py
│ ├── components
│ │ ├── EditorCommon.py
│ │ ├── EditorToolsExt.py
│ │ ├── EditorViewsExt.py
│ │ ├── SettingsExt.py
│ │ ├── WorkspaceExt.py
│ │ ├── editorTools.tox
│ │ ├── editorViews.tox
│ │ ├── preview_panel.tox
│ │ ├── settings.tox
│ │ ├── topMenuCallbacks.py
│ │ ├── topMenuDefine.txt
│ │ └── workspace.tox
│ └── editor.tox
├── rack
│ ├── RackExt.py
│ └── rack.tox
└── rack_tools
│ └── rack_tools.tox
├── recorder.py
├── recorder.tox
├── recorder
├── recorderCore.tox
└── recorderCoreExt.py
├── sop_merger.tox
├── td-components-tester.toe
├── tools
├── ParamHelperExt.py
├── PathSetupExt.py
├── UIColorEditorExt.py
├── base_save.tox
├── paramAdjuster.tox
├── paramAdjusterExt.py
├── param_helper.tox
├── path_setup.tox
├── saveEXT.py
└── ui_color_editor.tox
├── ui
├── slider_value_overlay.tox
├── statusOverlay-readme.txt
├── statusOverlay-thumb.jpg
├── statusOverlay.tox
├── statusOverlayExt.py
├── ui_overrides.txt
└── widget_ui_overrides.tox
└── utils
├── DataLock.py
├── ParamFilter.py
├── channel_remap.tox
├── chop_lock.tox
├── dat_lock.tox
├── lfo_generator.tox
├── modulated_value.tox
├── param_animator.tox
├── param_filter.tox
├── settings
├── SettingsExt.py
└── settings.tox
├── speed_generator.tox
├── taskManager.tox
└── taskManagerExt.py
/.gitignore:
--------------------------------------------------------------------------------
1 | CrashAutoSave*
2 | *.dmp
3 | .idea
4 | *.pyc
5 | *-log.txt
6 | .DS_Store
7 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | CC0 1.0 Universal
3 |
4 | Statement of Purpose
5 |
6 | The laws of most jurisdictions throughout the world automatically confer
7 | exclusive Copyright and Related Rights (defined below) upon the creator and
8 | subsequent owner(s) (each and all, an "owner") of an original work of
9 | authorship and/or a database (each, a "Work").
10 |
11 | Certain owners wish to permanently relinquish those rights to a Work for the
12 | purpose of contributing to a commons of creative, cultural and scientific
13 | works ("Commons") that the public can reliably and without fear of later
14 | claims of infringement build upon, modify, incorporate in other works, reuse
15 | and redistribute as freely as possible in any form whatsoever and for any
16 | purposes, including without limitation commercial purposes. These owners may
17 | contribute to the Commons to promote the ideal of a free culture and the
18 | further production of creative, cultural and scientific works, or to gain
19 | reputation or greater distribution for their Work in part through the use and
20 | efforts of others.
21 |
22 | For these and/or other purposes and motivations, and without any expectation
23 | of additional consideration or compensation, the person associating CC0 with a
24 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
25 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
26 | and publicly distribute the Work under its terms, with knowledge of his or her
27 | Copyright and Related Rights in the Work and the meaning and intended legal
28 | effect of CC0 on those rights.
29 |
30 | 1. Copyright and Related Rights. A Work made available under CC0 may be
31 | protected by copyright and related or neighboring rights ("Copyright and
32 | Related Rights"). Copyright and Related Rights include, but are not limited
33 | to, the following:
34 |
35 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
36 | and translate a Work;
37 |
38 | ii. moral rights retained by the original author(s) and/or performer(s);
39 |
40 | iii. publicity and privacy rights pertaining to a person's image or likeness
41 | depicted in a Work;
42 |
43 | iv. rights protecting against unfair competition in regards to a Work,
44 | subject to the limitations in paragraph 4(a), below;
45 |
46 | v. rights protecting the extraction, dissemination, use and reuse of data in
47 | a Work;
48 |
49 | vi. database rights (such as those arising under Directive 96/9/EC of the
50 | European Parliament and of the Council of 11 March 1996 on the legal
51 | protection of databases, and under any national implementation thereof,
52 | including any amended or successor version of such directive); and
53 |
54 | vii. other similar, equivalent or corresponding rights throughout the world
55 | based on applicable law or treaty, and any national implementations thereof.
56 |
57 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
58 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
59 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
60 | and Related Rights and associated claims and causes of action, whether now
61 | known or unknown (including existing as well as future claims and causes of
62 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
63 | duration provided by applicable law or treaty (including future time
64 | extensions), (iii) in any current or future medium and for any number of
65 | copies, and (iv) for any purpose whatsoever, including without limitation
66 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
67 | the Waiver for the benefit of each member of the public at large and to the
68 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
69 | shall not be subject to revocation, rescission, cancellation, termination, or
70 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
71 | by the public as contemplated by Affirmer's express Statement of Purpose.
72 |
73 | 3. Public License Fallback. Should any part of the Waiver for any reason be
74 | judged legally invalid or ineffective under applicable law, then the Waiver
75 | shall be preserved to the maximum extent permitted taking into account
76 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
77 | is so judged Affirmer hereby grants to each affected person a royalty-free,
78 | non transferable, non sublicensable, non exclusive, irrevocable and
79 | unconditional license to exercise Affirmer's Copyright and Related Rights in
80 | the Work (i) in all territories worldwide, (ii) for the maximum duration
81 | provided by applicable law or treaty (including future time extensions), (iii)
82 | in any current or future medium and for any number of copies, and (iv) for any
83 | purpose whatsoever, including without limitation commercial, advertising or
84 | promotional purposes (the "License"). The License shall be deemed effective as
85 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
86 | License for any reason be judged legally invalid or ineffective under
87 | applicable law, such partial invalidity or ineffectiveness shall not
88 | invalidate the remainder of the License, and in such case Affirmer hereby
89 | affirms that he or she will not (i) exercise any of his or her remaining
90 | Copyright and Related Rights in the Work or (ii) assert any associated claims
91 | and causes of action with respect to the Work, in either case contrary to
92 | Affirmer's express Statement of Purpose.
93 |
94 | 4. Limitations and Disclaimers.
95 |
96 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
97 | surrendered, licensed or otherwise affected by this document.
98 |
99 | b. Affirmer offers the Work as-is and makes no representations or warranties
100 | of any kind concerning the Work, express, implied, statutory or otherwise,
101 | including without limitation warranties of title, merchantability, fitness
102 | for a particular purpose, non infringement, or the absence of latent or
103 | other defects, accuracy, or the present or absence of errors, whether or not
104 | discoverable, all to the greatest extent permissible under applicable law.
105 |
106 | c. Affirmer disclaims responsibility for clearing rights of other persons
107 | that may apply to the Work or any use thereof, including without limitation
108 | any person's Copyright and Related Rights in the Work. Further, Affirmer
109 | disclaims responsibility for obtaining any necessary consents, permissions
110 | or other rights required for any use of the Work.
111 |
112 | d. Affirmer understands and acknowledges that Creative Commons is not a
113 | party to this document and has no duty or obligation with respect to this
114 | CC0 or use of the Work.
115 |
116 | For more information, please see
117 |
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | td-components
2 |
--------------------------------------------------------------------------------
/audio/audio_analysis_channel.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/audio/audio_analysis_channel.tox
--------------------------------------------------------------------------------
/color_palette.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/color_palette.tox
--------------------------------------------------------------------------------
/command_panel.md:
--------------------------------------------------------------------------------
1 | # Command Panel
2 |
3 | The command panel is a configurable toolbar for invoking scripts.
4 |
5 | The original use case was for editor shortcuts, like opening panel viewers or output windows, aligning selected OPs,
6 | and so on. But it was designed to be reusable for a variety of purposes.
7 |
8 | The panel loads a list of commands that each have an executable action, and a label and/or icon TOP.
9 | It generates a button for each command with the relevant text/icon and runs the associated script when it is clicked.
10 |
11 | ## Specifying commands
12 |
13 | There are several ways to specify commands for the panel.
14 |
15 | 1. Providing a table DAT via the "Command Table" parameter.
16 | 2. Specifying a list of `dict`s using the "Command Object List" parameter.
17 | 3. Enabling predefined lists of common commands, via the "Include ___ Commands" parameters.
18 |
19 | ### Using a command table
20 |
21 | Each row after the first row is converted to a `dict`, with the columns as keys. See the next section for how those are
22 | handled.
23 |
24 | ### Using `dict`s
25 |
26 | Each `dict` in a list represents a single command. The following settings are available for all (or many) types of
27 | commands:
28 |
29 | * `label` (required) - text to put in the button
30 | * `img` - optional TOP path for an image to show in the button
31 | * `isIcon` - if true, and there's no `img`, `label` is interpreted as an icon character in the Material Design Icons.
32 | * `help` - help text for the button
33 | * `type` - specifies what type of pre-defined action to use (see below)
34 |
35 | #### Command types
36 |
37 | * *(none)* - the `action` setting is interpreted as a reference to an executable Python function
38 | * `open` or `view` - opens an operator viewer or window for the specified `op`
39 | * `unique` - should the viewer be opened as a unique viewer for the op, defaults to true (not available for windows)
40 | * `borders` - should the viewer have window borders, defaults to true
41 | * `toggle` - equivalent to `open`/`view` but if the viewer is already open, it closes it
42 | * `pars` - open a parameter editor for the `op`
43 | * `edit` or `navigate` - opens the `op` in the network editor, reusing the main editor if possible
44 | * `run` - execute the `op` (must be a text DAT)
45 | * `script` - execute a string of python code, from the `script` setting. Note that if you omit the `type` entirely but
46 | have a `script` field, the type is assumed to be `script`
47 | * `reload` - reload the `op` (which can be multiple ops) from their associated files (such as text/table DATs)
48 | * `save` - saves either the selected COMPs or the COMP that the network editor is in as a tox
49 |
50 |
51 | ### Using script text DATs
52 |
53 | The `Cmdscripts` paramter can be used to specify a list of text DATs that each contain a script to run as a command.
54 | If the script has a `def action(...)` in it, the script is treated as a module, and it can specify settings by declaring
55 | them as variables. Otherwise the script is treated as a single block of executable code (using `theScriptDAT.run()`).
56 |
57 | DAT with just a raw chunk of code:
58 | ```
59 | op('/whatever/foo').par.Stuff = 'things'
60 | print('hello!')
61 | ```
62 |
63 | DAT with a function and settings:
64 | ```
65 | label = 'some command'
66 | help = 'do stuff with a script'
67 | def action():
68 | print('hello!')
69 | ```
70 |
71 |
72 | ## Command `Context`
73 |
74 | Command functions can optionally accept an argument that's generated by the panel, which provides access to shortcuts
75 | and helper functions based on the editor context (what OPs are selected in the network editor, etc).
76 |
--------------------------------------------------------------------------------
/command_panel.py:
--------------------------------------------------------------------------------
1 | from inspect import signature
2 | import os
3 |
4 | if False:
5 | from _stubs import *
6 |
7 | class Context:
8 | def __init__(self, ownerComp):
9 | self.ownerComp = ownerComp
10 |
11 | def resolveOP(self, o):
12 | if isinstance(o, str):
13 | return self.ownerComp.op(o)
14 | return o
15 |
16 | def openUI(self, o, unique=True, borders=True):
17 | o = self.resolveOP(o)
18 | if not o:
19 | return
20 | if o.type == 'window':
21 | o.par.winopen.pulse()
22 | else:
23 | o.openViewer(unique=unique, borders=borders)
24 |
25 | def closeUI(self, o, topMost=False):
26 | o = self.resolveOP(o)
27 | if not o:
28 | return
29 | if o.type == 'window':
30 | o.par.winclose.pulse()
31 | else:
32 | o.closeViewer(topMost=topMost)
33 |
34 | def openOrToggleUI(self, o, borders=True):
35 | o = self.resolveOP(o)
36 | if not o:
37 | return
38 | if o.type == 'window':
39 | self.toggleUI(o, borders=borders)
40 | else:
41 | self.openUI(o, unique=True, borders=borders)
42 |
43 | def toggleUI(self, o, borders=True):
44 | o = self.resolveOP(o)
45 | if not o:
46 | return
47 | if o.type == 'window':
48 | if o.isOpen:
49 | self.openUI(o, unique=True, borders=borders)
50 | else:
51 | self.closeUI(o, topMost=False)
52 | else:
53 | raise NotImplementedError('toggleUI only supported for Window COMP')
54 |
55 | @property
56 | def activeEditor(self):
57 | pane = ui.panes.current
58 | if pane.type == PaneType.NETWORKEDITOR:
59 | return pane
60 | for pane in ui.panes:
61 | if pane.type == PaneType.NETWORKEDITOR:
62 | return pane
63 |
64 | def getSelectedOps(self, predicate=None):
65 | pane = self.activeEditor
66 | if not pane:
67 | return []
68 | sel = pane.owner.selectedChildren or [pane.owner.currentChild]
69 | if predicate is not None:
70 | sel = list(filter(predicate, sel))
71 | return sel
72 |
73 | def getSelectedOrContext(self, predicate):
74 | sel = self.getSelectedOps(predicate)
75 | if sel:
76 | return sel[0]
77 | pane = self.activeEditor
78 | if not pane:
79 | return
80 | o = pane.owner
81 | while o:
82 | if predicate(o):
83 | return o
84 | o = o.parent()
85 |
86 | def navigateTo(self, o):
87 | o = self.resolveOP(o)
88 | if not o:
89 | return
90 | pane = self.activeEditor
91 | if pane:
92 | pane.owner = o
93 |
94 | def openNetwork(self, o):
95 | o = self.resolveOP(o)
96 | if not o or not o.isCOMP:
97 | return
98 | pane = ui.panes.createFloating(type=PaneType.NETWORKEDITOR)
99 | pane.owner = o
100 |
101 | @staticmethod
102 | def resolveFile(path):
103 | return path if os.path.exists(path) else ''
104 |
105 | def saveOP(self, o, path=None):
106 | o = self.resolveOP(o)
107 | if not o:
108 | return False
109 | if path is None:
110 | if o.isDAT and getattr(o.par, 'file') and hasattr(o.par, 'writepulse'):
111 | path = o.par.file
112 | o.par.writepulse.pulse()
113 | ui.status = 'saved {} to {}'.format(o.path, path)
114 | return True
115 | if o.isCOMP:
116 | path = o.par.externaltox.eval()
117 | elif hasattr(o.par, 'file'):
118 | path = o.par.file.eval()
119 | if not path:
120 | return False
121 | o.save(path)
122 | ui.status = 'saved {} to {}'.format(o.path, path)
123 | return True
124 |
125 | def reloadOPFile(self, o):
126 | o = self.resolveOP(o)
127 | if not o:
128 | return False
129 | if o.isDAT and hasattr(o.par, 'loadonstartpulse'):
130 | o.par.loadonstartpulse.pulse()
131 | return True
132 | if o.isCOMP:
133 | o.par.reinitnet.pulse()
134 | return True
135 | return False
136 |
137 | def _callWithOptionalParam(fn, param):
138 | if len(signature(fn).parameters) == 0:
139 | return fn()
140 | else:
141 | return fn(param)
142 |
143 | class Command:
144 | def __init__(self, action, **attrs):
145 | self.action = action
146 | self.attrs = attrs or {}
147 |
148 | @property
149 | def label(self): return self.attrs.get('label') or '--'
150 |
151 | @property
152 | def help(self): return self.attrs.get('help') or ''
153 |
154 | @property
155 | def img(self):
156 | i = self.attrs.get('img')
157 | return op(i) if isinstance(i, str) else i
158 |
159 | @property
160 | def isIcon(self):
161 | return self.attrs.get('isIcon')
162 |
163 | def invoke(self, context):
164 | _callWithOptionalParam(self.action, context)
165 |
166 | @classmethod
167 | def forOpenUI(cls, o, unique=True, borders=True, **kwargs):
168 | return cls(lambda context: context.openUI(o, unique=unique, borders=borders), **kwargs)
169 |
170 | @classmethod
171 | def forToggleUI(cls, o, borders=True, **kwargs):
172 | return cls(lambda context: context.openOrToggleUI(o, borders=borders), **kwargs)
173 |
174 | @classmethod
175 | def forEdit(cls, o, **kwargs):
176 | return cls(lambda context: context.navigateTo(o), **kwargs)
177 |
178 | @classmethod
179 | def forParams(cls, oppath, **kwargs):
180 | def _action(context):
181 | o = context.resolveOP(oppath)
182 | if o:
183 | o.openParameters()
184 | return cls(_action, **kwargs)
185 |
186 | @classmethod
187 | def forReload(cls, oppaths, **kwargs):
188 | def _action(context):
189 | targetops = ops(oppaths)
190 | for o in targetops:
191 | context.reloadOPFile(o)
192 | return cls(_action, **kwargs)
193 |
194 | @classmethod
195 | def forSave(cls, oppaths, path, **kwargs):
196 | def _action(context):
197 | targetops = ops(oppaths)
198 | for o in targetops:
199 | context.saveOP(o, path)
200 | return cls(_action, **kwargs)
201 |
202 | @classmethod
203 | def forExec(
204 | cls, oppath, args=None, endFrame=False, fromOP=None,
205 | group=None, delayFrames=0, delayMilliSeconds=0, **kwargs):
206 | def _action(context):
207 | o = context.resolveOP(oppath)
208 | if o and o.isDAT:
209 | o.run(
210 | *args,
211 | endFrame=endFrame, fromOP=fromOP, group=group,
212 | delayFrames=delayFrames, delayMilliSeconds=delayMilliSeconds,
213 | **kwargs)
214 | return cls(_action, **kwargs)
215 |
216 | @classmethod
217 | def forScriptDAT(cls, dat):
218 | if 'def action' not in dat.text:
219 | return cls.forExec(dat.path, label=dat.name)
220 | else:
221 | locs = dat.locals
222 | return cls(
223 | lambda context: _callWithOptionalParam(dat.module.action, context),
224 | label=locs.get('label', None) or dat.name,
225 | **{
226 | k: locs[k]
227 | for k in ['img', 'help', 'isIcon']
228 | if k in locs
229 | })
230 |
231 | @classmethod
232 | def forAction(cls, action, **kwargs):
233 | return cls(action, **kwargs)
234 |
235 | @classmethod
236 | def fromRow(cls, dat, row):
237 | obj = {
238 | dat[0, i].val: dat[row, i].val
239 | for i in range(dat.numCols)
240 | }
241 | return Command.fromObj(obj)
242 |
243 | @classmethod
244 | def fromObj(cls, obj):
245 | if _asbool(obj.get('hidden'), False):
246 | return None
247 | typename = obj.get('type')
248 | attrs = {
249 | 'label': obj.get('label'),
250 | 'help': obj.get('help'),
251 | 'img': obj.get('img'),
252 | 'isIcon': obj.get('isIcon'),
253 | }
254 | o = obj.get('op')
255 | if not typename and 'script' in obj:
256 | typename = 'script'
257 | if not typename:
258 | action = obj.get('action', _noop)
259 | return Command.forAction(action, **attrs)
260 | if typename in ['open', 'view']:
261 | return Command.forOpenUI(
262 | o,
263 | unique=_asbool(obj.get('unique'), True),
264 | borders=_asbool(obj.get('borders'), True),
265 | **attrs)
266 | elif typename in ['toggle']:
267 | return Command.forToggleUI(
268 | o,
269 | borders=_asbool(obj.get('borders'), False),
270 | **attrs)
271 | elif typename in ['pars']:
272 | return Command.forParams(o, **attrs)
273 | elif typename in ['edit', 'navigate']:
274 | return Command.forEdit(o, **attrs)
275 | elif typename in ['run']:
276 | return Command.forExec(
277 | o,
278 | delayFrames=_asint(obj.get('delayFrames'), 0),
279 | **attrs)
280 | elif typename == 'script':
281 | code = obj.get('script')
282 | if not code:
283 | return Command.forAction(_noop, **attrs)
284 | if code.startswith('lambda'):
285 | return Command.forAction(eval(code), **attrs)
286 | else:
287 | return Command.forAction(lambda _: eval(code), **attrs)
288 | elif typename in ['reload']:
289 | return Command.forReload(o, **attrs)
290 | elif typename in ['save']:
291 | return Command.forSave(
292 | o,
293 | path=obj.get('path') or obj.get('file'),
294 | **attrs)
295 |
296 | def _noop(*args, **kwargs):
297 | pass
298 |
299 | def _asbool(val, defval):
300 | if val is None or val == '':
301 | return defval
302 | if val == '1':
303 | return True
304 | if val == '0':
305 | return False
306 | return bool(val)
307 |
308 | def _asint(val, defval):
309 | if val is None or val == '':
310 | return defval
311 | return int(val)
312 |
313 | def _strornull(val):
314 | return str(val) if val is not None else None
315 |
316 | class CommandPanel:
317 | def __init__(self, comp):
318 | self.ownerComp = comp
319 | self.commandlist = []
320 |
321 | def _AddCommand(self, command):
322 | if command:
323 | self.commandlist.append(command)
324 |
325 | def _AddCommands(self, commands):
326 | if commands:
327 | for command in commands:
328 | self._AddCommand(command)
329 |
330 | def _AddCommandsFromTable(self, dat):
331 | if not dat or dat.numRows < 2:
332 | return
333 | for row in range(1, dat.numRows):
334 | self._AddCommand(Command.fromRow(dat, row))
335 |
336 | def _AddCommandsFromScriptDATs(self):
337 | scripts = self.ownerComp.par.Cmdscripts.evalOPs()
338 | for script in scripts:
339 | self._AddCommand(Command.forScriptDAT(script))
340 |
341 | def _AddCommandsFromList(self):
342 | cmdobjs = self.ownerComp.par.Cmdobjs.eval()
343 | if cmdobjs:
344 | if isinstance(cmdobjs, (list, tuple)):
345 | for cmdobj in cmdobjs:
346 | self._AddCommand(Command.fromObj(cmdobj))
347 | else:
348 | self._AddCommand(Command.fromObj(cmdobjs))
349 |
350 | def RebuildCommands(self):
351 | self.commandlist.clear()
352 | self._AddCommandsFromTable(self.ownerComp.op('./command_table_in'))
353 | if self.ownerComp.par.Includebasictoolcmds:
354 | self._AddCommands(_basicToolCommands)
355 | if self.ownerComp.par.Includetestcmds:
356 | self._AddCommandsFromTable(self.ownerComp.op('./TEST_commands'))
357 | self._AddCommandsFromList()
358 | self._AddCommandsFromScriptDATs()
359 |
360 | def BuildCommandTable(self, dat):
361 | self.RebuildCommands()
362 | dat.clear()
363 | dat.appendRow(['label', 'help', 'img', 'isIcon'])
364 | for command in self.commandlist:
365 | dat.appendRow([
366 | command.label,
367 | command.help,
368 | command.img or '',
369 | bool(command.isIcon),
370 | ])
371 |
372 | def _GetCommandByIndex(self, index):
373 | if index < 0 or index >= len(self.commandlist):
374 | return None
375 | return self.commandlist[index]
376 |
377 | def ExecuteCommandByIndex(self, index):
378 | command = self._GetCommandByIndex(index)
379 | if not command:
380 | return
381 | context = Context(self.ownerComp)
382 | command.invoke(context)
383 |
384 | def InitializeButton(self, button, index):
385 | command = self._GetCommandByIndex(index) # type: Command
386 | button.par.display = True
387 | button.par.Buttonofflabel = command.label
388 | button.par.Buttononlabel = command.label
389 | button.par.Offtoonscript0 = 'iop.commands.ExecuteCommandByIndex({})'.format(index)
390 | img = command.img
391 | imgpath = img.path if img is not None else ''
392 | button.par.Buttonofftop = imgpath
393 | button.par.Buttonontop = imgpath
394 | button.par.Popuphelp = command.help
395 | if command.isIcon and not img:
396 | button.par.Buttonfont = 'Material_Design_Icons'
397 |
398 | def _copyPaths(context: Context):
399 | sel = context.getSelectedOps()
400 | ui.clipboard = ' '.join([o.path for o in sel])
401 |
402 | def _saveTox(context: Context):
403 | comp = context.getSelectedOrContext(lambda o: o.isCOMP and o.par.externaltox)
404 | if comp:
405 | context.saveOP(comp)
406 |
407 | def _incrementComponentVersion(context: Context):
408 | comp = context.getSelectedOrContext(lambda o: o.isCOMP and hasattr(o.par, 'Compversion'))
409 | if not comp:
410 | comp = context.getSelectedOrContext(lambda o: o.isCOMP)
411 | if not comp:
412 | ui.status = 'Unable to increment component version, no suitable COMP'
413 | return
414 | ui.status = 'Updating component version of {!r}'.format(comp)
415 | if not hasattr(comp.par, 'Compversion'):
416 | page = comp.appendCustomPage(':meta')
417 | par = page.appendInt('Compversion', label=':Version')[0]
418 | par.val = 0
419 | par.default = 0
420 | par.readOnly = True
421 | else:
422 | par = comp.par.Compversion
423 | par.val += 1
424 | par.default = par.val
425 | if par.normMax < par.val:
426 | par.normMax = par.val
427 | par.readOnly = True
428 |
429 | def _makeLastPage(comp, page):
430 | if len(comp.customPages) == 1 and comp.customPages[0] == page:
431 | return
432 | orderedpages = [p.name for p in comp.customPages if p != page] + [page.name]
433 | comp.sortCustomPages(*orderedpages)
434 |
435 | def _addOrUpdatePar(appendmethod, name, label, value=None, expr=None, readonly=None, setdefault=False):
436 | p = appendmethod(name, label=label)[0]
437 | if expr is not None:
438 | p.expr = expr
439 | if setdefault:
440 | p.defaultExpr = expr
441 | elif value is not None:
442 | p.val = value
443 | if setdefault:
444 | p.default = value
445 | if readonly is not None:
446 | p.readOnly = readonly
447 | return p
448 |
449 | def _addOrUpdateMetadataPar(appendmethod, name, label, value):
450 | _addOrUpdatePar(appendmethod, name, label, value, readonly=True, setdefault=True)
451 |
452 | def _setComponentMetadata(
453 | context: Context,
454 | description=None,
455 | version=None,
456 | typeid=None,
457 | website=None,
458 | author=None,
459 | page=':meta'):
460 | comp = context.getSelectedOrContext(lambda o: o.isCOMP)
461 | if not comp:
462 | ui.status = 'Unable to create metadata, no target comp'
463 | return
464 | page = comp.appendCustomPage(page)
465 | if page.name.startswith(':'):
466 | _makeLastPage(comp, page)
467 | if typeid:
468 | _addOrUpdateMetadataPar(page.appendStr, 'Comptypeid', ':Type ID', typeid)
469 | _addOrUpdateMetadataPar(page.appendStr, 'Compdescription', ':Description', description)
470 | _addOrUpdateMetadataPar(page.appendInt, 'Compversion', ':Version', version)
471 | _addOrUpdateMetadataPar(page.appendStr, 'Compwebsite', ':Website', website)
472 | _addOrUpdateMetadataPar(page.appendStr, 'Compauthor', ':Author', author)
473 | page.sort('Comptypeid', 'Compdescription', 'Compversion', 'Compwebsite', 'Compauthor')
474 |
475 | def _createTektTdCompsMetadata(context: Context):
476 | _setComponentMetadata(
477 | context,
478 | website='https://github.com/optexture/td-components',
479 | author='tekt@optexture.com')
480 |
481 | def _destroyCustomPars(context: Context):
482 | for targetOp in context.getSelectedOps(lambda o: hasattr(o, 'destroyCustomPars')):
483 | targetOp.destroyCustomPars()
484 |
485 | _basicToolCommands = [
486 | Command.forAction(
487 | _copyPaths,
488 | label='copy path',
489 | help='copy paths of selected ops'),
490 | Command.forAction(
491 | _saveTox,
492 | label='save',
493 | help='save selected or active component tox file'),
494 | Command.forAction(
495 | _incrementComponentVersion,
496 | label='version++',
497 | help='increment the component version attribute on selected or active',
498 | ),
499 | Command.forAction(
500 | _destroyCustomPars,
501 | label='kill pars',
502 | help='destroy all custom parameters on the selected OP(s)'
503 | ),
504 | Command.forAction(
505 | _createTektTdCompsMetadata,
506 | label='[t] meta',
507 | help='add tekt TD-components metadata to current component',
508 | ),
509 | ]
510 |
--------------------------------------------------------------------------------
/command_panel.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/command_panel.tox
--------------------------------------------------------------------------------
/component-editor.toe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/component-editor.toe
--------------------------------------------------------------------------------
/components.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/components.tox
--------------------------------------------------------------------------------
/console_logger.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/console_logger.tox
--------------------------------------------------------------------------------
/control/ControlMappingExt.py:
--------------------------------------------------------------------------------
1 | from typing import List, Union
2 |
3 | # noinspection PyUnreachableCode
4 | if False:
5 | # noinspection PyUnresolvedReferences
6 | from _stubs import *
7 |
8 | class ControlTarget:
9 | def __init__(self, ownerComp: 'COMP'):
10 | self.ownerComp = ownerComp
11 |
12 | def HandleActionPulse(self, action):
13 | if action == 'Initmaptable':
14 | self._InitTable(createMissing=True, clearExisting=False)
15 | elif action == 'Clearmaptable':
16 | self._InitTable(createMissing=False, clearExisting=True)
17 | elif action == 'Editmaptable':
18 | self._InitTable(createMissing=True, clearExisting=False)
19 | dat = self.ownerComp.par.Maptable.eval()
20 | if dat:
21 | dat.openViewer()
22 |
23 | def _InitTable(self, createMissing=False, clearExisting=False):
24 | par = self.ownerComp.par.Maptable
25 | dat = par.eval() # type: DAT
26 | if not dat and createMissing:
27 | dat = self.ownerComp.parent().create(tableDAT, 'mappings')
28 | dat.nodeX = self.ownerComp.nodeX + self.ownerComp.nodeWidth + 50
29 | dat.nodeY = self.ownerComp.nodeY
30 | par.val = dat
31 | if not dat:
32 | return
33 | if clearExisting:
34 | dat.clear()
35 | if dat.numRows == 1 and dat.numCols == 1 and dat[0, 0] == '':
36 | dat.clear()
37 | mappingColumns = [
38 | 'path',
39 | 'param',
40 | 'enable',
41 | 'low',
42 | 'high',
43 | 'control',
44 | 'group',
45 | 'handling',
46 | ]
47 | if not dat.numRows:
48 | dat.appendRow(mappingColumns)
49 | else:
50 | for col in mappingColumns:
51 | if not dat.col(col):
52 | dat.appendCol([col])
53 |
54 | def AddMappingForParam(self, par: 'Par', control: str = None, enable: bool = False):
55 | self._InitTable(createMissing=True, clearExisting=False)
56 | table = self.ownerComp.par.Maptable.eval() # type: DAT
57 | root = self.ownerComp.par.Targetroot.eval()
58 | if par.owner == root:
59 | path = ''
60 | elif root:
61 | path = root.relativePath(par.owner)
62 | else:
63 | path = par.owner.path
64 | i = table.numRows
65 | table.appendRow([])
66 | table[i, 'path'] = path
67 | table[i, 'param'] = par.name
68 | table[i, 'enable'] = int(enable)
69 | table[i, 'control'] = control or ''
70 |
71 | def BuildDeviceControls(outDat: 'DAT', definition: 'COMP'):
72 | outDat.clear()
73 | outDat.appendRow([
74 | 'control',
75 | 'cc',
76 | 'chan',
77 | ])
78 | if not definition:
79 | return
80 | for ctrlTable in definition.ops('sliders', 'buttons'):
81 | if not ctrlTable or not ctrlTable.valid or ctrlTable.numCols < 2:
82 | continue
83 | for ctrlRow in range(ctrlTable.numRows):
84 | name = ctrlTable[ctrlRow, 0]
85 | if not name:
86 | continue
87 | pattern = ctrlTable[ctrlRow, 1].val
88 | if not pattern:
89 | continue
90 | parts = pattern.split(' ')
91 | if len(parts) != 3:
92 | continue
93 | try:
94 | cc = int(parts[1], 16)
95 | except ValueError:
96 | cc = None
97 | if cc is None:
98 | continue
99 | outDat.appendRow([
100 | name,
101 | cc,
102 | 'ch1c{}'.format(cc),
103 | ])
104 |
105 | class ControlMapper:
106 | def __init__(self, ownerComp: 'COMP'):
107 | self.ownerComp = ownerComp
108 | self.statePar = ownerComp.op('iparMapperState').par
109 |
110 | @staticmethod
111 | def BuildDeviceControls(outDat: 'DAT', definition: 'COMP'):
112 | BuildDeviceControls(outDat, definition)
113 |
114 | @staticmethod
115 | def BuildMapTable(outDat: 'DAT', targets: List[str], deviceControls: 'DAT'):
116 | outDat.clear()
117 | outDat.appendRow([
118 | 'param',
119 | 'path',
120 | 'parName',
121 | 'enable',
122 | 'low',
123 | 'high',
124 | 'targetName',
125 | 'group',
126 | 'parType',
127 | 'control',
128 | 'cc',
129 | 'chan',
130 | 'handling',
131 | ])
132 | for target in ops(*targets):
133 | mappings = target.op('mappings')
134 | if not mappings or mappings.numRows < 2:
135 | continue
136 | _AddToMapTable(
137 | outDat,
138 | mappings,
139 | targetRoot=op(getattr(target.par, 'Targetroot', None)),
140 | targetName=str(getattr(target.par, 'Targetname', '')),
141 | groups=str(getattr(target.par, 'Group', '')),
142 | deviceControls=deviceControls,
143 | )
144 |
145 | @staticmethod
146 | def BuildOpPars(outDat: 'DAT', mapDat: 'DAT'):
147 | outDat.clear()
148 | opPars = {}
149 | for i in range(1, mapDat.numRows):
150 | path = mapDat[i, 'path'].val
151 | param = mapDat[i, 'parName'].val
152 | if not path or not param:
153 | continue
154 | if path in opPars:
155 | opPars[path].append(param)
156 | else:
157 | opPars[path] = [param]
158 | for path, params in opPars.items():
159 | outDat.appendRow([path] + params)
160 |
161 | def HandleDrop(self, dropName, dropExt, baseName, destPath):
162 | print(f'{self.ownerComp}.HandleDrop(dropName: {dropName}, dropExt: {dropExt}, baseName: {baseName}, destPath: {destPath}')
163 | if dropExt != 'parameter':
164 | return
165 | o = op(baseName)
166 | print(f'DROP O {o!r}')
167 | if not o:
168 | return
169 | par = getattr(o.par, dropName, None)
170 | print(f'DROP PAR {par!r}')
171 | if par is None:
172 | return
173 | target = self.statePar.Selectedtarget.eval() # type: ControlTarget
174 | if not target:
175 | return
176 | target.AddMappingForParam(par)
177 |
178 |
179 |
180 | def _AddToMapTable(
181 | outDat: 'DAT',
182 | inDat: 'DAT',
183 | targetRoot: Union[str, 'OP'],
184 | targetName: str,
185 | groups: str,
186 | deviceControls: 'DAT'):
187 | relOp = op(targetRoot)
188 | if relOp and not targetName:
189 | targetName = relOp.path
190 | if groups:
191 | groups = ' ' + groups
192 | if relOp and not targetName:
193 | targetName = relOp.path
194 | for inRow in range(1, inDat.numRows):
195 | enabled = inDat[inRow, 'enable'] in ('True', '1', '')
196 | if not enabled:
197 | continue
198 | control = inDat[inRow, 'control']
199 | if control and deviceControls.row(control):
200 | cc = deviceControls[control, 'cc']
201 | chan = deviceControls[control, 'chan']
202 | else:
203 | cc = ''
204 | chan = ''
205 | relPath = inDat[inRow, 'path']
206 | if not relPath:
207 | o = relOp
208 | else:
209 | o = relOp.op(relPath) if relOp else op(relOp)
210 | if not o:
211 | continue
212 | par = getattr(o.par, str(inDat[inRow, 'param']), None) if o else None
213 | if par is None:
214 | isPulse = False
215 | low = ''
216 | high = ''
217 | parName = ''
218 | else:
219 | if not (par.isNumber or par.isToggle or par.isMenu or par.isPulse):
220 | continue
221 | parName = par.name
222 | isPulse = par.isPulse or par.isMomentary
223 | low = inDat[inRow, 'low'].val
224 | high = inDat[inRow, 'high'].val
225 | if isPulse:
226 | low = 0
227 | high = 1
228 | else:
229 | if low == '':
230 | if par.isMenu:
231 | low = 0
232 | else:
233 | low = par.normMin
234 | if high == '':
235 | if par.isMenu:
236 | high = len(par.menuNames) - 1
237 | else:
238 | high = par.normMax
239 | row = outDat.numRows
240 | outDat.appendRow([])
241 | outDat[row, 'param'] = o.path + ':' + parName
242 | outDat[row, 'path'] = o.path
243 | outDat[row, 'parName'] = parName
244 | outDat[row, 'enable'] = int(enabled)
245 | outDat[row, 'low'] = low
246 | outDat[row, 'high'] = high
247 | outDat[row, 'targetName'] = targetName
248 | outDat[row, 'group'] = (inDat[inRow, 'group'].val + groups).strip()
249 | outDat[row, 'parType'] = 'pulse' if isPulse else 'value'
250 | outDat[row, 'control'] = control
251 | outDat[row, 'cc'] = cc
252 | outDat[row, 'chan'] = chan
253 | outDat[row, 'handling'] = inDat[inRow, 'handling'] or 'script'
254 |
255 | class DeviceDisplay:
256 | def __init__(self, ownerComp):
257 | self.ownerComp = ownerComp
258 |
259 | @staticmethod
260 | def BuildDeviceControls(outDat: 'DAT', definition: 'COMP'):
261 | BuildDeviceControls(outDat, definition)
262 |
263 | @staticmethod
264 | def BuildLayout(outDat: 'DAT', layout: 'DAT', mappings: 'DAT', controls: 'DAT'):
265 | outDat.clear()
266 | outDat.appendRow([
267 | 'label',
268 | 'page', 'row', 'col',
269 | 'slider', 'button',
270 | 'sliderChan', 'buttonChan',
271 | 'mappingLabel', 'mappingHelp',
272 | ])
273 | if not layout:
274 | return
275 | for i in range(1, layout.numRows):
276 | slider = _cellOrDefault(layout, i, 'slider', '')
277 | button = _cellOrDefault(layout, i, 'button', '')
278 | controlLabelParts = []
279 | mapLabel = ''
280 | mapHelp = ''
281 | if slider:
282 | controlLabelParts.append(slider)
283 | sliderParam = mappings[slider, 'param'] if mappings else None
284 | if sliderParam:
285 | mapLabel = _getParamLabel(sliderParam)
286 | mapHelp = sliderParam.val
287 | if button:
288 | controlLabelParts.append(button)
289 | buttonParam = mappings[button, 'param'] if mappings else None
290 | if buttonParam:
291 | if mapLabel:
292 | mapLabel = 's: {}'.format(mapLabel)
293 | mapLabel += '\\nb: ' + _getParamLabel(buttonParam)
294 | if mapHelp:
295 | mapHelp = 's: {}: '.format(mapHelp)
296 | mapHelp += '\\nb: ' + buttonParam.val
297 |
298 | outDat.appendRow([
299 | ' | '.join(controlLabelParts),
300 | _cellOrDefault(layout, i, 'page', 0),
301 | _cellOrDefault(layout, i, 'row', 0),
302 | _cellOrDefault(layout, i, 'col', 0),
303 | slider,
304 | button,
305 | _cellOrDefault(controls, _cellOrDefault(layout, i, 'slider', None), 'chan', ''),
306 | _cellOrDefault(controls, _cellOrDefault(layout, i, 'button', None), 'chan', ''),
307 | mapLabel,
308 | mapHelp,
309 | ])
310 |
311 | @staticmethod
312 | def BuildControlMappings(outDat: 'DAT', controls: 'DAT', mappings: 'DAT'):
313 | outDat.clear()
314 | outDat.appendRow(['control', 'param', 'paramLabel'])
315 | for control in controls.col('control')[1:]:
316 | if not mappings or not control or not mappings.row(control):
317 | param = None
318 | else:
319 | param = mappings[control, 'param']
320 | outDat.appendRow([
321 | control,
322 | param or '',
323 | '\\n'.join(param.val.split(':')) if param else '',
324 | ])
325 |
326 | @staticmethod
327 | def BuildPages(outDat: 'DAT', definition: 'COMP', layout: 'DAT'):
328 | outDat.clear()
329 | outDat.appendRow(['page', 'label', 'triggerChan', 'triggerCc', 'triggerChanName'])
330 | pages = definition.op('pages') if definition else None
331 | pageNames = set()
332 | if pages:
333 | for i in range(1, pages.numRows):
334 | name = pages[i, 'page']
335 | if not name:
336 | continue
337 | pageNames.add(name.val)
338 | chan = _cellOrDefault(pages, i, 'triggerChan', '')
339 | cc = _cellOrDefault(pages, i, 'triggerCc', '')
340 | outDat.appendRow([
341 | name,
342 | _cellOrDefault(pages, i, 'label', name),
343 | chan,
344 | cc,
345 | 'ch{}c{}'.format(chan, cc),
346 | ])
347 | for i in range(1, layout.numRows):
348 | page = layout[i, 'page'].val
349 | if page not in pageNames:
350 | pageNames.add(page)
351 | outDat.appendRow([page, page, '', ''])
352 |
353 | def _getParamLabel(param):
354 | if not param:
355 | return ''
356 | param = str(param)
357 | if ':' not in param:
358 | return param
359 | path, parName = param.rsplit(':', 1)
360 | o = op(path)
361 | par = getattr(o.par, parName, None) if o else None
362 | if par is None:
363 | return parName
364 | return par.label
365 |
366 | def _cellOrDefault(dat: 'DAT', r, c, default):
367 | if None in (dat, r, c):
368 | return default
369 | cell = dat[r, c]
370 | return cell.val if cell not in (None, '') else default
371 |
--------------------------------------------------------------------------------
/control/codev2.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/codev2.tox
--------------------------------------------------------------------------------
/control/control_binder.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/control_binder.tox
--------------------------------------------------------------------------------
/control/control_mapper.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/control_mapper.tox
--------------------------------------------------------------------------------
/control/control_target.md:
--------------------------------------------------------------------------------
1 | `control_target` is a collection of mappings for a
2 | target component or group of components.
3 |
4 | When there is a single target component, that would
5 | be used for the `Target Root` parameter.
6 | When there are multiple target components, that would
7 | be a parent that contains all of those components, or
8 | the root (which is a parent of every component).
9 |
10 | Mappings use paths that are relative to the root.
11 | That can be used for being able to move/copy targets
12 | without redoing all of the mappings.
13 |
--------------------------------------------------------------------------------
/control/control_target.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/control_target.tox
--------------------------------------------------------------------------------
/control/device_control_display.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/device_control_display.tox
--------------------------------------------------------------------------------
/control/device_display.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/device_display.tox
--------------------------------------------------------------------------------
/control/mftwister.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/control/mftwister.tox
--------------------------------------------------------------------------------
/corner_rect.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/corner_rect.tox
--------------------------------------------------------------------------------
/file_logger.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/file_logger.tox
--------------------------------------------------------------------------------
/hexagonal_grid.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/hexagonal_grid.tox
--------------------------------------------------------------------------------
/lib/_stubs/ArcBallExt.py:
--------------------------------------------------------------------------------
1 | class ArcBallExt():
2 | """
3 | The ArcBallExt Class helps with interactively controlling a Camera COMP
4 | given a Container COMP with the Mouse UV Buttons Parameters for left,
5 | middle and right enabled.
6 |
7 | Attributes
8 | ----------
9 | ownerComp : OP
10 | Reference to the COMP this Class is initiated by.
11 |
12 | Methods
13 | -------
14 | StartTransform(btn=None, u=0, v=0)
15 | Will begin a transform depending on the mouse button pressed.
16 | Transform(btn=None, u=0, v=0, scaler=1)
17 | Applies a transform to the ArcBall depending on the mouse button pressed.
18 | Reset()
19 | Resets the ArcBall.
20 | LoadTransform(dat=None,matrix=None)
21 | Given a tableDAT or a tdu.Matrix() it can be used to recall a saved transformation.
22 | SaveTransform(dat=op('newMat'))
23 | Will save out the current ArcBall's transformation matrix to a tableDAT. If no
24 | TableDAT is given, the internal newMat TableDAT is being used.
25 | fillMat()
26 | Utility function used by the ArcBall.
27 | """
28 | def __init__(self, ownerComp : OP):
29 | #The component to which this extension is attached
30 | self.ownerComp = ownerComp
31 | self.arcInst = tdu.ArcBall(forCamera=True)
32 | self.matrix = tdu.Matrix()
33 |
34 | # initialize ArcBall to saved out transformation matrix
35 | matrixDAT = op('newMat')
36 | for i in range(4):
37 | for j in range(4):
38 | self.matrix[i,j] = float(matrixDAT[i,j])
39 |
40 | matCopy = tdu.Matrix(self.matrix)
41 | self.arcInst.setTransform(matCopy)
42 |
43 | def StartTransform(self, btn : str = None, u : float = 0, v : float = 0) -> None:
44 | """
45 |
46 | Parameters
47 | ----------
48 | btn : str
49 | The mouse btn pressed. Can be one of 'lselect', 'rselect' or 'mselect' corrseponding to 'rotate', 'pan' and 'zoom'.
50 | u : float
51 | The horizontal mouse position on the control panel.
52 | v : float
53 | The vertical mouse position on the control panel.
54 | """
55 |
56 | if btn == 'lselect':
57 | #if lselect ==> rotate
58 | self.arcInst.beginRotate(u,v)
59 | elif btn == 'rselect':
60 | #if rselect ==> pan
61 | self.arcInst.beginPan(u,v)
62 | elif btn == 'mselect':
63 | #if mselect ==> zoom
64 | self.arcInst.beginDolly(u,v)
65 |
66 | return
67 |
68 | def Transform(self, btn : str = None, u : float = 0, v : float = 0, scaler : float = 1) -> None:
69 | """
70 |
71 | Parameters
72 | ----------
73 | btn : str
74 | The mouse btn pressed. Can be one of 'lselect', 'rselect' or 'mselect' corrseponding to 'rotate', 'pan' and 'zoom'.
75 | u : float
76 | The horizontal mouse position on the control panel.
77 | v : float
78 | The vertical mouse position on the control panel.
79 | scaler : float
80 | A multiplier to increase or decrease the transformation.
81 | """
82 |
83 | if btn == 'lselect':
84 | #if lselect ==> rotate
85 | self.arcInst.rotateTo(u,v,scale=scaler)
86 | elif btn == 'rselect':
87 | #if rselect ==> pan
88 | self.arcInst.panTo(u,v,scale=scaler)
89 | elif btn == 'mselect':
90 | #if mselect ==> zoom
91 | self.arcInst.dollyTo(u,v,scale=scaler)
92 |
93 | self.fillMat()
94 | return
95 |
96 | def Reset(self) -> None:
97 | op('autoRotate/hold1').par.pulse.pulse()
98 | op('autoRotate/hold2').par.pulse.pulse()
99 | op('autoRotate/hold3').par.pulse.pulse()
100 | self.arcInst.identity()
101 | self.fillMat()
102 | return
103 |
104 | def LoadTransform(self, dat : tableDAT = None, matrix : tdu.Matrix = None) -> None:
105 | """
106 | Parameters
107 | ----------
108 | dat : tableDAT
109 | An tableDAT operator reference to the tableDAT that holds the matrix to be loaded.
110 | matrix : tdu.MAtrix
111 | A tdu.Matrix object that holds the matrix to be loaded.
112 | """
113 |
114 | if dat and dat.numRows == 4 and dat.numCols == 4:
115 | matrix = tdu.Matrix()
116 | for i in range(4):
117 | for j in range(4):
118 | matrix[i,j] = float(dat[i,j])
119 |
120 | self.arcInst.setTransform(matrix)
121 | self.fillMat()
122 | return
123 |
124 | def SaveTransform(self, dat : tableDAT = op('newMat')) -> None:
125 | """
126 | Parameters
127 | ----------
128 | dat : tableDAT
129 | A tableDAT operator reference to the tableDAT where to write the current transform matrix into.
130 | """
131 |
132 | if dat.OPType == 'tableDAT':
133 | self.matrix.fillTable(dat)
134 |
135 | def fillMat(self):
136 | newMat = self.arcInst.transform()
137 | self.matrix = newMat
138 | self.ownerComp.cook(force=True)
139 | newMat.fillTable(op('newMat'))
140 | return
--------------------------------------------------------------------------------
/lib/_stubs/CallbacksExt.py:
--------------------------------------------------------------------------------
1 | # This file and all related intellectual property rights are
2 | # owned by Derivative Inc. ("Derivative"). The use and modification
3 | # of this file is governed by, and only permitted under, the terms
4 | # of the Derivative [End-User License Agreement]
5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp]
6 | # (the "License Agreement"). Among other terms, this file can only
7 | # be used, and/or modified for use, with Derivative's TouchDesigner
8 | # software, and only by employees of the organization that has licensed
9 | # Derivative's TouchDesigner software by [accepting] the License Agreement.
10 | # Any redistribution or sharing of this file, with or without modification,
11 | # to or with any other person is strictly prohibited [(except as expressly
12 | # permitted by the License Agreement)].
13 | #
14 | # Version: 099.2017.30440.28Sep
15 | #
16 | # _END_HEADER_
17 |
18 | # callbacks extension
19 |
20 | import traceback
21 | import reprlib
22 |
23 | #short form repr for print callbacks
24 | shortRepr = reprlib.Repr()
25 | shortRepr.maxlevel = 3
26 | shortRepr.maxlist = 100
27 | shortRepr.maxdict = 100
28 | shortRepr.maxtuple = 100
29 | shortRepr.maxset = 100
30 | shortRepr.maxfrozenset = 100
31 | shortRepr.maxdeque = 3
32 | shortRepr.maxlong = 20
33 | shortRepr.maxstring = 200
34 | shortRepr.maxother = 200
35 |
36 | class CallbacksExt:
37 | """
38 | Component extension providing a python callback system. Just call
39 | DoCallback( callbackname, callbackInfo dictionary). See DoCallback method
40 | for details.
41 |
42 | Assigned callbacks can be created for easier, more specific use. See
43 | SetAssignedCallback for details.
44 |
45 | This extension looks for two parameters on its component: A toggle named
46 | Printcallbacks, and a DAT named Callbackdat. If these aren't present, the
47 | (non-promoted!) members callbackDat and printCallbacks can be set.
48 |
49 | If you want to set up a chain of callback targets, use PassCallbacksTo to
50 | set the next target. This target can be a dat or a function.
51 | Unfound callbacks will then be sent to the appropriate callback in the dat
52 | or to the function. Also, from within a callback, you can call
53 | info['ownerComp'].PassOnCallback(info)
54 | to pass it on to the target. If you need to return a value, use:
55 | info['returnValue'] =
56 | return info['ownerComp'].PassOnCallback(info)
57 | NOTE: Returning values in chained callbacks is tricky and requires special
58 | attention both to what is being ultimately returned and how the
59 | receiver is looking at it.
60 | """
61 |
62 | def __init__(self, ownerComp):
63 | #The component to which this extension is attached
64 | self.ownerComp = ownerComp
65 | self.AssignedCallbacks = {}
66 | self.PassTarget = None
67 | self._printCallbacks = False
68 | if hasattr(ownerComp.par, 'Callbackdat'):
69 | self.callbackDat = ownerComp.par.Callbackdat.eval()
70 | if self.callbackDat:
71 | try:
72 | if self.callbackDat.text.strip():
73 | self.callbackDat.module
74 | except:
75 | print(traceback.format_exc())
76 | self.ownerComp.addScriptError("Error in Callback DAT. "
77 | "See textport for details.")
78 | raise
79 |
80 | else:
81 | self.callbackDat = None
82 |
83 | # repr function for short prints
84 | self.shortRepr = shortRepr
85 |
86 | def SetAssignedCallback(self, callbackName, callback):
87 | """
88 | An assigned callback is a callback system made to call specified python
89 | methods rather than searching a callback DAT.
90 |
91 | callbackName is the name to be passed to the DoAssignedCallback method
92 | callback is a python function that takes a single callbackInfo argument,
93 | just like standard callbacks. If callback is None, callbackName is
94 | removed from the assigned callback system.
95 | details is extra info to be passed in the ['details'] key of infoDict
96 | when callback is called. callbackName will also be added to infoDict
97 | """
98 | if callback is not None:
99 | if callable(callback):
100 | self.AssignedCallbacks[callbackName] = callback
101 | else:
102 | raise TypeError("SetAssignedCallback" + callbackName +
103 | "attempted to assign non-callable object: "
104 | + callback)
105 | else:
106 | try:
107 | del self.AssignedCallbacks[callbackName]
108 | except:
109 | pass
110 |
111 | # def DoAssignedCallback(self, callbackName, callbackInfo=None):
112 | # """
113 | # Perform the assigned callback with callbackName. See DoCallback for
114 | # full details.
115 | # """
116 | # try:
117 | # callback, details = self.AssignedCallbacks[callbackName]
118 | # except:
119 | # if self.PrintCallbacks:
120 | # debug(callbackInfo)
121 | # self.DoCallback(callbackName + " (assigned callback)",
122 | # callbackInfo, None)
123 | # return
124 | # if callbackInfo is None:
125 | # callbackInfo = {'callbackName': callbackName}
126 | # if details is not None:
127 | # callbackInfo['details'] = details
128 | # self.DoCallback(callbackName, callbackInfo, callback)
129 | #
130 | def DoCallback(self, callbackName, callbackInfo=None, callbackOrDat=None):
131 | """
132 | If it exists, call the named callback in ownerComp.par.Callbackdat.
133 | Pass any data inside callbackInfo. callbackInfo['ownerComp'] is set to
134 | self.ownerComp. If callback needs special instructions, such as looking
135 | for return data, put them in an callbackInfo['about']
136 |
137 | callbackOrDat is used for redirection to a DAT or specific function
138 |
139 | If callback is provided, the mod search is skipped and it will be
140 | called instead.
141 |
142 | If a user callback was found, returns callbackInfo with the callback
143 | return value in callbackInfo['returnValue']. If no callback found,
144 | returns None.
145 |
146 | If ownerComp has a parameter called Printcallbacks, and that parameter
147 | is True, callbacks will be printed when called.
148 | """
149 | if callable(callbackOrDat):
150 | callback = callbackOrDat
151 | else:
152 | callback = self.AssignedCallbacks.get(callbackName)
153 | if not callback:
154 | if callbackOrDat:
155 | moduleDat = callbackOrDat
156 | else:
157 | try:
158 | self.callbackDat = moduleDat = \
159 | self.ownerComp.par.Callbackdat.eval()
160 | except:
161 | self.callbackDat = moduleDat = None
162 | try:
163 | try:
164 | callbackMod = self.callbackDat.module
165 | callback = getattr(callbackMod, callbackName, None)
166 | except:
167 | pass
168 | except:
169 | if moduleDat:
170 | print(self.ownerComp, "Invalid callback DAT:",
171 | moduleDat.path)
172 | raise
173 | else:
174 | if not self.PrintCallbacks:
175 | # callback dat is blank and no print, just forget it.
176 | return
177 | callback = None
178 | if callbackInfo is None:
179 | callbackInfo = {}
180 | callbackInfo.setdefault('ownerComp', self.ownerComp)
181 | callbackInfo['callbackName'] = callbackName
182 | # do callback if found
183 | if callback:
184 | # the next line is the actual function call
185 | # put returnValue into the callbackInfo dict
186 | callbackInfo['returnValue'] = callback(callbackInfo)
187 | retvalue = callbackInfo
188 | printCallback = self.PrintCallbacks
189 | # pass callback on if pass target
190 | elif self.PassTarget and self.PassTarget != callbackOrDat:
191 | callbackInfo['returnValue'] = self.PassOnCallback(callbackInfo)
192 | retvalue = callbackInfo
193 | printCallback = False
194 | # no callback
195 | else:
196 | retvalue = None
197 | printCallback = self.PrintCallbacks
198 | # print callback
199 | if printCallback:
200 | if retvalue is None or callback and self.PassTarget:
201 | notfound = 'NOT FOUND -'
202 | else:
203 | notfound = '-'
204 | # print(callbackName, notfound,'callbackInfo: ',
205 | # self.shortRepr.repr(callbackInfo), '\n')
206 | debug(callbackName, notfound,'callbackInfo: ',
207 | callbackInfo, '\n')
208 | return retvalue
209 |
210 | def PassCallbacksTo(self, passTarget):
211 | """
212 | Set a target DAT or function for passing on unfound callbacks to.
213 | """
214 | if callable(passTarget):
215 | self.PassTarget = passTarget
216 | elif isinstance(passTarget, DAT):
217 | try:
218 | passTarget.module
219 | except:
220 | self.ownerComp.error = traceback.format_exc() + \
221 | "\nError in Callback DAT. See textport for details."
222 | raise
223 | self.PassTarget = passTarget
224 | else:
225 | raise TypeError('PassCallbacksTo target must be callable or DAT. '
226 | 'Got ' + str(passTarget) + '.')
227 |
228 | def PassOnCallback(self, info):
229 | """
230 | Pass this callback to ContextExt's PassTarget. Use PassCallbacksTo to
231 | set target
232 | """
233 | callbackName = info['callbackName']
234 | firstReturnValue = info.get('returnValue',None)
235 | returnDict = self.DoCallback(callbackName, info, self.PassTarget)
236 | if returnDict:
237 | if returnDict['returnValue'] is None:
238 | return firstReturnValue
239 | else:
240 | return returnDict['returnValue']
241 | else:
242 | return firstReturnValue
243 |
244 | @property
245 | def CallbackDat(self):
246 | return self.callbackDat
247 | @CallbackDat.setter
248 | def CallbackDat(self, val):
249 | self.callbackDat = val
250 | if hasattr(ownerComp.par, 'Callbackdat'):
251 | ownerComp.par.Callbackdat.val = self.callbackDat
252 |
253 | @property
254 | def PrintCallbacks(self):
255 | if hasattr(self.ownerComp.par, 'Printcallbacks'):
256 | return self.ownerComp.par.Printcallbacks.eval()
257 | else:
258 | return self._printCallbacks
259 | @PrintCallbacks.setter
260 | def PrintCallbacks(self, val):
261 | if hasattr(self.ownerComp.par, 'Printcallbacks'):
262 | self.ownerComp.par.Printcallbacks = val
263 | else:
264 | self._printCallbacks = val
265 |
--------------------------------------------------------------------------------
/lib/_stubs/PopDialogExt.py:
--------------------------------------------------------------------------------
1 | # This file and all related intellectual property rights are
2 | # owned by Derivative Inc. ("Derivative"). The use and modification
3 | # of this file is governed by, and only permitted under, the terms
4 | # of the Derivative [End-User License Agreement]
5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp]
6 | # (the "License Agreement"). Among other terms, this file can only
7 | # be used, and/or modified for use, with Derivative's TouchDesigner
8 | # software, and only by employees of the organization that has licensed
9 | # Derivative's TouchDesigner software by [accepting] the License Agreement.
10 | # Any redistribution or sharing of this file, with or without modification,
11 | # to or with any other person is strictly prohibited [(except as expressly
12 | # permitted by the License Agreement)].
13 | #
14 | # Version: 099.2017.30440.28Sep
15 | #
16 | # _END_HEADER_
17 |
18 | from TDStoreTools import StorageManager
19 | import TDFunctions as TDF
20 | class PopDialogExt:
21 |
22 | def __init__(self, ownerComp):
23 | """
24 | Popup dialog extension. Just call the DoPopup method to create a popup.
25 | Provide info in that method. This component can be used over and over,
26 | no need for a different component for each dialog, unless you want to
27 | change the insides.
28 | """
29 | self.ownerComp = ownerComp
30 | self.windowComp = ownerComp.op('popDialogWindow')
31 | self.details = None
32 |
33 | # upgrade version
34 | h = self.ownerComp.par.h
35 | if h.mode == ParMode.EXPRESSION and h.expr == "op('./dialog').par.h":
36 | h.expr = "me.DialogHeight"
37 | h.readOnly = True
38 |
39 | TDF.createProperty(self, 'EnteredText', value='')
40 | TDF.createProperty(self, 'TextHeight', value=0)
41 | run("args[0].UpdateTextHeight()", self, delayFrames=1,
42 | delayRef=op.TDResources)
43 |
44 | def OpenDefault(self, text='', title='', buttons=('OK',), callback=None,
45 | details=None, textEntry=False, escButton=1,
46 | escOnClickAway=True, enterButton=1):
47 | self.Open(text, title, buttons, callback, details, textEntry, escButton,
48 | escOnClickAway, enterButton)
49 |
50 | def Open(self, text=None, title=None, buttons=None, callback=None,
51 | details=None, textEntry=None, escButton=None,
52 | escOnClickAway=None, enterButton=None):
53 | """
54 | Open a popup dialog.
55 | text goes in the center of the dialog. Default None, use pars.
56 | title goes on top of the dialog. Blank means no title bar. Default None,
57 | use pars
58 | buttons is a list of strings. The number of buttons is equal to the
59 | number of buttons, up to 4. Default is ['OK']
60 | callback: a method that will be called when a selection is made, see the
61 | SetCallback method. This is in addition to all internal callbacks.
62 | If not provided, Callback DAT will be searched.
63 | details: will be passed to callback in addition to item chosen.
64 | Default is None.
65 | If textEntry is a string, display textEntry field and use the string
66 | as a default. If textEntry is False, no entry field. Default is
67 | None, use pars
68 | escButton is a number from 1-4 indicating which button is simulated when
69 | esc is pressed or False for no button simulation. Default is None,
70 | use pars. First button is 1 not 0!!!
71 | enterButton is a number from 1-4 indicating which button is simulated
72 | when enter is pressed or False for no button simulation. Default is
73 | None, use pars. First button is 1 not 0!!!
74 | escOnClickAway is a boolean indicating whether esc is simulated when user
75 | clicks somewhere besides the dialog. Default is None, use pars
76 | """
77 | self.Close()
78 | # text and title
79 | if text is not None:
80 | self.ownerComp.par.Text = text
81 | if title is not None:
82 | self.ownerComp.par.Title = title
83 | # buttons
84 | if buttons is not None:
85 | if not isinstance(buttons, list):
86 | buttons = ['OK']
87 | self.ownerComp.par.Buttons = len(buttons)
88 | for i, label in enumerate(buttons[:4]):
89 | getattr(self.ownerComp.par,
90 | 'Buttonlabel' + str(i + 1)).val = label
91 | # callbacks
92 | if callback:
93 | ext.CallbacksExt.SetAssignedCallback('onSelect', callback)
94 | else:
95 | ext.CallbacksExt.SetAssignedCallback('onSelect', None)
96 | # textEntry
97 | if textEntry is not None:
98 | if isinstance(textEntry, str):
99 | self.ownerComp.par.Textentryarea = True
100 | self.ownerComp.par.Textentrydefault = str(textEntry)
101 | elif textEntry:
102 | self.ownerComp.par.Textentryarea = True
103 | self.ownerComp.par.Textentrydefault = ''
104 | else:
105 | self.ownerComp.par.Textentryarea = False
106 | self.EnteredText = self.ownerComp.par.Textentrydefault.eval()
107 | self.details = details
108 | self.ownerComp.op('entry/inputText').par.text = self.EnteredText
109 | self.ownerComp.op('entry').cook(force=True)
110 | if escButton is not None:
111 | if escButton is False or not (1 <= escButton <= 4):
112 | self.ownerComp.par.Escbutton = 'None'
113 | else:
114 | self.ownerComp.par.Escbutton = str(escButton)
115 | if escOnClickAway is not None:
116 | self.ownerComp.par.Esconclickaway = escOnClickAway
117 | if enterButton is not None:
118 | if enterButton is False or not (1 <= enterButton <= 4):
119 | self.ownerComp.par.Enterbutton = 'None'
120 | else:
121 | self.ownerComp.par.Enterbutton = str(enterButton)
122 | self.UpdateTextHeight()
123 | # HACK shouldn't be necessary - problem with clones/replicating
124 | self.ownerComp.op('replicator1').par.recreateall.pulse()
125 | run("op('" + self.ownerComp.path + "').ext.PopDialogExt.actualOpen()",
126 | delayFrames=1, delayRef=op.TDResources)
127 |
128 | def actualOpen(self):
129 | # needs to be deferred so that sizes can update properly
130 | self.windowComp.par.winopen.pulse()
131 | ext.CallbacksExt.DoCallback('onOpen')
132 | if self.ownerComp.op('entry').par.display.eval():
133 | # self.ownerComp.setFocus()
134 | # hack shouldn't have to wait a frame
135 | run('op("' + self.ownerComp.path + '").op("entry/inputText").'
136 | 'setKeyboardFocus(selectAll=True)',
137 | delayFrames=1, delayRef=op.TDResources)
138 | else:
139 | self.ownerComp.setFocus()
140 |
141 | def Close(self):
142 | """
143 | Close the dialog
144 | """
145 | ext.CallbacksExt.SetAssignedCallback('onSelect', None)
146 | ext.CallbacksExt.DoCallback('onClose')
147 | self.windowComp.par.winclose.pulse()
148 | self.ownerComp.op('entry/inputText').par.text = self.EnteredText
149 |
150 | def OnButtonClicked(self, buttonNum):
151 | """
152 | Callback from buttons
153 | """
154 | infoDict = {'buttonNum': buttonNum,
155 | 'button': getattr(self.ownerComp.par,
156 | 'Buttonlabel' + str(buttonNum)).eval(),
157 | 'details': self.details}
158 | if self.ownerComp.par.Textentryarea.eval():
159 | infoDict['enteredText'] = self.EnteredText
160 | try:
161 | ext.CallbacksExt.DoCallback('onSelect', infoDict)
162 | finally:
163 | self.Close()
164 |
165 | def OnKeyPressed(self, key):
166 | """
167 | Callback for esc or enterpressed.
168 | """
169 | if key == 'esc' and self.ownerComp.par.Escbutton.eval() != 'None':
170 | button = int(self.ownerComp.par.Escbutton.eval())
171 | if button <= self.ownerComp.par.Buttons:
172 | self.OnButtonClicked(button)
173 | if key == 'enter' and self.ownerComp.par.Enterbutton.eval() != 'None':
174 | button = int(self.ownerComp.par.Enterbutton.eval())
175 | if button <= self.ownerComp.par.Buttons:
176 | self.OnButtonClicked(button)
177 |
178 | def OnClickAway(self):
179 | """
180 | Callback for esc pressed. Only happens when Escbutton is not None
181 | """
182 | if self.ownerComp.par.Esconclickaway.eval():
183 | self.OnKeyPressed('esc')
184 |
185 | def OnParValueChange(self, par, val, prev):
186 | """
187 | Callback for when parameters change
188 | """
189 | if par.name == "Textentryarea":
190 | self.ownerComp.par.Textentrydefault.enable = par.eval()
191 | if par.name == "Escbutton":
192 | self.ownerComp.par.Esconclickaway.enable = par.eval() != "None"
193 |
194 | def OnParPulse(self, par):
195 | if par.name == "Open":
196 | self.Open()
197 | elif par.name == "Close":
198 | self.Close()
199 | elif par.name == 'Editcallbacks':
200 | dat = self.ownerComp.par.Callbackdat.eval()
201 | if dat:
202 | dat.par.edit.pulse()
203 | else:
204 | print("No callback dat for", self.ownerComp.path)
205 | elif par.name == 'Helppage':
206 | ui.viewFile('https://docs.derivative.ca/'
207 | 'index.php?title=Palette:popDialog')
208 |
209 | def UpdateTextHeight(self):
210 | self.TextHeight = self.ownerComp.op('text/text').evalTextSize(
211 | self.ownerComp.par.Text)[1]
212 |
213 | @property
214 | def DialogHeight(self):
215 | return 65 + self.TextHeight + \
216 | (20 if self.ownerComp.par.Title else 0) + \
217 | (37 if self.ownerComp.par.Textentryarea else 0)
--------------------------------------------------------------------------------
/lib/_stubs/TDCallbacksExt.py:
--------------------------------------------------------------------------------
1 | # This file and all related intellectual property rights are
2 | # owned by Derivative Inc. ("Derivative"). The use and modification
3 | # of this file is governed by, and only permitted under, the terms
4 | # of the Derivative [End-User License Agreement]
5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp]
6 | # (the "License Agreement"). Among other terms, this file can only
7 | # be used, and/or modified for use, with Derivative's TouchDesigner
8 | # software, and only by employees of the organization that has licensed
9 | # Derivative's TouchDesigner software by [accepting] the License Agreement.
10 | # Any redistribution or sharing of this file, with or without modification,
11 | # to or with any other person is strictly prohibited [(except as expressly
12 | # permitted by the License Agreement)].
13 | #
14 | # Version: 099.2017.30440.28Sep
15 | #
16 | # _END_HEADER_
17 |
18 | # callbacks extension
19 |
20 | import traceback
21 | import reprlib
22 |
23 | #short form repr for print callbacks
24 | shortRepr = reprlib.Repr()
25 | shortRepr.maxlevel = 3
26 | shortRepr.maxlist = 100
27 | shortRepr.maxdict = 100
28 | shortRepr.maxtuple = 100
29 | shortRepr.maxset = 100
30 | shortRepr.maxfrozenset = 100
31 | shortRepr.maxdeque = 3
32 | shortRepr.maxlong = 20
33 | shortRepr.maxstring = 200
34 | shortRepr.maxother = 200
35 |
36 | class CallbacksExt:
37 | """
38 | Component extension providing a python callback system. Just call
39 | DoCallback( callbackname, callbackInfo dictionary). See DoCallback method
40 | for details.
41 |
42 | Assigned callbacks can be created for easier, more specific use. See
43 | SetAssignedCallback for details.
44 |
45 | This extension looks for two parameters on its component: A toggle named
46 | Printcallbacks, and a DAT named Callbackdat. If these aren't present, the
47 | (non-promoted!) members callbackDat and printCallbacks can be set.
48 |
49 | If you want to set up a chain of callback targets, use PassCallbacksTo to
50 | set the next target. This target can be a dat or a function.
51 | Unfound callbacks will then be sent to the appropriate callback in the dat
52 | or to the function. Also, from within a callback, you can call
53 | info['ownerComp'].PassOnCallback(info)
54 | to pass it on to the target. If you need to return a value, use:
55 | info['returnValue'] =
56 | return info['ownerComp'].PassOnCallback(info)
57 | NOTE: Returning values in chained callbacks is tricky and requires special
58 | attention both to what is being ultimately returned and how the
59 | receiver is looking at it.
60 | """
61 |
62 | def __init__(self, ownerComp):
63 | #The component to which this extension is attached
64 | self.ownerComp = ownerComp
65 | self.AssignedCallbacks = {}
66 | self.PassTarget = None
67 | self._printCallbacks = False
68 | if hasattr(ownerComp.par, 'Callbackdat'):
69 | self.callbackDat = ownerComp.par.Callbackdat.eval()
70 | if self.callbackDat:
71 | try:
72 | if self.callbackDat.text.strip():
73 | self.callbackDat.module
74 | except:
75 | print(traceback.format_exc())
76 | self.ownerComp.addScriptError("Error in Callback DAT. "
77 | "See textport for details.")
78 | raise
79 |
80 | else:
81 | self.callbackDat = None
82 |
83 | # repr function for short prints
84 | self.shortRepr = shortRepr
85 |
86 | def SetAssignedCallback(self, callbackName, callback):
87 | """
88 | An assigned callback is a callback system made to call specified python
89 | methods rather than searching a callback DAT.
90 |
91 | callbackName is the name to be passed to the DoAssignedCallback method
92 | callback is a python function that takes a single callbackInfo argument,
93 | just like standard callbacks. If callback is None, callbackName is
94 | removed from the assigned callback system.
95 | details is extra info to be passed in the ['details'] key of infoDict
96 | when callback is called. callbackName will also be added to infoDict
97 | """
98 | if callback is not None:
99 | if callable(callback):
100 | self.AssignedCallbacks[callbackName] = callback
101 | else:
102 | raise TypeError("SetAssignedCallback" + callbackName +
103 | "attempted to assign non-callable object: "
104 | + callback)
105 | else:
106 | try:
107 | del self.AssignedCallbacks[callbackName]
108 | except:
109 | pass
110 |
111 | # def DoAssignedCallback(self, callbackName, callbackInfo=None):
112 | # """
113 | # Perform the assigned callback with callbackName. See DoCallback for
114 | # full details.
115 | # """
116 | # try:
117 | # callback, details = self.AssignedCallbacks[callbackName]
118 | # except:
119 | # if self.PrintCallbacks:
120 | # debug(callbackInfo)
121 | # self.DoCallback(callbackName + " (assigned callback)",
122 | # callbackInfo, None)
123 | # return
124 | # if callbackInfo is None:
125 | # callbackInfo = {'callbackName': callbackName}
126 | # if details is not None:
127 | # callbackInfo['details'] = details
128 | # self.DoCallback(callbackName, callbackInfo, callback)
129 | #
130 | def DoCallback(self, callbackName, callbackInfo=None, callbackOrDat=None):
131 | """
132 | If it exists, call the named callback in ownerComp.par.Callbackdat.
133 | Pass any data inside callbackInfo. callbackInfo['ownerComp'] is set to
134 | self.ownerComp. If callback needs special instructions, such as looking
135 | for return data, put them in an callbackInfo['about']
136 |
137 | callbackOrDat is used for redirection to a DAT or specific function
138 |
139 | If callback is provided, the mod search is skipped and it will be
140 | called instead.
141 |
142 | If a user callback was found, returns callbackInfo with the callback
143 | return value in callbackInfo['returnValue']. If no callback found,
144 | returns None.
145 |
146 | If ownerComp has a parameter called Printcallbacks, and that parameter
147 | is True, callbacks will be printed when called.
148 | """
149 | if callable(callbackOrDat):
150 | callback = callbackOrDat
151 | else:
152 | callback = self.AssignedCallbacks.get(callbackName)
153 | if not callback:
154 | if callbackOrDat:
155 | moduleDat = callbackOrDat
156 | else:
157 | try:
158 | self.callbackDat = moduleDat = \
159 | self.ownerComp.par.Callbackdat.eval()
160 | except:
161 | self.callbackDat = moduleDat = None
162 | try:
163 | try:
164 | callbackMod = self.callbackDat.module
165 | callback = getattr(callbackMod, callbackName, None)
166 | except:
167 | pass
168 | except:
169 | if moduleDat:
170 | print(self.ownerComp, "Invalid callback DAT:",
171 | moduleDat.path)
172 | raise
173 | else:
174 | if not self.PrintCallbacks:
175 | # callback dat is blank and no print, just forget it.
176 | return
177 | callback = None
178 | if callbackInfo is None:
179 | callbackInfo = {}
180 | callbackInfo.setdefault('ownerComp', self.ownerComp)
181 | callbackInfo['callbackName'] = callbackName
182 | # do callback if found
183 | if callback:
184 | # the next line is the actual function call
185 | # put returnValue into the callbackInfo dict
186 | callbackInfo['returnValue'] = callback(callbackInfo)
187 | retvalue = callbackInfo
188 | printCallback = self.PrintCallbacks
189 | # pass callback on if pass target
190 | elif self.PassTarget and self.PassTarget != callbackOrDat:
191 | callbackInfo['returnValue'] = self.PassOnCallback(callbackInfo)
192 | retvalue = callbackInfo
193 | printCallback = False
194 | # no callback
195 | else:
196 | retvalue = None
197 | printCallback = self.PrintCallbacks
198 | # print callback
199 | if printCallback:
200 | if retvalue is None or callback and self.PassTarget:
201 | notfound = 'NOT FOUND -'
202 | else:
203 | notfound = '-'
204 | # print(callbackName, notfound,'callbackInfo: ',
205 | # self.shortRepr.repr(callbackInfo), '\n')
206 | debug(callbackName, notfound,'callbackInfo: ',
207 | callbackInfo, '\n')
208 | return retvalue
209 |
210 | def PassCallbacksTo(self, passTarget):
211 | """
212 | Set a target DAT or function for passing on unfound callbacks to.
213 | """
214 | if callable(passTarget):
215 | self.PassTarget = passTarget
216 | elif isinstance(passTarget, DAT):
217 | try:
218 | passTarget.module
219 | except:
220 | self.ownerComp.error = traceback.format_exc() + \
221 | "\nError in Callback DAT. See textport for details."
222 | raise
223 | self.PassTarget = passTarget
224 | else:
225 | raise TypeError('PassCallbacksTo target must be callable or DAT. '
226 | 'Got ' + str(passTarget) + '.')
227 |
228 | def PassOnCallback(self, info):
229 | """
230 | Pass this callback to ContextExt's PassTarget. Use PassCallbacksTo to
231 | set target
232 | """
233 | callbackName = info['callbackName']
234 | firstReturnValue = info.get('returnValue',None)
235 | returnDict = self.DoCallback(callbackName, info, self.PassTarget)
236 | if returnDict:
237 | if returnDict['returnValue'] is None:
238 | return firstReturnValue
239 | else:
240 | return returnDict['returnValue']
241 | else:
242 | return firstReturnValue
243 |
244 | @property
245 | def CallbackDat(self):
246 | return self.callbackDat
247 | @CallbackDat.setter
248 | def CallbackDat(self, val):
249 | self.callbackDat = val
250 | if hasattr(ownerComp.par, 'Callbackdat'):
251 | ownerComp.par.Callbackdat.val = self.callbackDat
252 |
253 | @property
254 | def PrintCallbacks(self):
255 | if hasattr(self.ownerComp.par, 'Printcallbacks'):
256 | return self.ownerComp.par.Printcallbacks.eval()
257 | else:
258 | return self._printCallbacks
259 | @PrintCallbacks.setter
260 | def PrintCallbacks(self, val):
261 | if hasattr(self.ownerComp.par, 'Printcallbacks'):
262 | self.ownerComp.par.Printcallbacks = val
263 | else:
264 | self._printCallbacks = val
265 |
--------------------------------------------------------------------------------
/lib/_stubs/TDCodeGen.py:
--------------------------------------------------------------------------------
1 | # This file and all related intellectual property rights are
2 | # owned by Derivative Inc. ("Derivative"). The use and modification
3 | # of this file is governed by, and only permitted under, the terms
4 | # of the Derivative [End-User License Agreement]
5 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp]
6 | # (the "License Agreement"). Among other terms, this file can only
7 | # be used, and/or modified for use, with Derivative's TouchDesigner
8 | # software, and only by employees of the organization that has licensed
9 | # Derivative's TouchDesigner software by [accepting] the License Agreement.
10 | # Any redistribution or sharing of this file, with or without modification,
11 | # to or with any other person is strictly prohibited [(except as expressly
12 | # permitted by the License Agreement)].
13 | #
14 | # Version: 099.2017.30440.28Sep
15 | #
16 | # _END_HEADER_
17 |
18 | """TDCodeGen
19 |
20 | Helpers for automatically generated Python code. Heavily based on the ast
21 | Python library
22 | """
23 |
24 | import ast
25 | script = op('script1_callbacks')
26 |
27 | def datFunctionInfo(dat):
28 | """
29 | Get info about all the top level functions in a DAT
30 |
31 | Args:
32 | dat: the dat to analyze
33 |
34 | Returns:
35 | {'functionName': {'docString': docString, 'lines': line range}, ...}
36 | """
37 | ast = datAst(dat)
38 | functions = nodeFunctions(ast)
39 | return {functionName:
40 | {'docString': nodeDocString(node),
41 | 'lines': nodeLines(node)}
42 | for functionName, node in functions.items()}
43 |
44 | def datRemoveFunction(dat, functionName):
45 | """
46 | Remove a function from a dat
47 |
48 | Args:
49 | dat: the DAT
50 | functionName: name of function to remove
51 | """
52 | info = datFunctionInfo(dat)
53 | if functionName in info:
54 | functionInfo = info[functionName]
55 | lines = functionInfo['lines']
56 | lines[0] -= 1 # 1 based
57 | lines[1] -= 1 # 1 based
58 | datLines = dat.text.splitlines()
59 | # remove a blank line if surrounded on both sides
60 | if len(datLines) > lines[1] and not datLines[lines[0] - 1].strip() \
61 | and not datLines[lines[1]].strip():
62 | lines[1] += 1
63 | newLines = datLines[:lines[0]] + datLines[lines[1]:]
64 | dat.text = '\n'.join(newLines)
65 |
66 | def datInsertText(dat, text, insertLine=None):
67 | """
68 | Insert text into a dat.
69 |
70 | Args:
71 | dat: the DAT
72 | text: the text to be inserted.
73 | insertLine: the line at which to insert text. If None (default), insert
74 | text after last non-pblank line in file.
75 | """
76 | insertLines = text.splitlines()
77 | datLines = dat.text.splitlines()
78 | if insertLine is None:
79 | insertLine = len(datLines)
80 | while not datLines[insertLine-1].strip() and insertLine > 1:
81 | insertLine -= 1
82 | else:
83 | insertLine -= 1 # 1 based
84 | newText = datLines[:insertLine] + insertLines + datLines[insertLine:]
85 | dat.text = '\n'.join(newText)
86 |
87 | def datAst(dat):
88 | """
89 | Get the ast node of a DAT holding Python code
90 |
91 | Args:
92 | dat: the dat to analyze
93 |
94 | Returns:
95 | ast node of the DAT's text. Will be ast.Module at top
96 | """
97 | return ast.parse(dat.text)
98 |
99 | def nodeFunctions(node):
100 | """
101 | Get info about functions at the top level of node
102 |
103 | Args:
104 | node: the ast node to search
105 |
106 | Returns:
107 | {: ast node of function}
108 | """
109 | functionDict = {}
110 | for item in node.body:
111 | if isinstance(item, ast.FunctionDef):
112 | functionDict[item.name] = item
113 | return functionDict
114 |
115 | def nodeDocString(node):
116 | """
117 | Get the docstring of a node
118 |
119 | Args:
120 | node: ast node
121 |
122 | Returns:
123 | docstring or none
124 | """
125 | try:
126 | item = node.body[0]
127 | if isinstance(item, ast.Expr) and isinstance(item.value, ast.Str):
128 | return item.value.s
129 | except:
130 | return None
131 |
132 | def nodeLines(node):
133 | """
134 | Get the line number range of a node
135 |
136 | Args:
137 | node: an ast node
138 |
139 | Returns:
140 | (start line #, end line # + 1)
141 | """
142 |
143 | min_lineno = node.lineno
144 | max_lineno = node.lineno
145 | for node in ast.walk(node):
146 | if hasattr(node, "lineno"):
147 | min_lineno = min(min_lineno, node.lineno)
148 | max_lineno = max(max_lineno, node.lineno)
149 | return [min_lineno, max_lineno + 1]
--------------------------------------------------------------------------------
/lib/_stubs/TDStoreTools.py:
--------------------------------------------------------------------------------
1 |
2 | # This file and all related intellectual property rights are
3 | # owned by Derivative Inc. ("Derivative"). The use and modification
4 | # of this file is governed by, and only permitted under, the terms
5 | # of the Derivative [End-User License Agreement]
6 | # [https://www.derivative.ca/Agreements/UsageAgreementTouchDesigner.asp]
7 | # (the "License Agreement"). Among other terms, this file can only
8 | # be used, and/or modified for use, with Derivative's TouchDesigner
9 | # software, and only by employees of the organization that has licensed
10 | # Derivative's TouchDesigner software by [accepting] the License Agreement.
11 | # Any redistribution or sharing of this file, with or without modification,
12 | # to or with any other person is strictly prohibited [(except as expressly
13 | # permitted by the License Agreement)].
14 | #
15 | # Version: 099.2017.30440.28Sep
16 | #
17 | # _END_HEADER_
18 |
19 | # TDStoreTools
20 |
21 | from collections.abc import MutableSet, MutableMapping, MutableSequence
22 | from abc import ABCMeta, abstractmethod
23 | from numbers import Number
24 |
25 |
26 | class StorageManager(MutableMapping):
27 | def __init__(self, extension, ownerComp, storedItems=None,
28 | restoreAllDefaults=False, sync=True, dictName=None,
29 | locked=True):
30 | """
31 | StorageManager manages a TDStoreTools.dependDict for ease of use in
32 | extensions.
33 | extension: the extension using this StorageManager
34 | ownerComp: the comp to manage storage for
35 | storedItems: a list of dictionaries in the form:
36 | {'name': itemName, 'default': defaultValue, 'readOnly': T/F,
37 | 'property': T/F, 'dependable':T/F}
38 |
39 | if unspecified, defaultValue will be None.
40 | property defaults to True and decides whether to add item as a
41 | property of extension
42 | readOnly defaults to False and defines whether the created property
43 | is readOnly
44 | dependable defaults to False and determines if a container will be
45 | wrapped in a dependency. These are slower but will cause nodes
46 | that observe them to cook properly.
47 | capitalized properties can be promoted in the extension.
48 | restoreAllDefaults: if True, force all values to their default
49 | sync: if True, clear old items that are no longer defined and set to
50 | default value if they are new
51 | dictName: all stored data will go in this dict in storage. defaults to
52 | ownerComp.__class__.__name__ + 'Store'
53 | locked: if True, raise an exception if an attempt is made to add a value
54 | not defined in storedItems
55 | """
56 | self._items = {} # will be set up in setItems...
57 | # {'attrName': provided dict}
58 | # this is an internal list of item info and should
59 | # not be messed with lightly
60 |
61 | self.locked = False
62 | if isinstance(ownerComp, COMP):
63 | self.ownerComp = ownerComp
64 | else:
65 | raise TypeError('Invalid owner for StorageManager', ownerComp)
66 | self.extension = extension
67 | if dictName is None:
68 | dictName = extension.__class__.__name__ + 'Stored'
69 | if dictName in ownerComp.storage:
70 | self.storageDict = ownerComp.storage[dictName]
71 | else:
72 | self.storageDict = ownerComp.store(dictName, DependDict())
73 | setattr(extension, '_storageDict', self.storageDict)
74 | if storedItems is None:
75 | storedItems = []
76 | self._setItems(storedItems, sync=sync)
77 | if restoreAllDefaults:
78 | self.restoreAllDefaults()
79 | self.locked = locked
80 |
81 | def restoreAllDefaults(self):
82 | """
83 | Restore all storage items to the default value defined during init.
84 | If no default value was defined, item will be set to None
85 | """
86 | for item, info in self._items.items():
87 | self.storageDict[item] = info['default']
88 |
89 | def restoreDefault(self, storageItem):
90 | if storageItem in self._items:
91 | self.storageDict[storageItem] = self._items[storageItem]['default']
92 |
93 | def _sync(self, deleteOld=True):
94 | """
95 | Create items in storage, make properties, and set to default if
96 | necessary. If deleteOld is True, delete stored items that aren't in item
97 | list.
98 | Should really only be done during initialization.
99 | """
100 | if deleteOld:
101 | oldKeys = []
102 | for key in self.storageDict:
103 | if key not in self._items:
104 | oldKeys.append(key)
105 | for key in [key for key in self.storageDict
106 | if key not in self._items]:
107 | del self.storageDict[key]
108 | for key, info in self._items.items():
109 | if key not in self.storageDict:
110 | try:
111 | self[key] = info['default']
112 | except:
113 | import traceback;
114 | traceback.print_exc()
115 | print('Unable to create ' + info['name'],
116 | 'on', self.ownerComp, info['default'])
117 | raise
118 | else:
119 | # check to make sure 'dependable' flag hasn't changed:
120 | if info['dependable'] and not isinstance(self[key],
121 | DependMixin):
122 | # re-setting causes dependability to be updated
123 | self.storageDict[key] = self.storageDict[key]
124 | elif not info['dependable'] and isinstance(self[key],
125 | DependMixin):
126 | try:
127 | self[key] = self[key].getRaw() # attempt to update
128 | except:
129 | print('Unable to update ' + info['name'],
130 | 'on', self.ownerComp, self[key])
131 | raise
132 | if info['property']:
133 | self._makeProperty(key, info['readOnly'])
134 |
135 | def _makeProperty(self, key, readOnly=False):
136 | """
137 | Creates a property on ownerComp. Really should only be done during
138 | initialization.
139 | """
140 | if not isinstance(key, str) or not key.isidentifier():
141 | raise ValueError('Invalid identifier in stored items', key,
142 | self.ownerComp)
143 | # def getter(s):
144 | # return s.storage[self.dictName]
145 | # debug(id(self.storageDict),
146 | # id(self.ownerComp.storage[self.dictName]))
147 |
148 | if readOnly:
149 | def setter(s, val):
150 | raise AttributeError("Can't set attribute", key, val,
151 | self.ownerComp)
152 | else:
153 | def setter(s, val):
154 | s._storageDict[key] = val
155 | prop = property(lambda s: s._storageDict[key], setter)
156 | try:
157 | setattr(self.extension.__class__, key, prop)
158 | except:
159 | print('Unable to create', key, 'property on', self.extension)
160 | raise
161 |
162 | def _setItems(self, storedItems, sync=True):
163 | """
164 | Create values in dictionary and set up item values if necessary.
165 | items is a list of lists or dicts just like in __init__
166 | If sync is True, perform a sync as well.
167 | """
168 | oldItems = self._items.copy()
169 | self._items = {}
170 | for item in storedItems:
171 | self._addItem(item)
172 | for name, info in oldItems.items():
173 | if name not in self._items and info['property'] is False:
174 | # Watch out for this! Changing the ownerComp's class!
175 | delattr(self.ownerComp.__class__, name)
176 | if sync:
177 | self._sync()
178 |
179 | def _addItem(self, storageItem):
180 | """
181 | Add an item to StorageManager. WARNING: all instances of an extension
182 | class will share the properties of that class!
183 | """
184 | if isinstance(storageItem, dict) and storageItem.get('name', None):
185 | storageItem.setdefault('default', None)
186 | storageItem.setdefault('readOnly', False)
187 | storageItem.setdefault('property', True)
188 | storageItem.setdefault('dependable', False)
189 | self._items[storageItem['name']] = storageItem
190 | else:
191 | raise ValueError("StorageItems must be a list of dictionaries. See "
192 | "StorageManager docs.", self.ownerComp)
193 |
194 | def _removeItem(self, itemName):
195 | """
196 | Remove an item from StorageManager. WARNING: all instances of an
197 | extension class will share the properties of that class!
198 | """
199 | if itemName in self._items:
200 | propertyType = self._items[itemName][1]
201 | if propertyType:
202 | delattr(self.ownerComp.__class__, itemName)
203 | del self._items[itemName]
204 |
205 | # Fake operation as dictionary
206 |
207 | def __getitem__(self, key):
208 | return self.storageDict[key]
209 |
210 | def __setitem__(self, key, val):
211 | if self.locked and key not in self.storageDict:
212 | raise KeyError("Can't create key in locked storage dictionary",
213 | key, self.ownerComp)
214 | if self._items[key]['dependable']:
215 | self.storageDict[key] = val
216 | else:
217 | # allow dict, list, set to go in raw
218 | self.storageDict.setItem(key, val,
219 | raw=isinstance(val, (dict, list, set)))
220 |
221 | def __delitem__(self, key):
222 | del self.storageDict[key]
223 |
224 | def __iter__(self):
225 | return iter(self.storageDict)
226 |
227 | def __len__(self):
228 | return len(self.storageDict)
229 |
230 |
231 | # some basic functionalities in all our Depend collections
232 | class DependMixin:
233 | __metaclass__ = ABCMeta
234 |
235 | @abstractmethod
236 | def __init__(self):
237 | self.myMainDep = tdu.Dependency()
238 | self.parentDep = None
239 |
240 | @abstractmethod
241 | def getRaw(self):
242 | """
243 | returns dependable with dependency wrappers removed
244 | """
245 | self.myMainDep.val # this has to be in your method definition
246 |
247 | def getDependencies(self):
248 | """
249 | returns object with dependency wrappers intact
250 | """
251 | return self.myItems
252 |
253 | def __len__(self):
254 | # try:
255 | # debug(self.myItems)
256 | # except:
257 | # pass
258 | self.myMainDep.val # dummy for dependency
259 | return len(self.myItems)
260 |
261 | def __str__(self):
262 | self.myMainDep.val # dummy for dependency
263 | # return str(self.myItems)
264 | return str(self.getRaw())
265 |
266 | def __repr__(self):
267 | self.myMainDep.val # dummy for dependency
268 | return 'type: ' + self.__class__.__name__ + ' val: ' + str(self.myItems)
269 |
270 | def __iter__(self):
271 | self.myMainDep.val # dummy for dependency
272 | return iter(self.myItems)
273 |
274 | def __contains__(self, item):
275 | self.myMainDep.val # dummy for dependency
276 | return item in self.myItems
277 |
278 | def __del__(self):
279 | self.myMainDep.modified()
280 |
281 | def copy(self):
282 | return self.__class__(self.myItems)
283 |
284 |
285 | class DependDict(DependMixin, MutableMapping):
286 | def __init__(self, *args, **kwargs):
287 | DependMixin.__init__(self)
288 | self.myItems = dict()
289 | self.update(dict(*args, **kwargs)) # use the free update to set keys
290 |
291 | @property
292 | def val(self):
293 | return self
294 |
295 | @val.setter
296 | def val(self, value):
297 | try:
298 | self.clear()
299 | self.update(value)
300 | self.myMainDep.modified()
301 | except:
302 | print("DependDict.val can only be set to a dict")
303 |
304 | def getRaw(self, key=None):
305 | self.myMainDep.val
306 | if key is None:
307 | return {key: item.val.getRaw()
308 | if isinstance(item.val, DependMixin) else item.val
309 | for key, item in self.myItems.items()}
310 | if isinstance(self.myItems[key], DependMixin):
311 | return self.myItems[key].getRaw()
312 | else:
313 | return self.myItems[key].val
314 |
315 | def __getitem__(self, key):
316 | try:
317 | item = self.myItems[key]
318 | return item.val
319 | except:
320 | self.myMainDep.val # dummy for dependency
321 | raise
322 |
323 | def getDependency(self, key):
324 | return self.myItems[key]
325 |
326 | def __setitem__(self, key, item):
327 | self.setItem(key, item)
328 |
329 | def setItem(self, key, item, raw=False):
330 | if key in self.myItems:
331 | if raw or isImmutable(item):
332 | self.myItems[key].val = item
333 | return
334 | else:
335 | self.myItems[key].modified()
336 | newv = makeDependable(self, item, raw)
337 | self.myItems[key] = newv
338 | self.myMainDep.modified()
339 |
340 | def clear(self):
341 | try:
342 | for i in self.myItems:
343 | i.modified()
344 | except:
345 | pass
346 | self.myItems.clear()
347 | self.myMainDep.modified()
348 |
349 | # def update(self, *args, **kwargs):
350 | # self.myItems.update(*args, **kwargs)
351 |
352 | def __delitem__(self, key):
353 | self.myItems[key].modified()
354 | del self.myItems[key]
355 | self.myMainDep.modified()
356 |
357 |
358 |
359 | class DependList(DependMixin, MutableSequence):
360 | def __init__(self, arg=None):
361 | if arg is None:
362 | arg = []
363 | DependMixin.__init__(self)
364 | self.myItems = []
365 | for i in arg:
366 | self.append(i)
367 |
368 | @property
369 | def val(self):
370 | return self
371 |
372 | @val.setter
373 | def val(self, value):
374 | if isinstance(value, list):
375 | self.clear()
376 | for i in value:
377 | self.append(i)
378 | self.myMainDep.modified()
379 | else:
380 | raise TypeError("DependDict.val can only be set to a dict")
381 |
382 | def getRaw(self, index=None):
383 | self.myMainDep.val
384 | if index is None:
385 | return [item.val.getRaw() if isinstance(item.val, DependMixin)
386 | else item.val for item in self.myItems]
387 | if isinstance(self.myItems[index].val, DependMixin):
388 | return self.myItems[index].val.getRaw()
389 | else:
390 | return self.myItems[index].val
391 |
392 | def append(self, value, raw=False):
393 | self.insert(len(self.myItems), value, raw)
394 |
395 | def insert(self, index, item, raw=False):
396 | newitem = makeDependable(self, item, raw)
397 | self.myItems.insert(index, newitem)
398 | for i in range(index, len(self.myItems)):
399 | self.myItems[i].modified()
400 | self.myMainDep.modified()
401 |
402 | def __getitem__(self, index):
403 | try:
404 | item = self.myItems[index]
405 | return item.val
406 | except:
407 | self.myMainDep.val # dummy for dependency
408 | raise
409 |
410 | def __setitem__(self, index, item):
411 | self.setItem(index, item)
412 |
413 | def setItem(self, index, item, raw=False):
414 | newv = makeDependable(self, item, raw)
415 | self.myItems[index] = newv
416 | if 0 <= index < len(self.myItems):
417 | if raw or isImmutable(item):
418 | self.myItems[index].val = item
419 | return
420 | else:
421 | self.myItems[index].modified()
422 | self.myMainDep.modified()
423 |
424 | def getDependency(self, index):
425 | return self.myItems[index]
426 |
427 | def __iter__(self):
428 | self.myMainDep.val # dummy for dependency
429 | return iter([i.val for i in self.myItems])
430 |
431 | def clear(self):
432 | self.myItems.clear()
433 | self.myMainDep.modified()
434 |
435 | def __delitem__(self, index):
436 | del self.myItems[index]
437 | for i in range(index, len(self.myItems)):
438 | self.myItems[i].modified()
439 | self.myMainDep.modified()
440 |
441 | # def pop(self, *args, **kwargs):
442 | # self.myMainDep.modified()
443 | # return self.myItems.pop(*args, **kwargs)
444 |
445 | class DependSet(DependMixin, MutableSet):
446 | """
447 | DependSet is a bit different in that we don't need to convert items inside
448 | to dependencies.
449 | """
450 |
451 | def __init__(self, *args, **kwargs):
452 | DependMixin.__init__(self)
453 | self.myItems = set(*args, **kwargs)
454 |
455 | @property
456 | def val(self):
457 | return self
458 |
459 | @val.setter
460 | def val(self, value):
461 | try:
462 | self.clear()
463 | self.myItems = value
464 | self.myMainDep.modified()
465 | except:
466 | print("DependSet.val set to bad value")
467 |
468 | def getRaw(self):
469 | self.myMainDep.val
470 | return self.myItems
471 |
472 | def add(self, item):
473 | if item in self.myItems:
474 | return
475 | self.myItems.add(item)
476 | self.myMainDep.modified()
477 |
478 | def discard(self, item):
479 | if item not in self.myItems:
480 | return
481 | self.myItems.discard(item)
482 | self.myMainDep.modified()
483 |
484 | def update(self, *args, **kwargs):
485 | self.myItems.update(*args, **kwargs)
486 | self.myMainDep.modified()
487 |
488 | def clear(self):
489 | self.myItems.clear()
490 | self.myMainDep.modified()
491 |
492 | def union(self, *args):
493 | return self.myItems.union(*args)
494 |
495 | def difference(self, *args):
496 | return self.myItems.difference(*args)
497 |
498 | def intersection(self, *args):
499 | return self.myItems.intersection(*args)
500 |
501 | def symmetric_difference(self, *args):
502 | return self.myItems.symmetric_difference(*args)
503 |
504 | def issuperset(self, *args):
505 | return self.myItems.issuperset(*args)
506 |
507 | def isImmutable(item):
508 | if item is None:
509 | return True
510 | if isinstance(item, Number):
511 | return True
512 | if type(item) in (str, tuple, frozenset):
513 | return True
514 | return False
515 |
516 |
517 | def makeDependable(parentDep, value, raw=False):
518 | if raw and isinstance(value, (dict, list, set)):
519 | newv = value
520 | elif isinstance(value, dict):
521 | newv = DependDict(value)
522 | newv.parentDep = parentDep
523 | elif isinstance(value, list):
524 | newv = DependList(value)
525 | newv.parentDep = parentDep
526 | elif isinstance(value, set):
527 | newv = DependSet(value)
528 | newv.parentDep = parentDep
529 | elif isinstance(value, DependMixin):
530 | value.parentDep = parentDep
531 | newv = value
532 | elif type(value).__name__ in ['DependDict', 'DependList', 'DependSet']:
533 | value.parentDep = parentDep
534 | newv = value
535 | elif isinstance(value, tdu.Dependency):
536 | if isinstance(value.val, DependMixin):
537 | value.val.parentDep = parentDep
538 | elif type(value.val).__name__ in \
539 | ['DependDict', 'DependList', 'DependSet']:
540 | value.val.parentDep = parentDep
541 | return value
542 | else:
543 | newv = value
544 | if hasattr(newv, '_TDParentDep'):
545 | newv._TDParentDep = parentDep
546 | return tdu.Dependency(newv)
547 |
548 |
549 | def isImmutable(item):
550 | if item is None:
551 | return True
552 | if isinstance(item, Number):
553 | return True
554 | if type(item) in (str, tuple, frozenset):
555 | return True
556 | return False
557 |
--------------------------------------------------------------------------------
/lib/_stubs/Updater.py:
--------------------------------------------------------------------------------
1 | """
2 | Extension classes enhance TouchDesigner components with python. An
3 | extension is accessed via ext.ExtensionClassName from any operator
4 | within the extended component. If the extension is promoted via its
5 | Promote Extension parameter, all its attributes with capitalized names
6 | can be accessed externally, e.g. op('yourComp').PromotedFunction().
7 |
8 | Help: search "Extensions" in wiki
9 | """
10 |
11 | from distutils.version import LooseVersion
12 |
13 | from _stubs import *
14 | from .TDStoreTools import StorageManager
15 | TDF = op.TDModules.mod.TDFunctions
16 | popDialog = op.TDResources.op('popDialog')
17 |
18 | class Updater:
19 | """
20 | Updater description
21 | """
22 | def __init__(self, ownerComp):
23 | # The component to which this extension is attached
24 | self.ownerComp = ownerComp
25 | self.progressDialog = ownerComp.op('progressDialog')
26 | self.updateInfo = {}
27 | TDF.createProperty(self, 'UpdatesRemaining', value=0, dependable=True)
28 | TDF.createProperty(self, 'UpdatesQueued', value=0, dependable=True)
29 | TDF.createProperty(self, 'CurrentUpdateComp', value='', dependable=True)
30 |
31 | def onUpdateCompParValueChange(self, par, prev, updater):
32 | pass
33 |
34 | def onUpdateCompParPulse(self, par, updater):
35 | if par.name == 'Update':
36 | self.Update(par.owner, updater)
37 |
38 | def AbortUpdates(self):
39 | self.updateInfo = {}
40 | self.UpdatesRemaining = self.UpdatesQueued = 0
41 | print("*** Update Aborted ***")
42 |
43 | def Update(self, comps, updater=None, versionCheck='dialog',
44 | preUpdateMethod=None, postUpdateMethod=None,
45 | updateMethod=None):
46 | """
47 | Update a component or components. Updating will pulse clone and set the
48 | component's version to the version on the clone source.
49 |
50 | :param comps: a single component or a list of components
51 | :param updater: the TDUpdateSystem component
52 | :param versionCheck: defines versioning behavior. 'dialog' = opens
53 | warning dialog if clone source version is lower, True =
54 | does not update if clone source version is lower, False = ignore
55 | versions
56 | :param preUpdateMethod: will be called before update with a single
57 | argument containing a dictionary of info
58 | :param postUpdateMethod: will be called after update with a single
59 | argument containing a dictionary of info
60 | :param updateMethod: will be called instead of standard update with a
61 | single argument containing a dictionary of info
62 | :return:
63 | """
64 | if versionCheck not in ['dialog', True, False]:
65 | raise ValueError('versionCheck must be True, False, or "dialog"')
66 | info = self.updateInfo
67 | if isinstance(comps, COMP):
68 | comps = [comps]
69 | self.UpdatesRemaining += len(comps)
70 | self.UpdatesQueued += len(comps)
71 | if self.UpdatesQueued > 1:
72 | self.progressDialog.Open()
73 | updateInfo = {'queuedComps': comps,
74 | 'versionCheck': versionCheck,
75 | 'preUpdateMethod': preUpdateMethod,
76 | 'postUpdateMethod': postUpdateMethod,
77 | 'updateMethod': updateMethod,
78 | 'updater': updater}
79 | if info and info['queuedComps']:
80 | info['queuedInfo'].append(updateInfo)
81 | else:
82 | self.updateInfo = {'queuedComps': comps,
83 | 'versionCheck': versionCheck,
84 | 'preUpdateMethod': preUpdateMethod,
85 | 'postUpdateMethod': postUpdateMethod,
86 | 'updateMethod': updateMethod,
87 | 'updater': updater,
88 | 'queuedInfo': []}
89 | self.doNextUpdate()
90 |
91 | def doNextUpdate(self):
92 | info = self.updateInfo
93 | if not info:
94 | self.endUpdates()
95 | return
96 | comp = info['queuedComps'][0]
97 | self.CurrentUpdateComp = comp.path
98 | info['comp'] = comp
99 | if not isinstance(comp, COMP):
100 | raise TypeError("Invalid object sent to Update: " + str(comp))
101 | elif not comp.par.Update.enable:
102 | print('Skip update for', comp.path + '. Update disabled.\n')
103 | self.doNextUpdate()
104 | return
105 | else:
106 | if info['versionCheck'] is not False:
107 | source = comp.par.clone.eval()
108 | if source == comp:
109 | print("Can't update", comp.path + ". Component is it's own "
110 | "clone source.")
111 | while comp in info['queuedComps']:
112 | info['queuedComps'].remove(comp)
113 | if not info['queuedComps']:
114 | self.endUpdates()
115 | return
116 | self.doNextUpdate()
117 | return
118 | if not source:
119 | sourceMessage = \
120 | info['updater'].par.Invalidsourcemessage.eval()\
121 | if info['updater'] else ''
122 | errorMessage = "Invalid clone source for " + comp.path +'.'
123 | if sourceMessage:
124 | errorMessage += ' ' + sourceMessage
125 | print(errorMessage)
126 | self.OpenErrorDialog()
127 | self.doNextUpdate()
128 | return
129 | oldVersion = comp.par.Version.eval()
130 | newVersion = source.par.Version.eval()
131 | if LooseVersion(oldVersion) > LooseVersion(newVersion):
132 | if info['versionCheck'] is True:
133 | self.versionSkip()
134 | return
135 | else:
136 | op.TDResources.op('popDialog').Open(
137 | text='Clone source for ' + comp.path +
138 | ' has lower version. Update anyway?',
139 | title='Lower Version',
140 | buttons=['Yes', 'Yes All', 'No', 'No All'],
141 | callback=self.onVersionDialog,
142 | textEntry=False,
143 | escButton=3,
144 | enterButton=3,
145 | escOnClickAway=True
146 | )
147 | return
148 | self.startUpdate()
149 |
150 | def OpenErrorDialog(self):
151 | popDialog.OpenDefault(
152 | "There were errors running Update. See textport for details.",
153 | "Update Error",
154 | ["OK"])
155 |
156 | def versionSkip(self):
157 | print('Skip update for',
158 | self.updateInfo['comp'].path + '. Newer than source.\n')
159 | self.doNextUpdate()
160 |
161 | def onVersionDialog(self, info):
162 | if info['button'] == 'Yes':
163 | self.startUpdate()
164 | elif info['button'] == 'Yes All':
165 | self.updateInfo['versionCheck'] = False
166 | self.startUpdate()
167 | elif info['button'] == 'No':
168 | self.versionSkip()
169 | elif info['button'] == 'No All':
170 | self.updateInfo['versionCheck'] = True
171 | self.versionSkip()
172 |
173 | def startUpdate(self):
174 | info = self.updateInfo
175 | comp = info['comp']
176 | print('Updating', comp.path)
177 | if info['preUpdateMethod']:
178 | info['preUpdateMethod'](info)
179 | if info['updateMethod']:
180 | info['updateMethod'](info)
181 | else:
182 | self.updateMethod()
183 | if info['postUpdateMethod']:
184 | info['postUpdateMethod'](info)
185 | run('op(' + str(self.ownerComp.id) +
186 | ').ext.Updater.updateComplete()', delayFrames=1,
187 | delayRef=op.TDResources)
188 |
189 | def updateMethod(self):
190 | info = self.updateInfo
191 | comp = info['comp']
192 | info['preUpdateAllowCooking'] = comp.allowCooking
193 | comp.allowCooking = False
194 | comp.par.enablecloningpulse.pulse()
195 | if comp.pars('Version') and comp.par.clone.eval().pars('Version'):
196 | comp.par.Version = comp.par.clone.eval().par.Version.eval()
197 |
198 | def endUpdates(self):
199 | self.progressDialog.Close()
200 | self.UpdatesRemaining = 0
201 | self.UpdatesQueued = 0
202 | self.updateInfo = []
203 |
204 | def updateComplete(self):
205 | if not self.updateInfo or not self.UpdatesRemaining:
206 | self.endUpdates()
207 | return
208 | self.UpdatesRemaining -= 1
209 |
210 | info = self.updateInfo
211 | if 'preUpdateAllowCooking' in info:
212 | info['comp'].allowCooking = info['preUpdateAllowCooking']
213 | print(' Update complete.\n')
214 | while info['comp'] in info['queuedComps']:
215 | info['queuedComps'].remove(info['comp'])
216 | if info['queuedInfo']:
217 | newInfo = info['queuedInfo'].pop(0)
218 | info.update(newInfo)
219 | if info['queuedComps']:
220 | run('op(' + str(self.ownerComp.id) +
221 | ').ext.Updater.doNextUpdate()', delayFrames=1,
222 | delayRef=op.TDResources)
223 | else:
224 | self.endUpdates()
225 |
226 |
227 | def onUpdateSystemParValueChange(self, par, prev):
228 | system = par.owner
229 | comp = system.par.Comptoupdate.eval()
230 | if par.name == 'Enableupdatesystem':
231 | if comp.pars('Update'):
232 | comp.par.Update.enable = par.eval()
233 |
234 | def onUpdateSystemParPulse(self, par):
235 | system = par.owner
236 | comp = system.par.Comptoupdate.eval()
237 | if par.name == 'Setupparameters':
238 | self.SetupUpdateParameters(comp)
239 | elif par.name == 'Update':
240 | self.Update(comp)
241 |
242 | def SetupUpdateParameters(self, comp):
243 | """
244 | Add Version and Update parameters to comp, if they aren't there already.
245 | They will be added to "Update" page, which will also be created, if
246 | necessary.
247 |
248 | :param comp: Component to add parameters to.
249 | :return:
250 | """
251 | if isinstance(comp, COMP):
252 | if comp.pars('Version'):
253 | if not comp.par.Version.isString:
254 | raise TypeError('"Version" parameter not a string')
255 | else:
256 | if 'Update' not in comp.customPages:
257 | page = comp.appendCustomPage('Update')
258 | else:
259 | page = next((p for p in comp.customPages
260 | if p.name == 'Update'))
261 | page.appendStr('Version')
262 | comp.par.Version = '0.1'
263 | comp.par.Version.readOnly = True
264 | if comp.pars('Update'):
265 | if not comp.par.Update.isPulse:
266 | raise TypeError('"Update" parameter not a pulse')
267 | else:
268 | if 'Update' not in comp.customPages:
269 | page = comp.appendCustomPage('Update')
270 | else:
271 | page = next((p for p in comp.customPages
272 | if p.name == 'Update'))
273 | page.appendPulse('Update')
274 | print('Added Update parameters to', comp)
275 | else:
276 | raise TypeError('Can only setup parameters on COMP. Passed: ',
277 | str(comp))
278 |
--------------------------------------------------------------------------------
/lib/tdcomponents_resolve.py:
--------------------------------------------------------------------------------
1 | import os.path
2 |
3 | def file(path):
4 | return path if os.path.exists(path) else ''
5 |
6 | def modpath(*oppaths, checkprefix=None):
7 | for o in ops(*oppaths):
8 | if o.isDAT and o.isText and o.text and (not checkprefix or o.text.startswith(checkprefix)):
9 | return o.path
10 | return ''
11 |
--------------------------------------------------------------------------------
/loggly_logger.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/loggly_logger.tox
--------------------------------------------------------------------------------
/mapper.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mapper.tox
--------------------------------------------------------------------------------
/midimap.py:
--------------------------------------------------------------------------------
1 | import typing as T
2 |
3 | print('midimap.py loading...')
4 |
5 | if False:
6 | from _stubs import *
7 |
8 | class MidiMapper:
9 | def __init__(self, ownerComp):
10 | self.ownerComp = ownerComp
11 | self.mappings = [] # type: T.List[Mapping]
12 | self.LoadMapTable()
13 |
14 | @property
15 | def _ExternalMapTable(self):
16 | return self.ownerComp.par.Maptable.eval()
17 |
18 | @property
19 | def _InnerMapTable(self):
20 | return self.ownerComp.op('set_map_table')
21 |
22 | def LoadMapTable(self):
23 | self.mappings.clear()
24 | dat = self._ExternalMapTable
25 | if dat and dat.numRows:
26 | for i in range(1, dat.numRows):
27 | m = Mapping()
28 | m.ReadRow(dat, i)
29 | self.mappings.append(m)
30 | self._WriteMapTable(self._InnerMapTable)
31 | self._BuildActiveMappingTables()
32 |
33 | def SaveMapTable(self):
34 | dat = self._ExternalMapTable
35 | if not dat:
36 | raise Exception('No map table to write to!')
37 | self._WriteMapTable(dat)
38 |
39 | def _WriteMapTable(self, dat):
40 | dat.clear()
41 | dat.appendRow(Mapping.colnames)
42 | for m in self.mappings:
43 | m.WriteRow(dat)
44 |
45 | def _BuildActiveMappingTables(self):
46 | dat = self.ownerComp.op('set_active_mappings')
47 | dat.clear()
48 | dat.appendRow(['param', 'inchan', 'outchan', 'low', 'high', 'path', 'parname'])
49 | mappingsbyop = {} # type: T.Dict[str, T.List[Mapping]]
50 | for m in self.mappings:
51 | if not m.enable or not m.path or not m.param or m.cc in ('', None):
52 | continue
53 | o = op(m.path)
54 | if not o:
55 | continue
56 | p = getattr(o.par, m.param, None)
57 | if p is None:
58 | continue
59 | if not (p.isNumber or p.isToggle or p.isMenu):
60 | continue
61 | dat.appendRow([
62 | '{}:{}'.format(o.path, m.param),
63 | 'cc{}'.format(m.cc),
64 | 'ch1c{}'.format(m.cc),
65 | _format(m.rangelow if m.rangelow is not None else p.normMin),
66 | _format(m.rangehigh if m.rangehigh is not None else p.normMax),
67 | o.path,
68 | m.param,
69 | ])
70 | if o.path not in mappingsbyop:
71 | mappingsbyop[o.path] = []
72 | mappingsbyop[o.path].append(m)
73 | opsdat = self.ownerComp.op('set_mapped_ops')
74 | opsdat.clear()
75 | opsdat.appendRow(['path', 'params', 'fullparams', 'inchans', 'outchans'])
76 | for path in sorted(mappingsbyop.keys()):
77 | omaps = mappingsbyop[path]
78 | opsdat.appendRow([
79 | path,
80 | ' '.join([m.param for m in omaps]),
81 | ' '.join(['{}:{}'.format(path, m.param) for m in omaps]),
82 | ' '.join(['cc{}'.format(m.cc) for m in omaps]),
83 | ' '.join(['ch1c{}'.format(m.cc) for m in omaps]),
84 | ])
85 |
86 | def HandleDrop(self, args):
87 | pass
88 |
89 | def _HandleDrop(self, args):
90 | pass
91 |
92 | @staticmethod
93 | def PrepareDevices(dat):
94 | dat.appendCol(['label'])
95 | for i in range(1, dat.numRows):
96 | indev = dat[i, 'indevice']
97 | outdev = dat[i, 'outdevice']
98 | if indev == outdev:
99 | dat[i, 'label'] = indev
100 | else:
101 | dat[i, 'label'] = '{} / {}'.format(indev, outdev)
102 | for i in range(dat.numRows, 17):
103 | dat.appendRow([i])
104 | name = '({})'.format(i)
105 | dat[i, 'indevice'] = name
106 | dat[i, 'outdevice'] = name
107 | dat[i, 'label'] = name + ' (unknown)'
108 | dat[i, 'channel'] = 1
109 |
110 | class Mapping:
111 | def __init__(
112 | self,
113 | path=None,
114 | param=None,
115 | enable=True,
116 | rangelow=None,
117 | rangehigh=None,
118 | cc=None):
119 | self.path = path
120 | self.param = param
121 | self.enable = enable
122 | self.rangelow = rangelow
123 | self.rangehigh = rangehigh
124 | self.cc = cc
125 |
126 | colnames = ['path', 'param', 'enable', 'low', 'high', 'cc']
127 |
128 | def ReadRow(self, dat, i):
129 | self.path = _parse(dat[i, 'path'], str)
130 | self.param = _parse(dat[i, 'param'], str)
131 | self.enable = _parse(dat[i, 'enable'], bool, False)
132 | self.rangelow = _parse(dat[i, 'low'], float)
133 | self.rangehigh = _parse(dat[i, 'high'], float)
134 | self.cc = _parse(dat[i, 'cc'], int)
135 |
136 | def WriteRow(self, dat):
137 | i = dat.numRows
138 | dat.appendRow([])
139 | for c in Mapping.colnames:
140 | if dat.col(c) is None:
141 | dat.appendCol([c])
142 | dat[i, 'path'] = _format(self.path)
143 | dat[i, 'param'] = _format(self.param)
144 | dat[i, 'enable'] = _format(self.enable)
145 | dat[i, 'low'] = _format(self.rangelow)
146 | dat[i, 'high'] = _format(self.rangehigh)
147 | dat[i, 'cc'] = _format(self.cc)
148 |
149 | def _parse(val, t, default=None):
150 | if val in ('', None):
151 | return default
152 | try:
153 | return t(val)
154 | except ValueError:
155 | return default
156 |
157 | def _format(val):
158 | if val is None:
159 | return ''
160 | if isinstance(val, bool):
161 | return int(val)
162 | if isinstance(val, (int, float)):
163 | if val == int(val):
164 | return int(val)
165 | return val
166 |
--------------------------------------------------------------------------------
/mixer/MixerSourcesExt.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | import dataclasses
3 | from typing import Any, Dict, Iterable, List, Optional, Union
4 |
5 | # noinspection PyUnreachableCode
6 | if False:
7 | # noinspection PyUnresolvedReferences
8 | from _stubs import *
9 |
10 | class SourceTrack:
11 | def __init__(self, ownerComp):
12 | self.ownerComp = ownerComp
13 |
14 | @property
15 | def SourcesTable(self):
16 | o = self.ownerComp.par.Sources.eval()
17 | if not o:
18 | return None
19 | if o.isDAT:
20 | return o
21 | if not o.isCOMP:
22 | return None
23 | if hasattr(o.par, 'Sourcetable'):
24 | src = o.par.Sourcetable.eval()
25 | if src and src.isDAT:
26 | return src
27 | src = o.op('sources')
28 | if src and src.isDAT:
29 | return src
30 | return None
31 |
32 | def PrepareOpSources(self, dat: 'DAT', paths: Iterable[Union[str, 'OP']]):
33 | _prepareOpSources(dat, paths, removeComp=self.ownerComp)
34 |
35 | def GetSourceButtonLabels(self, wrapcount=None):
36 | sources = self.ownerComp.op('sources') # type: DAT
37 | labels = [
38 | str(sources[i, 'label'] or sources[i, 'shortName'] or sources[i, 'legalName'] or '-')
39 | for i in range(1, sources.numRows)
40 | ]
41 | if wrapcount is None or wrapcount <= 0:
42 | return labels
43 | return [_wrapString(s, wrapcount) for s in labels]
44 |
45 | def _wrapString(s, wraplen):
46 | strlen = len(s)
47 | if strlen <= wraplen:
48 | return s
49 | return r'\n'.join([s[i:i + wraplen] for i in range(0, strlen, wraplen)])
50 |
51 | class MixerSources:
52 | def __init__(self, ownerComp):
53 | self.ownerComp = ownerComp
54 |
55 | @staticmethod
56 | def PrepareOpSources(dat: 'DAT', paths: Iterable[Union[str, 'OP']]):
57 | _prepareOpSources(dat, paths)
58 |
59 | def _prepareOpSources(dat: 'DAT', paths: Iterable[Union[str, 'OP']], removeComp: Optional['OP'] = None):
60 | dat.clear()
61 | dat.appendRow(_sourceColumns)
62 | if not paths:
63 | return
64 | srcops = ops(*paths)
65 | for o in srcops:
66 | src = _Source(
67 | path=o.path,
68 | legalName=o.name,
69 | type='OP',
70 | )
71 | src.label = _firstStringPar(o, 'Label', 'Uilabel', 'Name') or o.name
72 | src.shortName = _firstStringPar(o, 'Shortlabel', 'Shortname') or src.label
73 | if o.isTOP:
74 | src.videoPath = o.path
75 | src.compPath = o.parent().path
76 | elif o.isCOMP:
77 | vidop = _getCompVideoSource(o)
78 | src.videoPath = vidop.path if vidop else None
79 | src.compPath = o.path
80 | else:
81 | continue
82 | if removeComp and src.compPath == removeComp.path:
83 | src.compPath = ''
84 | src.appendToRow(dat)
85 |
86 | def _getCompVideoSource(o):
87 | if o and o.isCOMP:
88 | for pattern in [
89 | 'out1',
90 | 'video_out',
91 | '*_out',
92 | 'out*',
93 | '*out',
94 | ]:
95 | for out in o.ops(pattern):
96 | if out.isTOP:
97 | return out
98 | pass
99 | return None
100 |
101 | def _firstStringPar(o, *names):
102 | if not o:
103 | return None
104 | for par in o.pars(*names):
105 | if par:
106 | return par.eval()
107 |
108 | @dataclass
109 | class _Source:
110 | path: str = None
111 | legalName: str = None
112 | sourceName: str = None
113 | shortName: str = None
114 | label: str = None
115 | compPath: str = None
116 | videoPath: str = None
117 | type: str = None
118 | deviceName: str = None
119 | active: bool = None
120 | streaming: bool = None
121 | fps: int = None
122 | url: str = None
123 | extraFields: Dict[str, Any] = None
124 |
125 | def appendToRow(self, dat: 'DAT'):
126 | data = dict(dataclasses.asdict(self))
127 | if 'extraFields' in data:
128 | del data['extraFields']
129 | if self.extraFields:
130 | data.update(self.extraFields)
131 | addDictRow(dat, data)
132 |
133 | _sourceColumns = [f.name for f in dataclasses.fields(_Source) if f.name != 'extraFields']
134 |
135 |
136 | NULL_PLACEHOLDER = '_'
137 |
138 | def formatValue(val, nonevalue=NULL_PLACEHOLDER):
139 | if isinstance(val, str):
140 | return val
141 | if val is None:
142 | return nonevalue
143 | if isinstance(val, bool):
144 | return str(int(val))
145 | if isinstance(val, float) and int(val) == val:
146 | return str(int(val))
147 | return str(val)
148 |
149 | def addDictRow(dat, obj: Dict[str, Any]):
150 | r = dat.numRows
151 | dat.appendRow([])
152 | setDictRow(dat, r, obj)
153 |
154 | def setDictRow(dat, rowkey: Union[str, int], obj: Dict[str, Any], clearmissing=False):
155 | for key, val in obj.items():
156 | dat[rowkey, key] = formatValue(val, nonevalue='')
157 | if clearmissing:
158 | for col in dat.row(0):
159 | if col.val not in obj:
160 | col.val = ''
161 |
--------------------------------------------------------------------------------
/mixer/mixer_sources.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/mixer_sources.tox
--------------------------------------------------------------------------------
/mixer/source_track.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/source_track.tox
--------------------------------------------------------------------------------
/mixer/track_mixer.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/track_mixer.tox
--------------------------------------------------------------------------------
/mixer/videoSenderCore.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/videoSenderCore.tox
--------------------------------------------------------------------------------
/mixer/video_sender.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/mixer/video_sender.tox
--------------------------------------------------------------------------------
/multi_logger.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/multi_logger.tox
--------------------------------------------------------------------------------
/rack.toe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack.toe
--------------------------------------------------------------------------------
/rack/component_editor/ComponentEditorExt.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | # noinspection PyUnreachableCode
4 | if False:
5 | # noinspection PyUnresolvedReferences
6 | from _stubs import *
7 | from typing import Any
8 | iop.hostedComp = COMP()
9 | ipar.compEditor = Any()
10 | ipar.workspace = Any()
11 |
12 | class ComponentEditor:
13 | def __init__(self, ownerComp):
14 | self.ownerComp = ownerComp
15 |
16 | def LoadComponent(self):
17 | comp = iop.hostedComp
18 | tox = ipar.compEditor.Toxfile.eval()
19 | if not tox:
20 | comp.par.externaltox = ''
21 | comp.destroyCustomPars()
22 | for child in list(comp.children):
23 | if child.valid:
24 | child.destroy()
25 | else:
26 | msg = f'Loading component {tdu.expandPath(tox)}'
27 | print(msg)
28 | ui.status = msg
29 | # comp = comp.loadTox(tox, unwired=True)
30 | comp.par.externaltox = tox
31 | comp.par.reinitnet.pulse()
32 | comp = iop.hostedComp
33 | if comp.isPanel:
34 | panel = self.ownerComp.op('component_ui_panel') # type: PanelCOMP
35 | if comp not in panel.panelChildren:
36 | panel.outputCOMPConnectors[0].connect(comp)
37 |
38 | def SaveComponent(self):
39 | comp = iop.hostedComp
40 | tox = ipar.compEditor.Toxfile.eval()
41 | if not tox:
42 | # TODO: prompt for a file
43 | ui.status = 'WARNING: UNABLE TO SAVE COMPONENT, NO TOX FILE'
44 | return
45 | tox = tdu.expandPath(tox)
46 | comp.save(tox, createFolders=False)
47 | msg = f'Saved component to {tox}'
48 | print(msg)
49 | ui.status = msg
50 | if ipar.workspace.Savethumbnails:
51 | img = ipar.compEditor.Thumbfile.eval()
52 | if not img:
53 | img = re.sub(r'\.tox$', '.png', tox)
54 | self.ownerComp.op('video_output').save(img)
55 |
56 | @staticmethod
57 | def CustomizeComponent():
58 | comp = iop.hostedComp
59 | ui.openCOMPEditor(comp)
60 |
61 | @staticmethod
62 | def ShowNetwork(useActive=True):
63 | comp = iop.hostedComp
64 | pane = None
65 | if useActive:
66 | pane = _GetActiveEditor()
67 | if not pane:
68 | pane = _GetPaneByName('compeditor')
69 | if not pane:
70 | pane = ui.panes.createFloating(type=PaneType.NETWORKEDITOR, name='compeditor')
71 | pane.owner = comp
72 |
73 | @staticmethod
74 | def FindVideoOutput():
75 | comp = iop.hostedComp
76 | o = comp.op('video_out') or comp.op('out1')
77 | if o and o.isTOP:
78 | return o
79 | for o in comp.findChildren(type=outTOP, depth=1):
80 | return o
81 |
82 | @staticmethod
83 | def FindAudioOutput():
84 | comp = iop.hostedComp
85 | for name in ['audio_out', 'out1', 'out2']:
86 | o = comp.op(name)
87 | if o and o.isCHOP:
88 | return o
89 | for o in comp.findChildren(type=outCHOP, depth=1):
90 | return o
91 |
92 | def Savecomponent(self, par):
93 | self.SaveComponent()
94 |
95 | def Loadcomponent(self, par):
96 | self.LoadComponent()
97 |
98 | def _GetActiveEditor():
99 | pane = ui.panes.current
100 | if pane.type == PaneType.NETWORKEDITOR:
101 | return pane
102 | for pane in ui.panes:
103 | if pane.type == PaneType.NETWORKEDITOR:
104 | return pane
105 |
106 | def _GetPaneByName(name):
107 | for pane in ui.panes:
108 | if pane.name == name:
109 | return pane
110 |
--------------------------------------------------------------------------------
/rack/component_editor/component_editor.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/component_editor/component_editor.tox
--------------------------------------------------------------------------------
/rack/component_picker/ComponentPickerExt.py:
--------------------------------------------------------------------------------
1 | import re
2 | from datetime import datetime
3 |
4 | # noinspection PyUnreachableCode
5 | if False:
6 | # noinspection PyUnresolvedReferences
7 | from _stubs import *
8 |
9 | class ComponentPicker:
10 | def __init__(self, ownerComp):
11 | self.ownerComp = ownerComp
12 | self.statePar = ownerComp.op('iparpicker').par
13 |
14 | @staticmethod
15 | def BuildComponentTable(dat: 'DAT', files: 'DAT'):
16 | dat.clear()
17 | dat.appendRow(['relpath', 'name', 'modified', 'timestamp', 'tox', 'thumb', 'folder'])
18 | for i in range(1, files.numRows):
19 | if files[i, 'extension'] != 'tox':
20 | continue
21 | relPath = files[i, 'relpath'].val
22 | thumbPath = _findThumbPath(files, relPath)
23 | timestamp = int(files[i, 'datemodified'] or 0)
24 | if not timestamp:
25 | modified = ''
26 | else:
27 | modified = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M')
28 | folder = files[i, 'folder'].val
29 | dat.appendRow([
30 | relPath,
31 | re.sub(r'([\w_ ]+)/\1\.tox$', r'\1', relPath),
32 | modified,
33 | timestamp,
34 | tdu.collapsePath(files[i, 'path'].val),
35 | tdu.collapsePath(thumbPath) if thumbPath else '',
36 | tdu.collapsePath(folder) if folder else '',
37 | ])
38 |
39 | def UpdateListFromPar(self, par: 'Par' = None):
40 | if par is None:
41 | par = self.ownerComp.par.Selectedcomp
42 | listWidget = self.ownerComp.op('component_list')
43 | if not par:
44 | index = None
45 | else:
46 | cell = self.ownerComp.op('component_table')[par.eval(), 'relpath']
47 | index = cell.row if cell is not None else None
48 | listWidget.par.Selectedrows = index if index is not None else ''
49 |
50 | def Refreshpulse(self, par):
51 | self.ownerComp.op('folder').par.refreshpulse.pulse()
52 |
53 | def Selectedcomp(self, par, val, prev):
54 | self.UpdateListFromPar(par)
55 |
56 | def OnSelectRow(self, info: dict):
57 | rowData = info.get('rowData') or {}
58 | rowObject = rowData.get('rowObject') or {}
59 | self.ownerComp.par.Selectedcomp = rowObject.get('relpath') or ''
60 | self.ownerComp.par.Onitemclick.pulse()
61 |
62 | def OnListRollover(self, info: dict):
63 | self.statePar.Hoverrow = info.get('row', -1)
64 |
65 | def _findThumbPath(files: 'DAT', toxPath: str):
66 | toxPathBase = re.sub('.tox$', '', toxPath)
67 | for path in files.col('relpath')[1:]:
68 | extension = files[path, 'extension'].val
69 | if extension not in tdu.fileTypes['image']:
70 | continue
71 | basePath = re.sub('.' + extension, '', path.val)
72 | if basePath == toxPathBase:
73 | return files[path.row, 'path'].val
74 | if basePath == toxPathBase.rsplit('/', maxsplit=1)[0] + '/thumb':
75 | return files[path.row, 'path'].val
76 |
77 | def OnStart():
78 | run(f'op({parent.picker.path!r}).UpdateListFromPar()', delayFrames=2)
79 |
80 | def OnCreate():
81 | OnStart()
82 |
--------------------------------------------------------------------------------
/rack/component_picker/component_picker.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/component_picker/component_picker.tox
--------------------------------------------------------------------------------
/rack/editor/components/EditorCommon.py:
--------------------------------------------------------------------------------
1 | import dataclasses
2 | from dataclasses import dataclass
3 | from typing import Any, Callable, Dict, List, Optional, Union
4 |
5 | # noinspection PyUnreachableCode
6 | if False:
7 | # noinspection PyUnresolvedReferences
8 | from _stubs import *
9 |
10 | ValueOrExpr = Union[Any, Dict[str, str]]
11 |
12 | @dataclass
13 | class LibrarySpec:
14 | shortcut: str
15 | path: str
16 | allPaths: str
17 |
18 | @dataclass
19 | class ComponentSpec:
20 | name: Optional[str] = None
21 | label: Optional[str] = None
22 | tox: Optional[str] = None
23 | copyOf: Optional[ValueOrExpr] = None
24 | pars: Optional[Dict[str, ValueOrExpr]] = None
25 |
26 | @classmethod
27 | def fromDict(cls, d: dict):
28 | return cls(**d)
29 |
30 | def toDict(self):
31 | return dataclasses.asdict(self)
32 |
33 | def createComp(self, destination: COMP) -> COMP:
34 | if self.copyOf:
35 | if isinstance(self.copyOf, Dict):
36 | master = destination.evalExpression(self.copyOf['$'])
37 | else:
38 | master = destination.op(self.copyOf)
39 | if not master:
40 | raise Exception(f'Invalid component spec {self!r}')
41 | comp = destination.copy(master, name=self.name)
42 | elif self.tox:
43 | comp = destination.loadTox(self.tox)
44 | else:
45 | raise Exception(f'Invalid component spec {self!r}')
46 | if self.name:
47 | comp.name = self.name
48 | # in case the name need to change for uniqueness, this will store the actual name
49 | self.name = comp.name
50 | self.applyParams(comp)
51 | return comp
52 |
53 | def applyParams(self, comp: COMP):
54 | if not self.pars:
55 | return
56 | for name, val in self.pars.items():
57 | par = getattr(comp.par, name, None)
58 | if par is None:
59 | print(f'Param {name} not found in {comp}')
60 | continue
61 | if isinstance(val, dict) and '$' in val:
62 | par.expr = val['$']
63 | else:
64 | par.val = val
65 |
66 | @dataclass
67 | class UITab:
68 | name: str
69 | label: str
70 | visible: bool = True
71 | icon: str = None
72 | componentName: str = None
73 | attrs: dict = None
74 |
75 | @classmethod
76 | def noneTab(cls):
77 | return cls(name='none', label='None', icon=chr(0xF156))
78 |
79 | @dataclass
80 | class UITabSet:
81 | tabs: List[UITab] = dataclasses.field(default_factory=list)
82 | hasNone: bool = False
83 | attrNames: List[str] = None
84 |
85 | def buildTable(self, dat: 'DAT'):
86 | dat.clear()
87 | dat.appendRow(['name', 'label', 'icon', 'componentName'] + (self.attrNames or []))
88 | for tab in self.tabs:
89 | if not tab.visible:
90 | continue
91 | vals = [tab.name or '', tab.label or '', tab.icon or '', tab.componentName or '']
92 | if self.attrNames:
93 | for name in self.attrNames:
94 | val = tab.attrs.get(name) if tab.attrs else None
95 | vals.append(val if val is not None else '')
96 | dat.appendRow(vals)
97 |
98 | def updateParMenu(self, par: 'Par'):
99 | names = []
100 | labels = []
101 | for tab in self.tabs:
102 | if tab.visible:
103 | names.append(tab.name)
104 | labels.append(tab.label)
105 | par.menuNames = names
106 | par.menuLabels = labels
107 |
--------------------------------------------------------------------------------
/rack/editor/components/EditorToolsExt.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, is_dataclass, asdict
2 | from typing import Callable, List, Union
3 |
4 | # noinspection PyUnreachableCode
5 | if False:
6 | # noinspection PyUnresolvedReferences
7 | from _stubs import *
8 | from ..EditorExt import Editor
9 | ext.editor = Editor(None)
10 | iop.hostedComp = COMP()
11 |
12 | @dataclass
13 | class ToolContext:
14 | toolName: str
15 | component: 'COMP'
16 | editorPane: 'NetworkEditor'
17 |
18 | @dataclass
19 | class _ToolDefinition:
20 | name: str
21 | action: Callable[[ToolContext], None]
22 | label: str = None
23 | icon: str = None
24 |
25 | @classmethod
26 | def parse(cls, spec: Union['_ToolDefinition', list, tuple, dict]) -> '_ToolDefinition':
27 | if isinstance(spec, _ToolDefinition):
28 | return spec
29 | if is_dataclass(spec):
30 | spec = asdict(spec)
31 | if isinstance(spec, dict):
32 | return cls(**spec)
33 | return cls(*spec)
34 |
35 | class EditorTools:
36 | def __init__(self, ownerComp: 'COMP'):
37 | self.ownerComp = ownerComp
38 | self.builtInTools = [] # type: List[_ToolDefinition]
39 | self.customTools = [] # type: List[_ToolDefinition]
40 |
41 | self.customToolsScript = self.ownerComp.op('customTools') # type: DAT
42 | self.toolTable = self.ownerComp.op('set_tool_table') # type: DAT
43 |
44 | self.initializeBuiltInTools()
45 | self.updateToolTable()
46 |
47 | def initializeBuiltInTools(self):
48 | self.builtInTools = [
49 | _ToolDefinition('saveComponent', lambda ctx: ext.editor.SaveComponent(), icon=chr(0xF193))
50 | ]
51 |
52 | def updateToolTable(self):
53 | self.toolTable.clear()
54 | self.toolTable.appendRow(['name', 'label', 'icon', 'category'])
55 | for tool in self.builtInTools:
56 | self.toolTable.appendRow([
57 | tool.name,
58 | tool.label or tool.name,
59 | tool.icon or '',
60 | 'builtIn'
61 | ])
62 | for tool in self.customTools:
63 | self.toolTable.appendRow([
64 | tool.name,
65 | tool.label or tool.name,
66 | tool.icon or '',
67 | 'custom'
68 | ])
69 |
70 | def ClearCustomTools(self):
71 | self.customToolsScript.clear()
72 | self.ownerComp.par.Customtoolscriptfile = ''
73 | self.ownerComp.par.Customtoolscript = ''
74 | self.updateToolTable()
75 |
76 | def LoadCustomTools(self):
77 | self.customTools.clear()
78 | self.customToolsScript.clear()
79 | file = self.ownerComp.par.Customtoolscriptfile.eval()
80 | if file:
81 | self.customToolsScript.par.file = file
82 | self.customToolsScript.par.loadonstartpulse.pulse()
83 | self.addCustomToolsFromScript(self.customToolsScript)
84 | self.addCustomToolsFromScript(self.ownerComp.par.Customtoolscript.eval())
85 | self.updateToolTable()
86 |
87 | def addCustomToolsFromScript(self, script: 'DAT'):
88 | if not script or not script.text:
89 | return
90 | try:
91 | m = script.module
92 | except Exception as e:
93 | print(self.ownerComp, f'ERROR loading custom tools script: {e}')
94 | return
95 | if not hasattr(m, 'getEditorTools'):
96 | print(self.ownerComp, 'ERROR: Custom tools script does not have `getEditorTools` function')
97 | return
98 | try:
99 | specs = m.getEditorTools()
100 | except Exception as e:
101 | print(self.ownerComp, f'ERROR loading custom tools script: {e}')
102 | return
103 | if not specs:
104 | return
105 | for spec in specs:
106 | try:
107 | tool = _ToolDefinition.parse(spec)
108 | except Exception as e:
109 | print(self.ownerComp, f'ERROR parsing custom tool spec: {spec!r}\n{e}')
110 | continue
111 | self.customTools.append(tool)
112 |
113 | def findTool(self, name: str):
114 | # custom tools take precedence over built-in tools
115 | for tool in self.customTools:
116 | if tool.name == name:
117 | return tool
118 | for tool in self.builtInTools:
119 | if tool.name == name:
120 | return tool
121 |
122 | def ExecuteTool(self, name: str):
123 | tool = self.findTool(name)
124 | if not tool:
125 | raise Exception(f'Editor tool not found: {name}')
126 | context = ToolContext(
127 | name,
128 | iop.hostedComp,
129 | ext.editor.GetActiveNetworkEditor())
130 | tool.action(context)
131 |
132 | def OnWorkspaceLoad(self):
133 | self.LoadCustomTools()
134 |
135 | def OnWorkspaceUnload(self):
136 | self.ClearCustomTools()
137 |
--------------------------------------------------------------------------------
/rack/editor/components/EditorViewsExt.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 | from .EditorCommon import ComponentSpec
3 |
4 | # noinspection PyUnreachableCode
5 | if False:
6 | # noinspection PyUnresolvedReferences
7 | from _stubs import *
8 |
9 | class EditorViews:
10 | def __init__(self, ownerComp):
11 | self.ownerComp = ownerComp # type: COMP
12 | self.customHolder = ownerComp.op('customViews') # type: COMP
13 |
14 | def LoadCustomViews(self):
15 | return
16 | self.ClearCustomViews()
17 | viewDicts = self.ownerComp.par.Customviews.eval()
18 | if not viewDicts:
19 | return
20 | viewSpecs = [ComponentSpec.fromDict(v) for v in viewDicts]
21 | # for i, viewSpec in enumerate(viewSpecs):
22 | # comp = viewSpec.createComp(self.customHolder)
23 | # comp.nodeY = 600 - (i * 150)
24 | # self.initializeCustomView(comp, viewSpec)
25 | self.updateCustomViewTable(viewSpecs)
26 |
27 | def initializeCustomView(self, comp: 'COMP', viewSpec: ComponentSpec):
28 | if not comp.isPanel:
29 | return
30 | try:
31 | comp.par.hmode = 'fill'
32 | comp.par.vmode = 'fill'
33 | comp.par.display.expr = f'parent.editorViews.par.Selectedview == {viewSpec.name!r}'
34 | except Exception as e:
35 | print(self.ownerComp, 'Error initializing custom view', comp, '\n', viewSpec, '\n', e)
36 |
37 | def ClearCustomViews(self):
38 | for o in self.customHolder.children:
39 | if not o or not o.valid:
40 | continue
41 | # try:
42 | o.destroy()
43 | # except:
44 | # pass
45 | self.updateCustomViewTable([])
46 |
47 | def OnWorkspaceUnload(self):
48 | self.ownerComp.par.Customviews = None
49 | self.ClearCustomViews()
50 |
51 | def OnWorkspaceLoad(self):
52 | self.LoadCustomViews()
53 |
54 | def updateCustomViewTable(self, viewSpecs: List[ComponentSpec]):
55 | dat = self.ownerComp.op('set_custom_view_table')
56 | dat.clear()
57 | dat.appendRow(['name', 'label'])
58 | if not viewSpecs:
59 | return
60 | for viewSpec in viewSpecs:
61 | dat.appendRow([viewSpec.name, viewSpec.label or viewSpec.name])
62 |
--------------------------------------------------------------------------------
/rack/editor/components/SettingsExt.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | from dataclasses import dataclass
3 | import json
4 | from pathlib import Path
5 | from typing import Any, List, Optional
6 |
7 | # noinspection PyUnreachableCode
8 | if False:
9 | # noinspection PyUnresolvedReferences
10 | from _stubs import *
11 |
12 | try:
13 | from TDStoreTools import DependList, DependDict
14 | except ImportError:
15 | from _stubs.TDStoreTools import DependList, DependDict
16 |
17 | class SettingsExtBase(ABC):
18 | @abstractmethod
19 | def getSettingsOps(self) -> List['SettingsOp']:
20 | pass
21 |
22 | def loadSettingsFile(self, filePath: Path):
23 | if filePath and filePath.exists():
24 | with filePath.open('r') as f:
25 | text = f.read()
26 | settings = json.loads(text or '{}')
27 | else:
28 | settings = {}
29 | self.applySettings(settings)
30 |
31 | def saveSettingsFile(self, filePath: Path):
32 | settings = self.buildSettings()
33 | with filePath.open('w') as f:
34 | json.dump(settings, f, indent=' ', cls=_CustomEncoder)
35 | return settings
36 |
37 | def applySettings(self, settings: dict):
38 | settingsOps = self.getSettingsOps()
39 | for settingsOp in settingsOps:
40 | settingsOp.applySettings(settings.get(settingsOp.name))
41 |
42 | def buildSettings(self):
43 | settingsOps = self.getSettingsOps()
44 | settings = {}
45 | for settingsOp in settingsOps:
46 | settings[settingsOp.name] = settingsOp.buildSettings()
47 | return settings
48 |
49 | @dataclass
50 | class SettingsOp:
51 | name: str
52 | op: Optional['OP'] = None
53 | # note that this is an OR combination, meaning that it is all the params that
54 | # are listed in `paramNames` plus all the params in all the pages listed in `paramPages`
55 | params: Optional[List[str]] = None
56 | pages: Optional[List[str]] = None
57 |
58 | @classmethod
59 | def fromDatRow(cls, dat: 'DAT', row):
60 | return cls(
61 | name=str(dat[row, 'name']),
62 | params=str(dat[row, 'params'] or '').split(' '),
63 | pages=str(dat[row, 'paramPages'] or '').split(' '),
64 | )
65 |
66 | def _getParams(self):
67 | if self.op:
68 | o = self.op
69 | else:
70 | o = getattr(iop, self.name, None)
71 | if not o:
72 | return []
73 | pars = []
74 | if self.params:
75 | for par in o.pars(*self.params):
76 | if self._isEligible(par):
77 | pars.append(par)
78 | if self.pages:
79 | for page in o.customPages:
80 | if page.name in self.pages:
81 | for par in page.pars:
82 | if par not in pars and self._isEligible(par):
83 | pars.append(par)
84 | return pars
85 |
86 | @staticmethod
87 | def _isEligible(par: 'Par'):
88 | if not par.enable or par.readOnly or not par.isCustom:
89 | return False
90 | if par.label.startswith('-'):
91 | return False
92 | if par.isPulse:
93 | return False
94 | return True
95 |
96 | def buildSettings(self):
97 | settings = {}
98 | for par in self._getParams():
99 | if par.mode == ParMode.CONSTANT:
100 | settings[par.name] = par.eval()
101 | elif par.mode == ParMode.EXPRESSION:
102 | settings[par.name] = {'$': par.expr}
103 | return settings
104 |
105 | def applySettings(self, settings: dict):
106 | for par in self._getParams():
107 | if settings and par.name in settings:
108 | val = settings[par.name]
109 | if isinstance(val, dict) and '$' in val:
110 | par.expr = val['$']
111 | else:
112 | par.val = val
113 | else:
114 | par.val = par.default
115 |
116 | class UserSettings(SettingsExtBase):
117 | def __init__(self, ownerComp):
118 | self.ownerComp = ownerComp # type: COMP
119 |
120 | def getSettingsOps(self) -> List['SettingsOp']:
121 | return [SettingsOp('settings', op=self.ownerComp, pages=['Settings'])]
122 |
123 | def buildSettings(self):
124 | settings = super().buildSettings()
125 | settings['recorderPresets'] = self.getRecorderPresets()
126 | return settings
127 |
128 | def getRecorderPresets(self):
129 | recorderPresets = self.getRecorderPresetsComp()
130 | if not recorderPresets:
131 | return {}
132 | return {
133 | 'Banks': recorderPresets.Banks
134 | }
135 |
136 | def setRecorderPresets(self, settings):
137 | recorderPresets = self.getRecorderPresetsComp()
138 | if not recorderPresets:
139 | return
140 | recorderPresets.Banks = (settings and settings.get('Banks')) or []
141 |
142 | def applySettings(self, settings: dict):
143 | super().applySettings(settings)
144 | self.setRecorderPresets(settings.get('recorderPresets'))
145 |
146 | def getRecorderPresetsComp(self) -> 'Any':
147 | return self.ownerComp.par.Recorderpresetscomp.eval()
148 |
149 | def getSettingsPath(self):
150 | return Path(self.ownerComp.par.Usersettingsfile.eval())
151 |
152 | def LoadSettings(self):
153 | self.loadSettingsFile(self.getSettingsPath())
154 |
155 | def Loadsettings(self, par):
156 | self.LoadSettings()
157 |
158 | def SaveSettings(self):
159 | filePath = self.getSettingsPath()
160 | self.saveSettingsFile(filePath)
161 | print(f'Settings saved to {filePath}')
162 |
163 | def Savesettings(self, par):
164 | self.SaveSettings()
165 |
166 | def RecentWorkspaces(self) -> List[str]:
167 | return [
168 | path
169 | for path in self.ownerComp.par.Recentworkspaces.eval() or []
170 | if path and path != '.'
171 | ]
172 |
173 | def AddRecentWorkspace(self, workspaceSettingsFile: str):
174 | workspaceSettingsFile = tdu.collapsePath(str(Path(workspaceSettingsFile).as_posix()))
175 | workspaces = self.RecentWorkspaces()
176 | if workspaceSettingsFile in workspaces:
177 | workspaces.remove(workspaceSettingsFile)
178 | workspaces.insert(0, workspaceSettingsFile)
179 | self.ownerComp.par.Recentworkspaces.val = workspaces
180 | self.SaveSettings()
181 |
182 | class _CustomEncoder(json.JSONEncoder):
183 | def default(self, o):
184 | if isinstance(o, (DependList, DependDict)):
185 | return o.getRaw()
186 | return o
187 |
--------------------------------------------------------------------------------
/rack/editor/components/WorkspaceExt.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import List, Optional
3 | from .SettingsExt import SettingsOp, SettingsExtBase
4 |
5 | # noinspection PyUnreachableCode
6 | if False:
7 | # noinspection PyUnresolvedReferences
8 | from _stubs import *
9 | from typing import Any
10 | iop.workspaceState = COMP()
11 | ipar.workspaceState = Any()
12 |
13 | class Workspace(SettingsExtBase):
14 | def __init__(self, ownerComp):
15 | self.ownerComp = ownerComp # type: COMP
16 |
17 | def getSettingsOps(self) -> List['SettingsOp']:
18 | settingsOps = [
19 | self.workspaceSettingsOp()
20 | ]
21 | opTable = self.ownerComp.par.Settingsoptable.eval() # type: DAT
22 | if opTable:
23 | for i in range(1, opTable.numRows):
24 | settingsOps.append(SettingsOp.fromDatRow(opTable, i))
25 | return settingsOps
26 |
27 | def PromptLoadWorkspaceFile(self):
28 | path = ui.chooseFile(load=True, fileTypes=['json'], title='Open Workspace File')
29 | if path:
30 | self.LoadWorkspaceFile(path)
31 |
32 | def PromptLoadWorkspaceFolder(self):
33 | path = ui.chooseFolder(title='Open Workspace Folder')
34 | if path:
35 | self.LoadWorkspaceFolder(path)
36 |
37 | def LoadWorkspaceFile(self, file: str):
38 | filePath = Path(file)
39 | self._LoadWorkspace(
40 | filePath,
41 | filePath.parent)
42 |
43 | def LoadWorkspaceFolder(self, path: str):
44 | folderPath = Path(path)
45 | self._LoadWorkspace(
46 | folderPath / 'workspace.json',
47 | folderPath)
48 |
49 | def OpenWorkspace(self, fileOrFolder: str):
50 | path = Path(fileOrFolder)
51 | if path.is_dir():
52 | self.LoadWorkspaceFolder(path)
53 | else:
54 | self.LoadWorkspaceFile(path)
55 |
56 | def workspaceSettingsOp(self):
57 | return SettingsOp('workspace', op=self.ownerComp, pages=['Settings'])
58 |
59 | def _LoadWorkspace(self, settingsPath: Optional[Path], folderPath: Optional[Path]):
60 | ipar.workspaceState.Rootfolder = folderPath or ''
61 | self.ownerComp.par.Settingsfile = settingsPath or ''
62 | if folderPath is not None:
63 | folderPath.mkdir(parents=True, exist_ok=True)
64 | self.loadSettingsFile(settingsPath)
65 | self.ownerComp.par.Name = self.ownerComp.par.Name or (folderPath.name if folderPath is not None else '')
66 | self.ownerComp.par.Onworkspaceload.pulse()
67 |
68 | def applySettings(self, settings: dict):
69 | super().applySettings(settings)
70 | self.ownerComp.par.Settings = settings
71 |
72 | def UnloadWorkspace(self):
73 | self._LoadWorkspace(None, None)
74 | self.ownerComp.par.Onworkspaceunload.pulse()
75 |
76 | def SaveSettings(self):
77 | if not ipar.workspaceState.Rootfolder or not self.ownerComp.par.Settingsfile:
78 | raise Exception('No workspace selected')
79 | folderPath = Path(ipar.workspaceState.Rootfolder.eval())
80 | folderPath.mkdir(parents=True, exist_ok=True)
81 | settingsPath = Path(self.ownerComp.par.Settingsfile.eval())
82 | settings = self.saveSettingsFile(settingsPath)
83 | self.ownerComp.par.Settings = settings
84 | print(f'Saved workspace settings to {settingsPath}')
85 |
--------------------------------------------------------------------------------
/rack/editor/components/editorTools.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/editorTools.tox
--------------------------------------------------------------------------------
/rack/editor/components/editorViews.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/editorViews.tox
--------------------------------------------------------------------------------
/rack/editor/components/preview_panel.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/preview_panel.tox
--------------------------------------------------------------------------------
/rack/editor/components/settings.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/settings.tox
--------------------------------------------------------------------------------
/rack/editor/components/topMenuCallbacks.py:
--------------------------------------------------------------------------------
1 | """
2 | TopMenu callbacks
3 |
4 | Callbacks always take a single argument, which is a dictionary
5 | of values relevant to the callback. Print this dictionary to see what is
6 | being passed. The keys explain what each item is.
7 |
8 | TopMenu info keys:
9 | 'widget': the TopMenu widget
10 | 'item': the item label in the menu list
11 | 'index': either menu index or -1 for none
12 | 'indexPath': list of parent menu indexes leading to this item
13 | 'define': TopMenu define DAT definition info for this menu item
14 | 'menu': the popMenu component inside topMenu
15 | """
16 |
17 |
18 |
19 | def getMenuItems(info):
20 | items = ext.editor.GetMenuItems(**info)
21 | # print(f'getMenuItems -> {items}')
22 | return items
23 |
24 |
25 | def onItemTrigger(info):
26 | ext.editor.OnMenuTrigger(**info)
27 |
28 | # standard menu callbacks
29 |
30 | def onSelect(info):
31 | """
32 | User selects a menu option
33 | """
34 | # debug(info)
35 | pass
36 |
37 | def onRollover(info):
38 | """
39 | Mouse rolled over an item
40 | """
41 |
42 | def onOpen(info):
43 | """
44 | Menu opened
45 | """
46 |
47 | def onClose(info):
48 | """
49 | Menu closed
50 | """
51 |
52 | def onMouseDown(info):
53 | """
54 | Item pressed
55 | """
56 |
57 | def onMouseUp(info):
58 | """
59 | Item released
60 | """
61 |
62 | def onClick(info):
63 | """
64 | Item pressed and released
65 | """
66 |
67 | def onLostFocus(info):
68 | """
69 | Menu lost focus
70 | """
--------------------------------------------------------------------------------
/rack/editor/components/topMenuDefine.txt:
--------------------------------------------------------------------------------
1 | button item1 item2 callback dividerAfter disable checked highlight shortcut rowCallback specialId actionOp actionMethod statePar itemValue itemDepth
2 | Workspace
3 | Open Workspace File onItemTrigger workspace PromptLoadWorkspaceFile
4 | Open Workspace Folder onItemTrigger workspace PromptLoadWorkspaceFolder
5 | Save Workspace onItemTrigger not ipar.workspace.Rootfolder workspace SaveSettings
6 | Close Workspace onItemTrigger not ipar.workspace.Rootfolder workspace UnloadWorkspace
7 | Recent True
8 | getMenuItems recentWorkspaces 2
9 | Component
10 | Save onItemTrigger not ipar.editorState.Hascomponent SaveComponent
11 | Save As onItemTrigger not ipar.editorState.Hascomponent PromptComponentSaveAs
12 | Save New Version onItemTrigger not ipar.editorState.Hascomponent SaveComponentNewVersion
13 | Close onItemTrigger True not ipar.editorState.Hascomponent UnloadComponent
14 | Show Component Network onItemTrigger not ipar.editorState.Hascomponent ShowNetwork
15 | Show Component Network (Popup) onItemTrigger not ipar.editorState.Hascomponent ShowNetworkPopup
16 | Customize Component onItemTrigger not ipar.editorState.Hascomponent CustomizeComponent
17 | View
18 | Left Panel True
19 | getMenuItems Selectedleftpanel 2
20 | Main View True
21 | getMenuItems Selectedview 2
22 | Right Panel True
23 | getMenuItems Selectedrightpanel 2
24 |
--------------------------------------------------------------------------------
/rack/editor/components/workspace.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/components/workspace.tox
--------------------------------------------------------------------------------
/rack/editor/editor.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/editor/editor.tox
--------------------------------------------------------------------------------
/rack/rack/RackExt.py:
--------------------------------------------------------------------------------
1 | from typing import Optional, Tuple
2 |
3 | # noinspection PyUnreachableCode
4 | if False:
5 | # noinspection PyUnresolvedReferences
6 | from _stubs import *
7 |
8 | class Rack:
9 | def __init__(self, ownerComp):
10 | self.ownerComp = ownerComp
11 |
12 | @property
13 | def RackTools(self): return self.ownerComp.op('rack_tools')
14 |
15 | @property
16 | def RackToolsPane(self) -> Optional['Pane']:
17 | tools = self.RackTools
18 | for pane in ui.panes:
19 | if pane.owner == tools and pane.type == PaneType.PANEL:
20 | return pane
21 |
22 | @property
23 | def RackToolsPaneSize(self) -> Tuple[int, int]:
24 | pane = self.RackToolsPane
25 | if not pane:
26 | return 0, 0
27 | w = pane.topRight.x - pane.bottomLeft.x
28 | h = pane.topRight.y - pane.bottomLeft.y
29 | return w, h
30 |
--------------------------------------------------------------------------------
/rack/rack/rack.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/rack/rack.tox
--------------------------------------------------------------------------------
/rack/rack_tools/rack_tools.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/rack/rack_tools/rack_tools.tox
--------------------------------------------------------------------------------
/recorder.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 | import pathlib
4 | import shutil
5 |
6 | from common import ExtensionBase, loggedmethod, formatValue, parseValue
7 |
8 | # noinspection PyUnreachableCode
9 | if False:
10 | # noinspection PyUnresolvedReferences
11 | from _stubs import *
12 |
13 | _inputResMultipliers = {
14 | 'quarter': 0.25,
15 | 'half': 0.5,
16 | 'original': 1,
17 | 'double': 2,
18 | 'quadruple': 4,
19 | }
20 |
21 | class Recorder(ExtensionBase):
22 | def __init__(self, ownerComp):
23 | super().__init__(ownerComp)
24 | self.DiskSpace = tdu.Dependency(None)
25 | if self.IsRecordingVideo:
26 | self.EndVideoCapture()
27 |
28 | def BuildImageFileName(self):
29 | folder = self._OutputFolderPath
30 | return self._BuildFileName(folder, isvideo=False, isimagesequence=False)
31 |
32 | def BuildImageSequenceFileName(self):
33 | folder = self._OutputFolderPath
34 | return self._BuildFileName(folder, isvideo=False, isimagesequence=True)
35 |
36 | def BuildVideoFileName(self):
37 | folder = self._OutputFolderPath
38 | return self._BuildFileName(folder, isvideo=True, isimagesequence=False)
39 |
40 | def _GetFileBaseName(self):
41 | name = self.ownerComp.par.Basename.eval()
42 | if not name:
43 | name = os.path.splitext(project.name)[0] + '-output'
44 | if name.endswith('.toe'):
45 | name = name[:-4]
46 | if self.ownerComp.par.Includedate:
47 | name += '-' + str(datetime.date.today())
48 | return name + '-'
49 |
50 | @property
51 | def Resolution(self):
52 | if self.ownerComp.par.Useinputres:
53 | video = self.ownerComp.op('./video')
54 | multipliername = self.ownerComp.par.Inputresmult.eval()
55 | w, h = video.width, video.height
56 | mult = _inputResMultipliers.get(multipliername) or 1
57 | w = round(w * mult)
58 | h = round(h * mult)
59 | else:
60 | w, h = int(self.ownerComp.par.Resolution1), int(self.ownerComp.par.Resolution2)
61 | return w, h
62 |
63 | @property
64 | def FormattedResolution(self):
65 | w, h = self.Resolution
66 | return '{}x{}'.format(w, h)
67 |
68 | @property
69 | def _VideoCodecSuffix(self):
70 | vcodec = self.ownerComp.par.Videocodec.eval()
71 | suffix = self.ownerComp.op('./video_codecs')[vcodec, 'suffix']
72 | return suffix.val if suffix and suffix.val else vcodec
73 |
74 | @property
75 | def _ImageExtension(self):
76 | imgtype = self.ownerComp.par.Imagefiletype.eval()
77 | ext = self.ownerComp.op('./image_ext_overrides')[imgtype, 1]
78 | return ext.val if ext else imgtype
79 |
80 | @property
81 | def _HasAudio(self):
82 | return self.ownerComp.op('./have_audio')[0]
83 |
84 | def _GetSuffix(self, isvideo, isimagesequence):
85 | suffix = self.ownerComp.par.Suffix.eval()
86 | suffixparts = [suffix] if suffix else []
87 | if self.ownerComp.par.Addresolutionsuffix:
88 | suffixparts.append(self.FormattedResolution)
89 | if isvideo or isimagesequence:
90 | if self.ownerComp.par.Addvcodecsuffix:
91 | suffixparts.append(self._VideoCodecSuffix)
92 | if self.ownerComp.par.Addacodecsuffix and self._HasAudio:
93 | suffixparts.append(self.ownerComp.par.Audiocodec.eval())
94 | if self.ownerComp.par.Addfpssuffix:
95 | suffixparts.append(str(self.ownerComp.par.Fps) + 'fps')
96 | if isvideo:
97 | ext = '.mov'
98 | else:
99 | ext = '.' + self._ImageExtension
100 | if not suffixparts:
101 | return ext
102 | return '-'.join([''] + suffixparts) + ext
103 |
104 | @property
105 | def _OutputFolderPath(self):
106 | folder = self.ownerComp.par.Folder.eval()
107 | if folder:
108 | folder = mod.tdu.expandPath(folder)
109 | return pathlib.Path(folder or project.folder)
110 |
111 | def _BuildFileName(self, folder: pathlib.Path, isvideo, isimagesequence):
112 | basename = self._GetFileBaseName()
113 | i = 1
114 | if folder.exists():
115 | while any(folder.glob(basename + str(i) + '[.-]*')):
116 | i += 1
117 | suffix = self._GetSuffix(isvideo, isimagesequence)
118 | return basename + str(i) + suffix
119 |
120 | @staticmethod
121 | def _CreateOutputFolder(folder):
122 | if folder.exists():
123 | ui.status = 'Output folder exists: {}'.format(folder)
124 | else:
125 | folder.mkdir(parents=True, exist_ok=True)
126 | ui.status = 'Created output folder: {}'.format(folder)
127 |
128 | @property
129 | def _MovieOut(self):
130 | return self.ownerComp.op('moviefileout')
131 |
132 | @property
133 | def IsRecordingVideo(self):
134 | return self._MovieOut.par.record.eval()
135 |
136 | @loggedmethod
137 | def StartVideoCapture(self):
138 | folder = self._OutputFolderPath
139 | self._CreateOutputFolder(folder)
140 | if self.ownerComp.par.Videotype == 'imagesequence':
141 | filename = self.BuildImageSequenceFileName()
142 | else:
143 | filename = self.BuildVideoFileName()
144 | filepath = folder.joinpath(filename)
145 | ui.status = 'Start video capture ' + str(filepath)
146 | fileout = self._MovieOut
147 | fileout.par.file = filepath
148 | fileout.par.record = True
149 |
150 | @loggedmethod
151 | def EndVideoCapture(self):
152 | fileout = self._MovieOut
153 | fileout.par.record = False
154 | ui.status = 'Wrote video to ' + fileout.par.file.eval()
155 |
156 | @loggedmethod
157 | def CaptureImage(self):
158 | folder = self._OutputFolderPath
159 | self._CreateOutputFolder(folder)
160 | filepath = folder.joinpath(self.BuildImageFileName())
161 | fileout = self.ownerComp.op('video')
162 | fileout.save(filepath)
163 | ui.status = 'Wrote image to ' + str(filepath)
164 |
165 | def UpdateDiskSpace(self):
166 | folder = self._OutputFolderPath
167 | if not folder.exists():
168 | self.DiskSpace.val = None
169 | else:
170 | space = shutil.disk_usage(str(folder))
171 | self.DiskSpace.val = space.free
172 |
173 | @property
174 | def FormattedDiskSpace(self):
175 | val = self.DiskSpace.val
176 | return '' if val is None else _formatBytes(val)
177 |
178 | @loggedmethod
179 | def EmergencyShutOff(self):
180 | if not self.IsRecordingVideo:
181 | self._LogEvent('Not recording, nothing to stop')
182 | return
183 | msg = 'WARNING: halting recording due to insufficient disk space'
184 | self._LogEvent(msg)
185 | ui.status = 'WARNING: halting recording due to insufficient disk space'
186 | self.EndVideoCapture()
187 |
188 | def UpdatePanelHeight(self):
189 | height = _panelsHeight(self.ownerComp.op('root_panel').panelChildren)
190 | height += _panelsHeight(self.ownerComp.op('settings_panel').panelChildren)
191 | maxheight = self.ownerComp.par.Maxheight.eval()
192 | if 0 < maxheight < height:
193 | height = maxheight
194 | self.ownerComp.par.h = height
195 |
196 | def GetSettingsDict(self):
197 | pars = []
198 | for page in self.ownerComp.customPages:
199 | if page.name in ['Output', 'Format', 'Advanced']:
200 | pars += page.pars
201 | return {
202 | par.name: formatValue(par.eval())
203 | for par in pars
204 | }
205 |
206 | def SetSettingsDict(self, settings: dict):
207 | for key, val in settings.items():
208 | par = self.ownerComp.par[key]
209 | if par is not None:
210 | par.val = parseValue(val)
211 |
212 | def _panelsHeight(panels):
213 | return sum([
214 | c.height
215 | for c in panels
216 | if c.isPanel and c.par.display and c.par.vmode == 'fixed'
217 | ])
218 |
219 | _sizes = ["B", "KB", "MB", "GB", "TB"]
220 | def _formatBytes(bytes_num):
221 |
222 | i = 0
223 | dblbyte = bytes_num
224 |
225 | while i < len(_sizes) and bytes_num >= 1024:
226 | dblbyte = bytes_num / 1024.0
227 | i = i + 1
228 | bytes_num = bytes_num / 1024
229 |
230 | return str(round(dblbyte, 2)) + " " + _sizes[i]
231 |
232 |
--------------------------------------------------------------------------------
/recorder.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/recorder.tox
--------------------------------------------------------------------------------
/recorder/recorderCore.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/recorder/recorderCore.tox
--------------------------------------------------------------------------------
/recorder/recorderCoreExt.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import os
3 | import datetime
4 |
5 | # noinspection PyUnreachableCode
6 | if False:
7 | # noinspection PyUnresolvedReferences
8 | from _stubs import *
9 |
10 |
11 | class RecorderCore:
12 | def __init__(self, ownerComp):
13 | self.ownerComp = ownerComp # type: COMP
14 | self.processedVideo = ownerComp.op('video')
15 | self.movieOut = ownerComp.op('moviefileout')
16 | self.imageOut = ownerComp.op('imagefileout')
17 | self.videoRes = ownerComp.op('video_res')
18 | self.audioState = ownerComp.op('audio_state')
19 |
20 | @property
21 | def formattedResolution(self):
22 | w = int(self.videoRes['width'])
23 | h = int(self.videoRes['height'])
24 | return f'{int(w)}x{int(h)}'
25 |
26 | @property
27 | def videoCodecSuffix(self):
28 | vcodec = self.ownerComp.par.Videocodec.eval()
29 | suffix = self.ownerComp.op('./video_codecs')[vcodec, 'suffix']
30 | if suffix is None:
31 | return vcodec
32 | return suffix.val
33 |
34 | @property
35 | def imageExtension(self):
36 | imgtype = self.ownerComp.par.Imagefiletype.eval()
37 | extension = self.ownerComp.op('./image_ext_overrides')[imgtype, 1]
38 | return extension.val if extension else imgtype
39 |
40 | @property
41 | def haveAudio(self):
42 | return self.audioState['haveaudio'] > 0
43 |
44 | def getSuffix(self, fileType):
45 | suffix = self.ownerComp.par.Suffix.eval()
46 | suffixParts = [suffix] if suffix else []
47 | if self.ownerComp.par.Addresolutionsuffix:
48 | suffixParts.append(self.formattedResolution)
49 | if fileType in ['movie', 'imagesequence']:
50 | if self.ownerComp.par.Addvcodecsuffix:
51 | vcSuffix = self.videoCodecSuffix
52 | if vcSuffix:
53 | suffixParts.append(vcSuffix)
54 | if self.ownerComp.par.Audioenabled and self.ownerComp.par.Addacodecsuffix and self.haveAudio:
55 | suffixParts.append(self.ownerComp.par.Audiocodec.eval())
56 | if self.ownerComp.par.Addfpssuffix:
57 | suffixParts.append(str(self.ownerComp.par.Fps) + 'fps')
58 | if fileType == 'movie':
59 | extension = '.mov'
60 | else:
61 | extension = '.' + self.imageExtension
62 | if not suffixParts:
63 | return extension
64 | return '-'.join([''] + suffixParts) + extension
65 |
66 | @property
67 | def outputFolderPath(self):
68 | folder = self.ownerComp.par.Folder.eval()
69 | if folder:
70 | folder = mod.tdu.expandPath(folder)
71 | return Path(folder or project.folder)
72 |
73 | @property
74 | def fileBaseName(self):
75 | name = self.ownerComp.par.Basename.eval()
76 | if not name:
77 | name = os.path.splitext(project.name)[0] + '-output'
78 | if name.endswith('.toe'):
79 | name = name[:-4]
80 | if self.ownerComp.par.Includedate:
81 | dateMode = self.ownerComp.par.Datetype.eval()
82 | if dateMode == 'custom':
83 | dateValue = self.ownerComp.par.Customdate.eval()
84 | else:
85 | dateValue = str(datetime.date.today())
86 | if dateValue:
87 | name += '-' + dateValue
88 | return name + '-'
89 |
90 | def buildFileName(self, fileType):
91 | baseName = self.fileBaseName
92 | i = 1
93 | folder = self.outputFolderPath
94 | if folder.exists():
95 | existingFiles = folder.glob(baseName + '[0-9]*')
96 | maxIndex = None
97 | for file in existingFiles:
98 | try:
99 | fileIndex = int(file.stem[len(baseName):].split('-', 1)[0])
100 | except ValueError:
101 | continue
102 | if maxIndex is None or fileIndex > maxIndex:
103 | maxIndex = fileIndex
104 | if maxIndex is not None:
105 | i = maxIndex + 1
106 | suffix = self.getSuffix(fileType)
107 | return baseName + str(i) + suffix
108 |
109 | def CreateOutputFolder(self):
110 | folder = self.outputFolderPath
111 | if folder.exists():
112 | # ui.status = 'Output folder exists: {}'.format(folder)
113 | pass
114 | else:
115 | folder.mkdir(parents=True, exist_ok=True)
116 | ui.status = 'Created output folder: ' + str(folder)
117 | return folder
118 |
119 | def BuildImageFileName(self):
120 | return self.buildFileName('image')
121 |
122 | def BuildVideoFileName(self):
123 | return self.buildFileName('movie')
124 |
125 | def BuildImageSequenceFileName(self):
126 | return self.buildFileName('imagesequence')
127 |
128 | def StartVideoCapture(self):
129 | folder = self.CreateOutputFolder()
130 | if self.ownerComp.par.Videotype == 'imagesequence':
131 | fileName = self.BuildImageSequenceFileName()
132 | else:
133 | fileName = self.BuildVideoFileName()
134 | filePath = folder.joinpath(fileName)
135 | ui.status = 'Start video capture ' + str(filePath)
136 | self.movieOut.par.file = filePath
137 | self.movieOut.par.record = True
138 |
139 | def EndVideoCapture(self):
140 | if not self.movieOut.par.record:
141 | return
142 | self.movieOut.par.record = False
143 | ui.status = 'Wrote video to ' + str(self.movieOut.par.file)
144 | self.movieOut.par.file = ''
145 |
146 | def CaptureImage(self):
147 | folder = self.CreateOutputFolder()
148 | filePath = folder.joinpath(self.BuildImageFileName())
149 | self.imageOut.par.file = filePath
150 | self.imageOut.par.record.pulse(1)
151 | ui.status = 'Wrote image to ' + str(filePath)
152 | self.imageOut.par.file = ''
153 |
154 |
155 |
--------------------------------------------------------------------------------
/sop_merger.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/sop_merger.tox
--------------------------------------------------------------------------------
/td-components-tester.toe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/td-components-tester.toe
--------------------------------------------------------------------------------
/tools/ParamHelperExt.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnresolvedReferences
2 | import tdu
3 |
4 | # noinspection PyUnreachableCode
5 | if False:
6 | from _stubs import *
7 |
8 | class ParamHelper:
9 | def __init__(self, ownerComp):
10 | self.ownerComp = ownerComp
11 |
12 | @property
13 | def DestinationPageNames(self):
14 | d = self._Destination
15 | return [p.name for p in d.customPages] if d else []
16 |
17 | def HandleDrop(self, dropName, xPos, yPos, index, totalDragged, dropExt, baseName, destPath):
18 | print('ParamHelper HandleDrop {!r}'.format(locals()))
19 | if dropExt == 'parameter':
20 | o = op(baseName)
21 | par = getattr(o.par, dropName) if o else None
22 | if par is not None:
23 | self._ApplyChangesToParameter(par)
24 | elif dropExt in ('DAT', 'TOP', 'CHOP', 'SOP', 'MAT'):
25 | parentOp = op(baseName)
26 | o = parentOp.op(dropName) if parentOp else None
27 | if o is not None:
28 | self._HandleDropOp(o)
29 | pass
30 |
31 | def _HandleDropOp(self, o: 'OP'):
32 | pass
33 |
34 | @property
35 | def _Destination(self) -> 'COMP':
36 | return self.ownerComp.par.Destination.eval()
37 |
38 | def _UpdateStatus(self, message: str):
39 | ui.status = message
40 | statuses = self.ownerComp.op('set_statuses')
41 | statuses.appendRow([message])
42 |
43 | def _ApplyChangesToParameter(self, par: 'Par'):
44 | dest = self._Destination
45 | if not dest:
46 | self._UpdateStatus('No destination OP')
47 | return
48 | if not dest.isCOMP:
49 | self._UpdateStatus('Destination is not a COMP!')
50 | return
51 | namePrefix = self.ownerComp.par.Nameprefix.eval()
52 | if self.ownerComp.par.Labelusecustomprefix:
53 | labelPrefix = self.ownerComp.par.Labelprefix.eval()
54 | elif not namePrefix:
55 | labelPrefix = ''
56 | else:
57 | labelPrefix = namePrefix
58 | if labelPrefix and not labelPrefix.endswith(' '):
59 | labelPrefix += ' '
60 | namePrefix = tdu.legalName(namePrefix)
61 | newName = tdu.legalName(namePrefix + par.name).capitalize()
62 | label = labelPrefix + par.label
63 | if self.ownerComp.par.Pageusecustomname:
64 | pageName = self.ownerComp.par.Page.eval()
65 | else:
66 | pageName = par.page.name
67 | if not pageName:
68 | pageName = 'Custom'
69 | existingPar = getattr(dest.par, newName, None)
70 | if existingPar is not None:
71 | self._UpdateStatus('Updating existing parameter: {}'.format(newName))
72 | existingPar.label = label
73 | newPar = existingPar
74 | else:
75 | self._UpdateStatus('Attempting to get/create page: {!r}'.format(pageName))
76 | page = dest.appendCustomPage(pageName)
77 | self._UpdateStatus('Attempting to create par {!r}'.format(newName))
78 | newPar = page.appendPar(newName, par=par, label=label)[0]
79 | self._UpdateStatus('Created parameter {!r}'.format(newPar))
80 | if newPar.defaultExpr in (None, 'None'):
81 | newPar.defaultExpr = ''
82 | newPar.default = par.default
83 | attachType = self.ownerComp.par.Attachmenttype.eval()
84 | if attachType == 'none':
85 | return
86 | if self.ownerComp.par.Attachdirection == 'oldmaster':
87 | fromOp = dest
88 | toOp = par.owner
89 | fromPar = newPar
90 | toPar = par
91 | else:
92 | fromOp = par.owner
93 | toOp = dest
94 | fromPar = par
95 | toPar = newPar
96 | self._UpdateStatus('Attempting to connect {!r} to {!r} using {}'.format(
97 | toPar, fromPar, attachType))
98 | expr = self._GetParExpr(
99 | fromOp=fromOp, toOp=toOp, parName=toPar.name)
100 | self._UpdateStatus('Attempting to connect {!r} to {!r} using {}, expr: {!r}'.format(
101 | toPar, fromPar, attachType, expr))
102 | if not expr:
103 | return
104 | if attachType == 'binding':
105 | fromPar.bindExpr = expr
106 | elif attachType == 'reference':
107 | fromPar.expr = expr
108 |
109 | def _GetParExpr(self, fromOp: 'OP', toOp: 'OP', parName: str):
110 | pathType = self.ownerComp.par.Referencepathtype.eval()
111 | if pathType == 'absolute':
112 | return 'op({!r}).par.{}'.format(toOp.path, parName)
113 | elif pathType == 'relative':
114 | path = fromOp.relativePath(toOp)
115 | if not path:
116 | self._UpdateStatus('Unable to create relative path from {} to {}'.format(fromOp, toOp))
117 | return None
118 | return 'op({!r}).par.{}'.format(path, parName)
119 | else:
120 | path = fromOp.shortcutPath(toOp, toParName=parName)
121 | if not path:
122 | self._UpdateStatus('Unable to create shortcut path from {} to {}'.format(fromOp, toOp))
123 | return path
124 |
--------------------------------------------------------------------------------
/tools/PathSetupExt.py:
--------------------------------------------------------------------------------
1 | import os.path
2 |
3 | # noinspection PyUnreachableCode
4 | if False:
5 | # noinspection PyUnresolvedReferences
6 | from _stubs import *
7 |
8 | class PathSetup:
9 | def __init__(self, ownerComp):
10 | self.ownerComp = ownerComp
11 |
12 | def BuildPossiblePathsFromParams(self, dat: 'DAT'):
13 | dat.clear()
14 | for i in range(1, 5):
15 | name = getattr(self.ownerComp.par, 'Path{}name'.format(i), None)
16 | if not name:
17 | continue
18 | for j in range(1, 5):
19 | folder = getattr(self.ownerComp.par, 'Path{}folder{}'.format(i, j), None)
20 | if folder:
21 | dat.appendRow([name, folder])
22 |
23 | @staticmethod
24 | def PreparePathTable(outDat: 'DAT', inDat: 'DAT'):
25 | paths = {}
26 | for cells in inDat.rows():
27 | name = cells[0].val
28 | if name in paths:
29 | continue
30 | path = os.path.expanduser(os.path.expandvars(cells[1].val))
31 | if os.path.exists(path):
32 | paths[name] = path
33 | outDat.clear()
34 | for name, path in paths.items():
35 | outDat.appendRow([name, path])
36 |
37 | def ApplyPaths(self):
38 | table = self.ownerComp.op('path_table') # type: DAT
39 | newPaths = {name.val: path.val for name, path in table.rows()}
40 | missingPathNames = [name for name in project.paths if name not in newPaths]
41 | for name, path in newPaths.items():
42 | project.paths[name] = path
43 | if self.ownerComp.par.Removeotherpaths:
44 | for name in missingPathNames:
45 | del project.paths[name]
46 | self.ownerComp.op('build_current_paths_table').cook()
47 |
48 | @staticmethod
49 | def ClearAllPaths():
50 | project.paths.clear()
51 |
52 | @staticmethod
53 | def BuildCurrentPathsTable(dat: 'DAT'):
54 | dat.clear()
55 | for name, path in project.paths.items():
56 | dat.appendRow([name, path])
57 |
--------------------------------------------------------------------------------
/tools/UIColorEditorExt.py:
--------------------------------------------------------------------------------
1 | class UIColorEditor:
2 | def __init__(self, ownerComp):
3 | self.ownerComp = ownerComp
4 |
5 | @staticmethod
6 | def GetUIColors():
7 | return {
8 | key: ui.colors[key]
9 | for key in ui.colors
10 | }
11 |
12 | @staticmethod
13 | def BuildColorNameTable(dat):
14 | dat.clear()
15 | dat.appendRow(['fullname', 'namespace', 'subname'])
16 | for fullname in sorted(ui.colors):
17 | fullname: str
18 | if '.' not in fullname:
19 | dat.appendRow([fullname, '', fullname])
20 | else:
21 | parts = fullname.split('.', maxsplit=3)
22 | dat.appendRow([fullname, '.'.join(parts[:-1]), parts[-1]])
23 |
24 | @staticmethod
25 | def BuildColorTable(dat):
26 | dat.clear()
27 | dat.appendRow(['name', 'r', 'g', 'b'])
28 | for key in sorted(ui.colors):
29 | dat.appendRow([key] + list(ui.colors[key]))
30 |
31 | def UpdateListColors(self, colorTable):
32 | colorList = self.ownerComp.op('color_list').par.Lister.eval()
33 | for i in range(1, colorTable.numRows):
34 | color = colorTable[i, 'r'], colorTable[i, 'g'], colorTable[i, 'b'], 1
35 | colorList.SetCellOverlay(i, 1, color)
36 | colorList.SetCellOverlay(i, 2, color)
37 | colorList.SetCellOverlay(i, 3, color)
38 |
39 | pass
40 |
--------------------------------------------------------------------------------
/tools/base_save.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/base_save.tox
--------------------------------------------------------------------------------
/tools/paramAdjuster.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/paramAdjuster.tox
--------------------------------------------------------------------------------
/tools/paramAdjusterExt.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnreachableCode
2 | if False:
3 | # noinspection PyUnresolvedReferences
4 | from _stubs import *
5 |
6 | def getNormVal(p: 'Par'):
7 | if p is None or p.isPulse or p.isMomentary or p.isOP:
8 | return 0
9 | if p.isMenu:
10 | return p.menuIndex / (len(p.menuNames) - 1)
11 | if p.isString:
12 | return 0
13 | if p.isNumber:
14 | return p.normVal
15 | return 0
16 |
17 | def setNormVal(p: 'Par', normVal: float):
18 | if p is None or p.isOP:
19 | return False
20 | if p.isPulse or p.isMomentary:
21 | p.pulse(1)
22 | elif p.isMenu:
23 | p.menuIndex = round(normVal * (len(p.menuNames) - 1))
24 | elif p.isString:
25 | return False
26 | elif p.isNumber:
27 | p.normVal = normVal
28 | else:
29 | return False
30 | return True
31 |
32 | lastControl = None
33 |
34 | def onMidiInput(channel: 'Channel'):
35 | p = ui.rolloverPar
36 | if p is None:
37 | return
38 | name = channel.name
39 | d = tdu.digits(name)
40 | if d is not None:
41 | name = tdu.base(name) + str(d - 1)
42 | op('set_output_val').par.name0 = name
43 | setNormVal(p, float(channel))
44 | pass
45 |
46 | def onTableChange(dat):
47 | path = dat['path', 1].val
48 | o = path and op(path)
49 | if not o:
50 | return
51 | p = o.par[dat['param', 1]]
52 | normVal = 0
53 | if p is None:
54 | return
55 | if p.isPulse or p.isMomentary:
56 | return
57 | if p.isMenu:
58 | normVal = p.menuIndex / (len(p.menuNames)-1)
59 | elif p.isString:
60 | return
61 | elif p.isNumber:
62 | normVal = p.normVal
63 | op('set_output_val').par.value0 = normVal
64 | return
65 |
66 | def onValueChange(channel, sampleIndex, val, prev):
67 | p = ui.rolloverPar
68 | # op('set_last_input_name')[0,0] = channel.name
69 | name = channel.name
70 | d = tdu.digits(name)
71 | if d is not None:
72 | name = tdu.base(name) + str(d - 1)
73 | op('set_output_val').par.name0 = name
74 | if p is None or p.isOP or p.isPython:
75 | return
76 | if p.isPulse or p.isMomentary:
77 | p.pulse()
78 | else:
79 | val /= 127.0
80 | if p.isMenu:
81 | i = round(val * (len(p.menuNames) - 1))
82 | p.menuIndex = i
83 | elif p.isString:
84 | return
85 | elif p.isNumber:
86 | if p.isInt:
87 | p.val = round(tdu.remap(val, 0, 1, p.normMin, p.normMax))
88 | else:
89 | p.normVal = val
90 |
91 | class ParamAdjuster:
92 | def __init__(self, ownerComp: 'COMP'):
93 | self.ownerComp = ownerComp
94 | self.lastControl = None
95 |
96 | def onMidiInput(self, channel: 'Channel'):
97 | p = ui.rolloverPar
98 | if p is None:
99 | return
100 | if setNormVal(p, float(channel)):
101 | self.lastControl = tdu.digits(channel.name)
102 |
103 | def onRolloverParamChange(self):
104 | p = ui.rolloverPar
105 | pass
106 |
--------------------------------------------------------------------------------
/tools/param_helper.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/param_helper.tox
--------------------------------------------------------------------------------
/tools/path_setup.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/path_setup.tox
--------------------------------------------------------------------------------
/tools/saveEXT.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | if False:
4 | from _stubs import *
5 |
6 |
7 | class ExternalFiles:
8 | """
9 | The ExternalFiles class is used to handle working with both externalizing files,
10 | as well as ingesting files that were previously externalized. This helps
11 | to minimize the amount of manual work that might need to otherise be used
12 | for handling external files.
13 | """
14 |
15 | def __init__(self, my_op):
16 | """
17 | Stands in place of an execute dat - ensures all elements start-up correctly
18 |
19 | Notes
20 | ---------
21 |
22 | Args
23 | ---------
24 | myOp (touchDesignerOperator):
25 | > the operator that is loading the current extension
26 |
27 | Returns
28 | ---------
29 | none
30 | """
31 |
32 | self.my_op = my_op
33 | self.Flash_duration = 4
34 |
35 | init_msg = "Save init from {}".format(my_op)
36 | self.Defaultcolor = self.my_op.pars('Defaultcolor*')
37 | self.Op_finder = self.my_op.op('opfind1')
38 | self.Extension_flag = self.my_op.par.Extensionflag.val
39 |
40 | print(init_msg)
41 |
42 | return
43 |
44 | def Prompt_to_save(self, current_loc):
45 | """
46 | The method used to save an external TOX to file
47 |
48 | Notes
49 | ---------
50 |
51 | Args
52 | ---------
53 | current_loc (str):
54 | > the operator that's related to the currently focused
55 | > pane object. This is required to ensure that we correctly
56 | > grab the appropriate COMP and check to see if needs to be saved
57 |
58 | Returns
59 | ---------
60 | none
61 | """
62 |
63 | ext_color = self.my_op.pars("Extcolor*")
64 | msg_box_title = "TOX Save"
65 | msg_box_msg = "Replacing External\n\nYou are about to overwite an external TOX"
66 | msg_box_buttons = ["Cancel", "Continue"]
67 |
68 | sav_msg_box_title = "Externalize Tox"
69 | sav_msg_box_msg = "This TOX is not yet externalized\n\nWould you like to externalize this TOX?"
70 | sav_msg_box_buttons = ["No", "Yes"]
71 |
72 | save_msg_buttons_parent_too = ["No", "This COMP Only", "This COMP and the Parent"]
73 |
74 | # check if location is the root of the project
75 | if current_loc == '/':
76 | # skip if we're at the root of the project
77 | pass
78 |
79 | else:
80 | # if we're not at the root of the project
81 |
82 | # check if external
83 | if current_loc.par.externaltox != '' and 'noextern' not in current_loc.tags:
84 | confirmation = ui.messageBox(
85 | msg_box_title, msg_box_msg + '\ncomponent: ' + current_loc.path,
86 | buttons=msg_box_buttons)
87 |
88 | if confirmation:
89 |
90 | # save external file
91 | self.Save_over_tox(current_loc)
92 |
93 | else:
94 | # if the user presses "cancel" we pass
95 | pass
96 |
97 | # in this case we are not external, so let's ask if we want to externalize the file
98 | else:
99 |
100 | # check if the parent is externalized
101 | if current_loc.parent().par.externaltox != '' and 'noextern' not in current_loc.parent().tags:
102 | save_ext = ui.messageBox(
103 | sav_msg_box_title,
104 | sav_msg_box_msg +
105 | '\nparent: ' + current_loc.parent().path +
106 | '\ncomponent: ' + current_loc.path,
107 | buttons=save_msg_buttons_parent_too)
108 |
109 | # save this comp only
110 | if save_ext == 1:
111 | self.Save_tox(current_loc)
112 |
113 | # save this comp and the parent
114 | elif save_ext == 2:
115 | self.Save_tox(current_loc)
116 | print("save this tox")
117 |
118 | # save parent() COMP
119 | self.Save_over_tox(current_loc.parent())
120 | print("Save the parent too!")
121 |
122 | # user selected 'No'
123 | else:
124 | pass
125 |
126 | # the parent is not external, so let's ask about externalizing the tox
127 | elif 'noextern' not in current_loc.tags:
128 | save_ext = ui.messageBox(
129 | sav_msg_box_title, sav_msg_box_msg + '\ncomponent: ' + current_loc.path,
130 | buttons=sav_msg_box_buttons)
131 |
132 | if save_ext:
133 | self.Save_tox(current_loc)
134 |
135 | else:
136 | # the user selected "No"
137 | pass
138 |
139 | return
140 |
141 | def Save_over_tox(self, current_loc):
142 | ext_color = self.my_op.pars("Extcolor*")
143 | external_path = current_loc.par.externaltox
144 | current_loc.save(external_path)
145 |
146 | # set color for COMP
147 | current_loc.color = (ext_color[0], ext_color[1], ext_color[2])
148 |
149 | # flash color
150 | self.Flash_bg("Bgcolor")
151 |
152 | # create and print log message
153 | log_msg = "{} saved to {}/{}".format(
154 | current_loc,
155 | project.folder,
156 | external_path)
157 |
158 | self.Logtotextport(log_msg)
159 |
160 | return
161 |
162 | def Save_tox(self, current_loc):
163 | ext_color = self.my_op.pars("Extcolor*")
164 |
165 | # ask user for a save location
166 | save_loc = ui.chooseFolder(title="TOX Location", start=project.folder)
167 |
168 | if not save_loc:
169 | self.Logtotextport('No file location selected for {}'.format(current_loc))
170 | return
171 |
172 | # construct a relative path and relative loaction for our elements
173 | print(save_loc)
174 | rel_path = tdu.collapsePath(save_loc)
175 |
176 | # check to see if the location is at the root of the project folder structure
177 | if rel_path == "$TOUCH":
178 | rel_loc = '{new_tox}/{new_tox}.tox'.format(new_tox=current_loc.name)
179 |
180 | # save path is not in the root of the project
181 | else:
182 | rel_loc = '{new_module}/{new_tox}/{new_tox}.tox'.format(new_module=rel_path, new_tox=current_loc.name)
183 |
184 | # create path and directory in the OS
185 | new_path = '{selected_path}/{new_module}'.format(selected_path=save_loc, new_module=current_loc.name)
186 | os.mkdir(new_path)
187 |
188 | # format our tox path
189 | tox_path = '{dir_path}/{tox}.tox'.format(dir_path=new_path, tox=current_loc.name)
190 |
191 | # setup our module correctly
192 | current_loc.par.externaltox = rel_loc
193 | current_loc.par.savebackup = False
194 |
195 | # set color for COMP
196 | current_loc.color = (ext_color[0], ext_color[1], ext_color[2])
197 |
198 | # save our tox
199 | current_loc.save(tox_path)
200 |
201 | # flash color
202 | self.Flash_bg("Bgcolor")
203 |
204 | # create and print log message
205 | log_msg = "{} saved to {}/{}".format(
206 | current_loc,
207 | project.folder,
208 | tox_path)
209 | self.Logtotextport(log_msg)
210 | return
211 |
212 | def Reload_ext_dat(self, external_file):
213 | """
214 | Used to reload DAT files, and to re-init modules.
215 |
216 | Notes
217 | ---------
218 |
219 | Args
220 | ---------
221 | external_file (str):
222 | > the operator that's related to the currently focused
223 | > pane object. This is required to ensure that we correctly
224 | > grab the appropriate COMP and check to see if needs to be saved
225 |
226 | Returns
227 | ---------
228 | none
229 | """
230 |
231 | file_path = '{project}/{file}'.format(project=project.folder, file=external_file)
232 |
233 | # loop through all the dats
234 | for each_op in self.Op_finder.cols(2)[0][1:]:
235 | each_op_path = op(each_op).par.file.val
236 | # print( file_path == each_op_path)
237 |
238 | # check our file path to see if it's relative or absolute
239 | # change our path if we need to
240 | if os.path.isabs(each_op_path):
241 | pass
242 | else:
243 | each_op_path = '{}/{}'.format(project.folder, each_op_path)
244 |
245 | # if there's a match reload the DAT
246 | if file_path == each_op_path:
247 | op(each_op.val).par.loadonstartpulse.pulse()
248 |
249 | # flash the background so we know a file has been loaded
250 | self.Flash_bg("Savecolor")
251 |
252 | # check to see if the external file is python
253 | if external_file.split('.')[1] == "py":
254 |
255 | # check to see if an op is flagged as an extension:
256 | if self.Extension_flag in op(each_op.val).tags:
257 |
258 | # check to see the op's parent has any extensions
259 | extension_pars = [ext for ext in op(each_op.val).parent().pars('extension*')]
260 | if len(extension_pars) > 0:
261 | # print(op(each_op.val).parent())
262 |
263 | # reinit the parent's extensions
264 | op(each_op.val).parent().par.reinitextensions.pulse()
265 |
266 | elif op(each_op.val).parent().isCOMP and op(each_op.val).parent().par.externaltox != '':
267 | # print("This needs reinit")
268 |
269 | # if the DAT has a parent COMP, reinit the extension
270 | op(each_op.val).parent().par.reinitextensions.pulse()
271 |
272 | self.Flash_bg("Savecolor")
273 |
274 | else:
275 | # COMP is the only consideration we care about at the moment
276 | pass
277 | else:
278 | # skip other file types for now
279 | pass
280 |
281 | # stop once we have a hit
282 | break
283 |
284 | else:
285 | pass
286 |
287 | return
288 |
289 | def Flash_bg(self, parColors):
290 | """
291 | Used to flash the background of the TD network.
292 |
293 | Notes
294 | ---------
295 | This is a simple tool to flash indicator colors in the
296 | background to help you have some visual confirmation that
297 | you have in fact externalized a file.
298 |
299 | Args
300 | ---------
301 | parColors (str):
302 | > this is the string name to match against the parent's pars()
303 | > for to pull colors to use for changing the background
304 |
305 | Returns
306 | ---------
307 | none
308 | """
309 | par_color = '{}*'.format(parColors)
310 | over_ride_color = self.my_op.pars(par_color)
311 |
312 | # change background color (0.1, 0.105, 0.12)
313 | ui.colors['worksheet.bg'] = over_ride_color
314 | delay_script = "ui.colors['worksheet.bg'] = args[0]"
315 |
316 | # want to change the background color back
317 | run(delay_script, self.Defaultcolor, delayFrames=self.Flash_duration)
318 |
319 | return
320 |
321 | def Logtotextport(self, logMsg):
322 |
323 | if self.my_op.par.Logtotextport:
324 | print(logMsg)
325 |
326 | else:
327 | pass
328 |
329 | return
330 |
--------------------------------------------------------------------------------
/tools/ui_color_editor.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/tools/ui_color_editor.tox
--------------------------------------------------------------------------------
/ui/slider_value_overlay.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/slider_value_overlay.tox
--------------------------------------------------------------------------------
/ui/statusOverlay-readme.txt:
--------------------------------------------------------------------------------
1 | statusOverlay
2 | Version: 1.0
3 | Author: Tekt
4 |
5 | Shows temporary or permanent status messages as an overlay
6 | on a UI panel.
7 |
8 | By default the panel will be non-visible. When messages are
9 | added, the panel will show the messages and block the UI
10 | underneath it. Once no more messages are present, the overlay
11 | will hide itself.
12 |
13 | Temporary messages will be shown for a limited time (controlled
14 | by the "Temporary Message Duration" parameter), after which
15 | they will disappear.
16 |
17 | Static messages will be shown until they are explicitly removed.
18 |
19 |
20 | To use, make it a child of the panel that it should cover, and
21 | make sure that the following parameters are set (which they
22 | will be by default when dropping in the tox):
23 |
24 | - Depth Layer: 1
25 | - Horizontal Mode: Fill
26 | - Vertical Mode: Fill
27 | - Parent Alignment: Ignore
28 | - Display: False
29 |
30 |
31 | To add a temporary message, either call `.AddMessage('some text')`
32 | or put the text in the "Message to Add" parameter and click the
33 | "Add Temporary Message" pulse parameter.
34 |
35 | To add a static message, either call `.AddStaticMessage('some text')`
36 | or use the "Add Static Message" pulse parameter. Calling the method
37 | will return an identifier that can later be passed to
38 | `.ClearMessage(messageId)`.
39 |
40 | To remove all messages, either call `.ClearAllMessages()` or use
41 | the "Clear Messages" pulse parameter.
--------------------------------------------------------------------------------
/ui/statusOverlay-thumb.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/statusOverlay-thumb.jpg
--------------------------------------------------------------------------------
/ui/statusOverlay.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/statusOverlay.tox
--------------------------------------------------------------------------------
/ui/statusOverlayExt.py:
--------------------------------------------------------------------------------
1 | from typing import List, Optional
2 | from dataclasses import dataclass
3 |
4 | # noinspection PyUnreachableCode
5 | if False:
6 | # noinspection PyUnresolvedReferences
7 | from _stubs import *
8 |
9 | class StatusOverlay:
10 | def __init__(self, ownerComp):
11 | self.ownerComp = ownerComp # type: panelCOMP
12 | self.messages = [] # type: List[_Message]
13 | self.timer = ownerComp.op('update_timer') # type: timerCHOP
14 | self.labelComp = ownerComp.op('label') # type: panelCOMP
15 | self.nextId = 0
16 |
17 | def ClearAllMessages(self):
18 | self.stopTimer()
19 | self.messages.clear()
20 | self.updateUI()
21 |
22 | def ClearMessage(self, messageId: int):
23 | record = self.getMessageById(messageId)
24 | if not record:
25 | return
26 | self.messages.remove(record)
27 | if not self.messages:
28 | self.stopTimer()
29 | self.updateUI()
30 |
31 | def getMessageById(self, messageId: int):
32 | for record in self.messages:
33 | if record.id == messageId:
34 | return record
35 |
36 | def AddStaticMessage(self, message: str):
37 | return self.AddMessage(message, temporary=False)
38 |
39 | def Clearmessages(self, par: 'Par'):
40 | self.ClearAllMessages()
41 |
42 | def Addtemporarymessage(self, par: 'Par'):
43 | self.addMessageFromPar(temporary=True)
44 |
45 | def Addstaticmessage(self, par: 'Par'):
46 | self.addMessageFromPar(temporary=False)
47 |
48 | def addMessageFromPar(self, temporary: bool):
49 | message = self.ownerComp.par.Messagetoadd.eval()
50 | print('OMG ADD MESSAGE FROM PAR: ', repr(message))
51 | if message:
52 | self.AddMessage(message, temporary=temporary)
53 |
54 | @staticmethod
55 | def now():
56 | return absTime.seconds
57 |
58 | def AddMessage(self, message: str, temporary: bool = True):
59 | if temporary:
60 | record = _Message(
61 | self.nextId, message, self.now() + self.ownerComp.par.Temporaryduration)
62 | else:
63 | record = _Message(self.nextId, message)
64 | self.nextId += 1
65 | self.messages.append(record)
66 | self.updateUI()
67 | if temporary:
68 | # if not self.timer['timer_active']:
69 | self.startTimer()
70 | return record.id
71 |
72 | def startTimer(self):
73 | self.timer.par.start.pulse()
74 |
75 | def stopTimer(self):
76 | self.timer.par.initialize.pulse()
77 |
78 | def updateUI(self):
79 | if not self.messages:
80 | self.labelComp.par.Widgetlabel = ''
81 | self.ownerComp.par.display = False
82 | else:
83 | self.labelComp.par.Widgetlabel = '\n'.join([
84 | record.text
85 | for record in self.messages
86 | ])
87 | self.ownerComp.par.display = True
88 |
89 | def onUpdateTrigger(self):
90 | needAnotherUpdate = self.clearExpiredMessages()
91 | self.updateUI()
92 | if needAnotherUpdate:
93 | self.startTimer()
94 |
95 | def clearExpiredMessages(self):
96 | now = self.now()
97 | self.messages = [
98 | record
99 | for record in self.messages
100 | if record.endTime is None or record.endTime > now
101 | ]
102 | changed = False
103 | needAnotherUpdate = False
104 | filteredMessages = []
105 | for record in self.messages:
106 | if record.endTime is None:
107 | filteredMessages.append(record)
108 | elif record.endTime > now:
109 | needAnotherUpdate = True
110 | filteredMessages.append(record)
111 | else:
112 | changed = True
113 | if changed:
114 | self.messages = filteredMessages
115 | return needAnotherUpdate
116 |
117 | @dataclass
118 | class _Message:
119 | id: int
120 | text: str
121 | endTime: Optional[float] = None # in seconds
122 |
--------------------------------------------------------------------------------
/ui/ui_overrides.txt:
--------------------------------------------------------------------------------
1 | template path parameter value
2 | global_slider * Font 'Roboto'
3 | global_slider * Labelfontbold 0
4 | global_slider * Labelbgcolorr 0.2
5 | global_slider * Labelbgcolorg 0.2
6 | global_slider * Labelbgcolorb 0.2
7 | global_slider * Labelbgcolora 1.0
8 | global_slider * Labelfontcolorr 0.65
9 | global_slider * Labelfontcolorg 0.65
10 | global_slider * Labelfontcolorb 0.65
11 | global_slider * Labelfontcolora 1.0
12 | global_slider * Sliderlabelfontcolorr 0.8
13 | global_slider * Sliderlabelfontcolorg 0.8
14 | global_slider * Sliderlabelfontcolorb 0.8
15 | global_slider * Sliderlabelfontcolora 1.0
16 | global_slider * Sliderbgcolorr 0.15
17 | global_slider * Sliderbgcolorg 0.15
18 | global_slider * Sliderbgcolorb 0.15
19 | global_slider * Sliderbgcolora 1.0
20 | global_slider * Sliderknobcolorr 0.6
21 | global_slider * Sliderknobcolorg 0.6
22 | global_slider * Sliderknobcolorb 0.6
23 | global_slider * Sliderknobcolora 1.0
24 | global_slider * Sliderlabelfontcolorr 0.8
25 | global_slider * Sliderlabelfontcolorg 0.8
26 | global_slider * Sliderlabelfontcolorb 0.8
27 | global_slider * Sliderlabelfontcolora 1.0
28 | global_slider * Sliderindicatorcolorr 0.325
29 | global_slider * Sliderindicatorcolorg 0.325
30 | global_slider * Sliderindicatorcolorb 0.325
31 | global_slider * Sliderindicatorcolora 1.0
32 | button *_button Buttonfont 'Roboto'
33 | buttonicon *_buttonicon Buttonfont 'Material Design Icons'
34 | dropfield *_dropfield Buttonfont 'Material Design Icons'
35 | filefield *_filefield Buttonfont 'Material Design Icons'
36 | radio *_radio Radiobasecolorr 0.0
37 | radio *_radio Radiobasecolorg 0.0
38 | radio *_radio Radiobasecolorb 0.0
39 | radio *_radio Radiobasecolora 0.0
40 | radio *_radio Radioofffacecolorr 0.28
41 | radio *_radio Radioofffacecolorg 0.28
42 | radio *_radio Radioofffacecolorb 0.28
43 | radio *_radio Radioofffacecolora 1.0
44 | radio *_radio Radioonfacecolorr 0.3276
45 | radio *_radio Radioonfacecolorg 0.52
46 | radio *_radio Radioonfacecolorb 0.343633
47 | radio *_radio Radioonfacecolora 1.0
48 | radio *_radio Radioofffontcolorr 0.9
49 | radio *_radio Radioofffontcolorg 0.9
50 | radio *_radio Radioofffontcolorb 0.9
51 | radio *_radio Radioofffontcolora 1.0
52 | radio *_radio Radioonfontcolorr 1.0
53 | radio *_radio Radioonfontcolorg 1.0
54 | radio *_radio Radioonfontcolorb 1.0
55 | radio *_radio Radioonfontcolora 1.0
56 | radio *_radio Radioborderacolorr 1.0
57 | radio *_radio Radioborderacolorg 1.0
58 | radio *_radio Radioborderacolorb 1.0
59 | radio *_radio Radioborderacolora 1.0
60 | radio *_radio Radioborderbcolorr 0.0
61 | radio *_radio Radioborderbcolorg 0.0
62 | radio *_radio Radioborderbcolorb 0.0
63 | radio *_radio Radioborderbcolora 1.0
64 | section *_section Buttonfont 'Material Design Icons'
65 | toggle *_toggle Buttonfont 'Roboto'
66 | toggleicon *_toggleicon Buttonfont 'Material Design Icons'
67 | trigger *_trigger Buttonfont 'Roboto'
68 | global_toggle * Buttonbasecolorr 0.0
69 | global_toggle * Buttonbasecolorg 0.0
70 | global_toggle * Buttonbasecolorb 0.0
71 | global_toggle * Buttonbasecolora 0.0
72 | global_toggle * Buttonofffacecolorr 0.28
73 | global_toggle * Buttonofffacecolorg 0.28
74 | global_toggle * Buttonofffacecolorb 0.28
75 | global_toggle * Buttonofffacecolora 1.0
76 | global_toggle * Buttononfacecolorr 0.3276
77 | global_toggle * Buttononfacecolorg 0.52
78 | global_toggle * Buttononfacecolorb 0.343633
79 | global_toggle * Buttononfacecolora 1.0
80 | global_toggle * Buttonofffontcolorr 0.9
81 | global_toggle * Buttonofffontcolorg 0.9
82 | global_toggle * Buttonofffontcolorb 0.9
83 | global_toggle * Buttonofffontcolora 1.0
84 | global_toggle * Buttononfontcolorr 1.0
85 | global_toggle * Buttononfontcolorg 1.0
86 | global_toggle * Buttononfontcolorb 1.0
87 | global_toggle * Buttononfontcolora 1.0
88 | global_toggle * Buttonbordercolorr 1.0
89 | global_toggle * Buttonbordercolorg 1.0
90 | global_toggle * Buttonbordercolorb 1.0
91 | global_toggle * Buttonbordercolora 1.0
92 | section *_section Buttontype 'Toggle Down'
93 | section *_section Sectionlabelbgcolorr 0.0
94 | section *_section Sectionlabelfontcolorr 0.956863
95 | section *_section Sectionlabelbgcolorg 0.0
96 | section *_section Sectionlabelfontcolorg 1.0
97 | section *_section Sectionlabelbgcolorb 0.0
98 | section *_section Sectionlabelfontcolorb 0.498039
99 | section *_section Sectionlabelbgcolora 0.0
100 | section *_section Sectionlabelfontcolora 0.6
101 | overlay *_overlay Markercolorr 0.3276
102 | overlay *_overlay Markercolorg 0.52
103 | overlay *_overlay Markercolorb 0.343633
104 | overlay *_overlay Markercolora 1.0
105 | list *_list Listerbasecolorr 0.1
106 | list *_list Listerheadercolorr 0.2
107 | list *_list Listerheaderfontcolorr 0.8
108 | list *_list Listeritemscolorr 0.45
109 | list *_list Listerodditemscolorr 0.45
110 | list *_list Listeritemsfontcolorr 0.1
111 | list *_list Listerrowselectcolorr 0.413424
112 | list *_list Listerrowselectfontcolorr 0.0
113 | list *_list Listerrowrollcolorr 0.75
114 | list *_list Listerrowrollfontcolorr 0.1
115 | list *_list Listerbuttonoffcolorr 0.7
116 | list *_list Listerbuttonrollcolorr 0.3276
117 | list *_list Listerbuttononcolorr 0.0
118 | list *_list Listerbuttonofffontcolorr 0.2
119 | list *_list Listerbuttonrollfontcolorr 0.0
120 | list *_list Listerbuttononfontcolorr 0.0
121 | list *_list Listerbasecolorg 0.1
122 | list *_list Listerheadercolorg 0.2
123 | list *_list Listerheaderfontcolorg 0.8
124 | list *_list Listeritemscolorg 0.45
125 | list *_list Listerodditemscolorg 0.45
126 | list *_list Listeritemsfontcolorg 0.1
127 | list *_list Listerrowselectcolorg 0.957
128 | list *_list Listerrowselectfontcolorg 0.1012
129 | list *_list Listerrowrollcolorg 0.75
130 | list *_list Listerrowrollfontcolorg 0.1
131 | list *_list Listerbuttonoffcolorg 0.7
132 | list *_list Listerbuttonrollcolorg 0.52
133 | list *_list Listerbuttononcolorg 0.0
134 | list *_list Listerbuttonofffontcolorg 0.2
135 | list *_list Listerbuttonrollfontcolorg 0.0
136 | list *_list Listerbuttononfontcolorg 0.0
137 | list *_list Listerbasecolorb 0.1
138 | list *_list Listerheadercolorb 0.2
139 | list *_list Listerheaderfontcolorb 0.8
140 | list *_list Listeritemscolorb 0.45
141 | list *_list Listerodditemscolorb 0.45
142 | list *_list Listeritemsfontcolorb 0.1
143 | list *_list Listerrowselectcolorb 0.458721
144 | list *_list Listerrowselectfontcolorb 0.184
145 | list *_list Listerrowrollcolorb 0.75
146 | list *_list Listerrowrollfontcolorb 0.1
147 | list *_list Listerbuttonoffcolorb 0.7
148 | list *_list Listerbuttonrollcolorb 0.343633
149 | list *_list Listerbuttononcolorb 0.0
150 | list *_list Listerbuttonofffontcolorb 0.2
151 | list *_list Listerbuttonrollfontcolorb 0.0
152 | list *_list Listerbuttononfontcolorb 0.0
153 | list *_list Listerbasecolora 1.0
154 | list *_list Listerheadercolora 1.0
155 | list *_list Listerheaderfontcolora 1.0
156 | list *_list Listeritemscolora 1.0
157 | list *_list Listerodditemscolora 1.0
158 | list *_list Listeritemsfontcolora 1.0
159 | list *_list Listerrowselectcolora 0.6
160 | list *_list Listerrowselectfontcolora 1.0
161 | list *_list Listerrowrollcolora 1.0
162 | list *_list Listerrowrollfontcolora 1.0
163 | list *_list Listerbuttonoffcolora 1.0
164 | list *_list Listerbuttonrollcolora 1.0
165 | list *_list Listerbuttononcolora 1.0
166 | list *_list Listerbuttonofffontcolora 1.0
167 | list *_list Listerbuttonrollfontcolora 1.0
168 | list *_list Listerbuttononfontcolora 1.0
169 | topMenu * Menubuttonbgcolorr 0.3
170 | topMenu * Menubuttonbgcolorg 0.3
171 | topMenu * Menubuttonbgcolorb 0.3
172 | topMenu * Menubuttonbgcolora 1.0
173 | topMenu * Menubuttonfontcolorr 0.7
174 | topMenu * Menubuttonfontcolorg 0.7
175 | topMenu * Menubuttonfontcolorb 0.7
176 | topMenu * Menubuttonfontcolora 1.0
177 | topMenu * Menubuttononbgcolorr 0.8
178 | topMenu * Menubuttononbgcolorg 0.8
179 | topMenu * Menubuttononbgcolorb 0.8
180 | topMenu * Menubuttononbgcolora 1.0
181 | topMenu * Menubuttononfontcolorr 0.1
182 | topMenu * Menubuttononfontcolorg 0.1
183 | topMenu * Menubuttononfontcolorb 0.1
184 | topMenu * Menubuttononfontcolora 1.0
185 | topMenu * Menubuttondisablecolorr 0.0
186 | topMenu * Menubuttondisablecolorg 0.0
187 | topMenu * Menubuttondisablecolorb 0.0
188 | topMenu * Menubuttondisablecolora 0.5
189 | topMenu * Menubgcolorr 0.3
190 | topMenu * Menubgcolorg 0.3
191 | topMenu * Menubgcolorb 0.3
192 | topMenu * Menubgcolora 1.0
193 | topMenu * Menufontcolorr 0.9
194 | topMenu * Menufontcolorg 0.9
195 | topMenu * Menufontcolorb 0.9
196 | topMenu * Menufontcolora 1.0
197 | topMenu * Menurollbgcolorr 1.0
198 | topMenu * Menurollbgcolorg 1.0
199 | topMenu * Menurollbgcolorb 1.0
200 | topMenu * Menurollbgcolora 0.1
201 | topMenu * Menurollfontcolorr 0.9
202 | topMenu * Menurollfontcolorg 0.9
203 | topMenu * Menurollfontcolorb 0.9
204 | topMenu * Menurollfontcolora 1.0
205 | topMenu * Menuselectbgcolorr 0.5
206 | topMenu * Menuselectbgcolorg 0.5
207 | topMenu * Menuselectbgcolorb 0.5
208 | topMenu * Menuselectbgcolora 1.0
209 | topMenu * Menuselectfontcolorr 0.9
210 | topMenu * Menuselectfontcolorg 0.9
211 | topMenu * Menuselectfontcolorb 0.9
212 | topMenu * Menuselectfontcolora 1.0
213 | topMenu * Menudisablebgcolorr 0.2
214 | topMenu * Menudisablebgcolorg 0.2
215 | topMenu * Menudisablebgcolorb 0.2
216 | topMenu * Menudisablebgcolora 0.5
217 | topMenu * Menudisablefontcolorr 0.4
218 | topMenu * Menudisablefontcolorg 0.4
219 | topMenu * Menudisablefontcolorb 0.4
220 | topMenu * Menudisablefontcolora 1.0
221 | topMenu * Menuhighlightbgcolorr 0.3
222 | topMenu * Menuhighlightbgcolorg 0.3
223 | topMenu * Menuhighlightbgcolorb 0.6
224 | topMenu * Menuhighlightbgcolora 1.0
225 | topMenu * Menuhighlightfontcolorr 0.9
226 | topMenu * Menuhighlightfontcolorg 0.9
227 | topMenu * Menuhighlightfontcolorb 0.9
228 | topMenu * Menuhighlightfontcolora 1.0
229 | topMenu * Menubordercolorr 0.9
230 | topMenu * Menubordercolorg 0.9
231 | topMenu * Menubordercolorb 0.9
232 | topMenu * Menubordercolora 1.0
233 | topMenu * Menudividercolorr 0.9
234 | topMenu * Menudividercolorg 0.9
235 | topMenu * Menudividercolorb 0.9
236 | topMenu * Menudividercolora 1.0
237 |
--------------------------------------------------------------------------------
/ui/widget_ui_overrides.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/ui/widget_ui_overrides.tox
--------------------------------------------------------------------------------
/utils/DataLock.py:
--------------------------------------------------------------------------------
1 | def update(scriptOp):
2 | scriptOp.copy(scriptOp.inputs[0])
3 |
4 | def onCook(scriptOp):
5 | if not parent().par.Locked:
6 | update(scriptOp)
7 |
8 | def onPulse(par):
9 | scriptOp = op('data_lock')
10 | if par.name == 'Update':
11 | update(scriptOp)
12 | elif par.name == 'Clear':
13 | scriptOp.clear()
14 |
15 | onStart = update
16 | onCreate = update
17 |
--------------------------------------------------------------------------------
/utils/ParamFilter.py:
--------------------------------------------------------------------------------
1 | # noinspection PyUnreachableCode
2 | if False:
3 | # noinspection PyUnresolvedReferences
4 | from _stubs import *
5 |
6 | def InitHost():
7 | filterOp = parent()
8 | hostOp = filterOp.par.Parfilterop.eval()
9 | if not hostOp:
10 | return
11 | prefix = filterOp.par.Installedlabelprefix.eval()
12 | page = hostOp.appendCustomPage('Param Filter')
13 | fp = filterOp.par.Parfilterenable
14 | page.appendToggle(fp.name, label=prefix + fp.label)
15 | fp = filterOp.par.Parfiltertype
16 | p = page.appendMenu(fp.name, label=prefix + fp.label)[0]
17 | p.menuNames = fp.menuNames
18 | p.menuLabels = fp.menuLabels
19 | p.default = fp.default
20 | _copyFloatPar(page, filterOp.par.Parfilterwidth, prefix)
21 | _copyFloatPar(page, filterOp.par.Parfilterlag1, prefix)
22 | _copyFloatPar(page, filterOp.par.Parfilterovershoot1, prefix)
23 |
24 | exprPrefix = 'op("{}").par.'.format(filterOp.relativePath(hostOp))
25 | for p in filterOp.customPages[0].pars:
26 | p.expr = exprPrefix + p.name
27 |
28 | def _copyFloatPar(page: 'Page', src: 'Par', prefix: str):
29 | sourceTuplet = src.tuplet
30 | parTuplet = page.appendFloat(src.tupletName, label=prefix + src.label, size=len(sourceTuplet))
31 | for i, srcp in enumerate(sourceTuplet):
32 | for a in (
33 | 'default', 'normMin', 'normMax',
34 | 'clampMin', 'clampMax', 'min', 'max'
35 | ):
36 | setattr(parTuplet[i], a, getattr(srcp, a))
37 |
--------------------------------------------------------------------------------
/utils/channel_remap.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/channel_remap.tox
--------------------------------------------------------------------------------
/utils/chop_lock.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/chop_lock.tox
--------------------------------------------------------------------------------
/utils/dat_lock.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/dat_lock.tox
--------------------------------------------------------------------------------
/utils/lfo_generator.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/lfo_generator.tox
--------------------------------------------------------------------------------
/utils/modulated_value.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/modulated_value.tox
--------------------------------------------------------------------------------
/utils/param_animator.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/param_animator.tox
--------------------------------------------------------------------------------
/utils/param_filter.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/param_filter.tox
--------------------------------------------------------------------------------
/utils/settings/SettingsExt.py:
--------------------------------------------------------------------------------
1 | import json
2 |
3 | # noinspection PyUnreachableCode
4 | if False:
5 | # noinspection PyUnresolvedReferences
6 | from _stubs import *
7 | from typing import Any
8 | iop.hostedComp = COMP()
9 | ipar.compEditor = Any()
10 | from _stubs.TDCallbacksExt import CallbacksExt
11 | ext.Callbacks = CallbacksExt(None)
12 |
13 | class Settings:
14 | def __init__(self, ownerComp):
15 | self.ownerComp = ownerComp # type: COMP
16 |
17 | @staticmethod
18 | def BuildSettings(parameters: 'DAT'):
19 | settings = {}
20 | for i in range(1, parameters.numRows):
21 | if parameters[i, 'readonly'] == '1' or parameters[i, 'enabled'] == '0':
22 | continue
23 | path = parameters[i, 'path'].val
24 | if path not in settings:
25 | settings[path] = {}
26 | name = parameters[i, 'name'].val
27 | mode = parameters[i, 'mode']
28 | if mode == '':
29 | settings[path][name] = parameters[i, 'value'].val
30 | elif mode == 'expression':
31 | settings[path][name] = {'$': parameters[i, 'expression'].val}
32 | elif mode == 'constant':
33 | settings[path][name] = parameters[i, 'constant'].val
34 | return settings
35 |
36 | def ParseSettings(self, settings: dict):
37 | if not settings:
38 | return
39 | for path, opSettings in settings.items():
40 | if not opSettings:
41 | continue
42 | o = op(path)
43 | if not o:
44 | continue
45 | for name, val in opSettings.items():
46 | par = o.par[name] # type: Par
47 | if par is None:
48 | print(f'{self.ownerComp.path}: Warning par not found. path: {path} name: {name}')
49 | continue
50 | if isinstance(val, dict):
51 | if '$' not in val:
52 | print(f'{self.ownerComp.path}: Warning invalid setting. path: {path} name: {name} val: {val!r}')
53 | continue
54 | par.expr = val['$']
55 | else:
56 | if par.isNumber:
57 | par.val = float(val)
58 | elif par.isToggle:
59 | par.val = val in ['1', 'true', 1, True]
60 | else:
61 | par.val = val
62 |
63 | def OnFileInChange(self, dat: 'DAT'):
64 | if dat.text:
65 | settings = json.loads(dat.text)
66 | else:
67 | settings = {}
68 | self.ParseSettings(settings)
69 |
70 | def Save(self, par):
71 | file = self.ownerComp.par.File.eval()
72 | if not file:
73 | raise Exception('No settings file specified')
74 | self.ownerComp.op('fileout').par.write.pulse()
75 |
76 | def Load(self, par):
77 | fileIn = self.ownerComp.op('filein')
78 | if not fileIn.par.file:
79 | return
80 | fileIn.par.refreshpulse.pulse()
81 | if not self.ownerComp.par.Autoload:
82 | self.OnFileInChange(fileIn)
83 |
84 | def Autoload(self, par):
85 | if par:
86 | self.Load(par)
87 |
88 | def Autosave(self, par):
89 | if par:
90 | self.Save(par)
91 |
--------------------------------------------------------------------------------
/utils/settings/settings.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/settings/settings.tox
--------------------------------------------------------------------------------
/utils/speed_generator.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/speed_generator.tox
--------------------------------------------------------------------------------
/utils/taskManager.tox:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optexture/td-components/4b1531c704b9ad49a9ccdf1d5d026b0d8335dc72/utils/taskManager.tox
--------------------------------------------------------------------------------
/utils/taskManagerExt.py:
--------------------------------------------------------------------------------
1 | from typing import Generic, Callable, List, Optional, Union
2 |
3 | # noinspection PyUnreachableCode
4 | if False:
5 | # noinspection PyUnresolvedReferences
6 | from _stubs import *
7 |
8 | T = TypeVar('T')
9 |
10 | class TaskManager:
11 | def __init__(self):
12 | self.tasks = [] # type: List[Callable]
13 | self.totalTasks = 0
14 | self.batchFutureTasks = 0
15 |
16 | def updateProgress(self):
17 | pass
18 |
19 | def getProgressRatio(self):
20 | if not self.tasks:
21 | return 1
22 | remaining = max(0, len(self.tasks) - self.batchFutureTasks)
23 | total = self.totalTasks
24 | if not total or not remaining:
25 | return 1
26 | return 1 - (remaining / total)
27 |
28 | def getFrameInterval(self):
29 | return 1
30 |
31 | def getDelayRef(self):
32 | return None
33 |
34 | def onTaskSucceeded(self, result):
35 | pass
36 |
37 | def onTaskFailed(self, err):
38 | pass
39 |
40 | def runNextTask(self):
41 | if not self.tasks:
42 | self.ClearTasks()
43 | return
44 | task = self.tasks.pop(0)
45 | result = task()
46 |
47 | def _onSuccess(res):
48 | self.onTaskSucceeded(res)
49 | self.updateProgress()
50 | self.queueNextTask()
51 |
52 | def _onFailure(err):
53 | self.onTaskFailed(err)
54 | # self.ClearTasks()
55 | self.updateProgress()
56 | self.queueNextTask()
57 |
58 | if isinstance(result, Future):
59 | result.then(success=_onSuccess, failure=_onFailure)
60 | else:
61 | _onSuccess(result)
62 |
63 | def queueNextTask(self):
64 | if not self.tasks:
65 | self.ClearTasks()
66 | return
67 | td.run(
68 | 'args[0].runNextTask()', self,
69 | delayFrames=self.getFrameInterval(),
70 | delayRef=self.getDelayRef())
71 |
72 | def AddTaskBatch(self, tasks: List[Callable], label=None) -> 'Future':
73 | label = f'TaskBatch({label or ""})'
74 | tasks = [task for task in tasks if task]
75 | if not tasks:
76 | return Future.immediate(label=f'{label or ""} (empty batch)')
77 | result = Future(label=label)
78 | self.tasks.extend(tasks)
79 |
80 | # TODO: get rid of this and fix the queue system!
81 | def _noOp():
82 | # Log('NO-OP for batch: {}'.format(label))
83 | pass
84 | self.tasks.append(_noOp)
85 | self.batchFutureTasks += 1
86 |
87 | def _finishBatch():
88 | result.resolve()
89 |
90 | self.tasks.append(_finishBatch)
91 | self.totalTasks += len(tasks)
92 | self.batchFutureTasks += 1
93 | self.queueNextTask()
94 | return result
95 |
96 | def ClearTasks(self):
97 | self.tasks.clear()
98 | self.totalTasks = 0
99 | self.batchFutureTasks = 0
100 | self.updateProgress()
101 |
102 | class Future(Generic[T]):
103 | def __init__(self, onListen=None, onInvoke=None, label=None):
104 | self._successCallback = None # type: Optional[Callable[[Union[T, 'Future']], None]]
105 | self._failureCallback = None # type: Optional[Callable[[Union[T, 'Future']], None]]
106 | self._resolved = False
107 | self._canceled = False
108 | self._result = None # type: Optional[T]
109 | self._error = None
110 | self._onListen = onListen # type: Callable
111 | self._onInvoke = onInvoke # type: Callable
112 | self.label = label
113 |
114 | def then(
115 | self,
116 | success: Callable[[Union[T, 'Future']], None] = None,
117 | failure: Callable[[Union[object, 'Future']], None] = None):
118 | if self._successCallback or self._failureCallback:
119 | raise Exception('Future already has success and/or failure callbacks!')
120 | if self._onListen:
121 | self._onListen()
122 | self._successCallback = success
123 | self._failureCallback = failure
124 | if self._resolved:
125 | self._invoke()
126 | return self
127 |
128 | def _invoke(self):
129 | if self._error is not None:
130 | if self._failureCallback:
131 | self._failureCallback(self._error)
132 | else:
133 | if self._successCallback:
134 | self._successCallback(self._result if self._result is not None else self)
135 | if self._onInvoke:
136 | self._onInvoke()
137 |
138 | def _resolve(self, result: T, error):
139 | if self._canceled:
140 | return
141 | if self._resolved:
142 | raise Exception('Future has already been resolved')
143 | self._resolved = True
144 | self._result = result
145 | self._error = error
146 | # if self._error is not None:
147 | # Log('FUTURE FAILED {}'.format(self))
148 | # else:
149 | # Log('FUTURE SUCCEEDED {}'.format(self))
150 | if self._successCallback or self._failureCallback:
151 | self._invoke()
152 |
153 | def resolve(self, result: Optional[T] = None):
154 | self._resolve(result, None)
155 | return self
156 |
157 | def fail(self, error):
158 | self._resolve(None, error or Exception())
159 | return self
160 |
161 | def cancel(self):
162 | if self._resolved:
163 | raise Exception('Future has already been resolved')
164 | self._canceled = True
165 |
166 | @property
167 | def isResolved(self):
168 | return self._resolved
169 |
170 | @property
171 | def result(self) -> T:
172 | return self._result
173 |
174 | def __str__(self):
175 | if self._canceled:
176 | state = 'canceled'
177 | elif self._resolved:
178 | if self._error is not None:
179 | state = 'failed: {}'.format(self._error)
180 | elif self._result is None:
181 | state = 'succeeded'
182 | else:
183 | state = 'succeeded: {}'.format(self._result)
184 | else:
185 | state = 'pending'
186 | return '{}({}, {})'.format(self.__class__.__name__, self.label or '<>', state)
187 |
188 | @classmethod
189 | def immediate(cls, value: T = None, onListen=None, onInvoke=None, label=None) -> 'Future[T]':
190 | future = cls(onListen=onListen, onInvoke=onInvoke, label=label)
191 | future.resolve(value)
192 | return future
193 |
194 | @classmethod
195 | def immediateError(cls, error, onListen=None, onInvoke=None, label=None):
196 | future = cls(onListen=onListen, onInvoke=onInvoke, label=label)
197 | future.fail(error)
198 | return future
199 |
200 | @classmethod
201 | def of(cls, obj):
202 | if isinstance(obj, Future):
203 | return obj
204 | return cls.immediate(obj)
205 |
206 | @classmethod
207 | def all(cls, *futures: 'Future', onListen=None, onInvoke=None) -> 'Future[List]':
208 | if not futures:
209 | return cls.immediate([], onListen=onListen, onInvoke=onInvoke)
210 | merged = cls(onListen=onListen, onInvoke=onInvoke)
211 | state = {
212 | 'succeeded': 0,
213 | 'failed': 0,
214 | 'results': [None] * len(futures),
215 | 'errors': [None] * len(futures),
216 | }
217 |
218 | def _checkComplete():
219 | if (state['succeeded'] + state['failed']) < len(futures):
220 | return
221 | if state['failed'] > 0:
222 | merged.fail((state['errors'], state['results']))
223 | else:
224 | merged.resolve(state['results'])
225 |
226 | def _makeCallbacks(index):
227 | def _resolver(val):
228 | state['results'][index] = val
229 | state['succeeded'] += 1
230 | _checkComplete()
231 |
232 | def _failer(err):
233 | state['errors'][index] = err
234 | state['failed'] += 1
235 | _checkComplete()
236 |
237 | return _resolver, _failer
238 |
239 | for i, f in enumerate(futures):
240 | cls.of(f).then(*_makeCallbacks(i))
241 | return merged
242 |
--------------------------------------------------------------------------------