summaryrefslogtreecommitdiff
path: root/editty/editty.py
diff options
context:
space:
mode:
Diffstat (limited to 'editty/editty.py')
-rw-r--r--editty/editty.py1134
1 files changed, 1134 insertions, 0 deletions
diff --git a/editty/editty.py b/editty/editty.py
new file mode 100644
index 0000000..cf4449b
--- /dev/null
+++ b/editty/editty.py
@@ -0,0 +1,1134 @@
1# -*- coding: utf-8 -*-
2# Copyright (C) 2019 James E. Blair <corvus@gnu.org>
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17import argparse
18import copy
19import os
20import sys
21import struct
22import time
23import threading
24import json
25import uuid
26try:
27 import queue
28except:
29 import Queue as queue
30import logging
31import math
32
33import urwid
34
35from editty.segment import *
36from editty.program import *
37import editty.source
38
39PALETTE = [
40 ('reversed', 'standout', ''),
41
42 ('timeline-title', 'dark cyan', ''),
43 ('selected-timeline-title', 'light cyan', ''),
44
45 ('timeline-1-text', 'dark cyan', ''),
46 ('timeline-1', 'black', 'dark cyan'),
47 ('timeline-1-selection', 'black,standout', 'dark cyan'),
48 ('selected-timeline-1-text', 'light cyan', ''),
49 ('selected-timeline-1', 'black', 'light cyan'),
50 ('selected-timeline-1-selection', 'black,standout', 'light cyan'),
51
52 ('timeline-2-text', 'dark magenta', ''),
53 ('timeline-2', 'black', 'dark magenta'),
54 ('timeline-2-selection', 'black,standout', 'dark magenta'),
55 ('selected-timeline-2-text', 'light magenta', ''),
56 ('selected-timeline-2', 'black', 'light magenta'),
57 ('selected-timeline-2-selection', 'black,standout', 'light magenta'),
58
59 ('timeline-3-text', 'dark green', ''),
60 ('timeline-3', 'black', 'dark green'),
61 ('timeline-3-selection', 'black,standout', 'dark green'),
62 ('selected-timeline-3-text', 'light green', ''),
63 ('selected-timeline-3', 'black', 'light green'),
64 ('selected-timeline-3-selection', 'black,standout', 'light green'),
65
66 ('timeline-4-text', 'brown', ''),
67 ('timeline-4', 'black', 'brown'),
68 ('timeline-4-selection', 'black,standout', 'brown'),
69 ('selected-timeline-4-text', 'yellow', ''),
70 ('selected-timeline-4', 'black', 'yellow'),
71 ('selected-timeline-4-selection', 'black,standout', 'yellow'),
72
73 ('timeline-black-text', 'dark gray', ''),
74 ('timeline-black', 'black', 'dark gray'),
75 ('timeline-black-selection', 'black,standout', 'dark gray'),
76 ('selected-timeline-black-text', 'light gray', ''),
77 ('selected-timeline-black', 'black', 'light gray'),
78 ('selected-timeline-black-selection', 'black,standout', 'light gray'),
79
80 ('start-timecode', 'dark cyan', ''),
81 ('selected-start-timecode', 'light cyan', ''),
82 ('current-timecode', 'light red', ''),
83 ('end-timecode', 'dark cyan', ''),
84 ('selected-end-timecode', 'light cyan', ''),
85]
86
87FOCUS_MAP = {}
88for x in PALETTE:
89 name = x[0]
90 if 'selected-'+name in [y[0] for y in PALETTE]:
91 FOCUS_MAP[name]='selected-'+name
92
93class Monitor(urwid.Widget):
94 _selectable = False
95 _sizing = frozenset([urwid.widget.FIXED])
96
97 signals = ['closed', 'beep', 'leds', 'title']
98
99 def __init__(self, editor, size):
100 self.log = logging.getLogger('monitor')
101 self.editor = editor
102 self.size = size
103 self.term_modes = urwid.TermModes()
104 self.term = urwid.TermCanvas(size[0], size[1], self)
105 self.term.modes.main_charset = urwid.vterm.CHARSET_UTF8
106 self.off_canvas = urwid.TextCanvas(maxcol=size[0], text=[b'' for x in range(size[1])])
107 self.off_canvas.cacheable = False
108 self.off = False
109
110 def render(self, size, focus=False):
111 if self.off:
112 return self.off_canvas
113 else:
114 return self.term
115
116 def pack(self, size, focus=False):
117 self.log.debug("pack")
118 return self.size
119
120 def rows(self, size, focus=False):
121 return self.size[1]
122
123 def keypress(self, size, key):
124 return None
125
126 def beep(self):
127 self._emit('beep')
128
129 def set_title(self, title):
130 self._emit('title', title)
131
132 def setFrame(self, frame):
133 self.term.term = [line[:] for line in frame.content]
134 self.term.set_term_cursor(frame.cursor[0], frame.cursor[1])
135 self.editor.loop.draw_screen()
136
137 def setTimecode(self, start, current, end):
138 if start is not None:
139 self._setTimecode(self.editor.screen.start_timecode,
140 'selected-start-timecode', start)
141 else:
142 self._setTimecode(self.editor.screen.start_timecode,
143 'start-timecode', 0.0)
144 self._setTimecode(self.editor.screen.timecode,
145 'current-timecode', current)
146 if end is not None:
147 self._setTimecode(self.editor.screen.end_timecode,
148 'selected-end-timecode', end)
149 else:
150 self._setTimecode(self.editor.screen.end_timecode,
151 'end-timecode', 0.0)
152
153 def _setTimecode(self, widget, attr, seconds):
154 hours, seconds = divmod(seconds, 60*60)
155 minutes, seconds = divmod(seconds, 60)
156 tc = '%02i:%02i:%09.6f' % (hours, minutes, seconds)
157 widget.set_text((attr, tc))
158
159class Timeline(urwid.Text):
160 _selectable = True
161
162 def __init__(self):
163 super(Timeline, self).__init__('')
164 self.log = logging.getLogger('timeline')
165 self.uuid = str(uuid.uuid4())
166 self.set_text('AB'*60)
167 self.log.debug("init")
168 self.scale = 1.0 # one second per char
169 self.hoffset = 0
170 self.current_width = 0
171 self.framecounts = []
172 self.framecolors = []
173 self.start_time = None
174 self.end_time = None
175 self.current_time = 0.0
176 self.current_frame = None
177 self.monitor = None
178 self.play_queue = queue.Queue()
179 self._thread = threading.Thread(target=self.run)
180 self._thread.daemon=True
181 self._thread.start()
182 self.playing = False
183 self.color = 'timeline-black'
184
185 def toJSON(self):
186 if self.program:
187 program = self.program.uuid
188 else:
189 program = None
190 return dict(uuid=self.uuid,
191 color=self.color,
192 program=program)
193
194 @classmethod
195 def fromJSON(cls, data):
196 t = Timeline()
197 t.uuid = data['uuid']
198 t.color = data['color']
199 return t
200
201 def setProgram(self, program):
202 self.program = program
203 self.setScale(self.scale)
204 for fi in program:
205 self.color = fi.frame.timeline_color
206 break
207
208 def setMonitor(self, monitor):
209 self.monitor = monitor
210 self.updateMonitor()
211
212 def updateMonitor(self, update_frame=True):
213 if not self.monitor:
214 return
215 if update_frame and self.current_frame:
216 self.monitor.setFrame(self.current_frame.frame)
217 self.monitor.setTimecode(self.start_time, self.current_time, self.end_time)
218 screen = self.monitor.editor.screen
219 if self.current_frame:
220 screen.md_segment.set_text(str(self.current_frame.segment_index+1))
221 screen.md_type.set_text(self.current_frame.segment.__class__.__name__)
222 if hasattr(self.current_frame.segment, 'source'):
223 screen.md_source.set_text(self.current_frame.segment.source.title)
224 else:
225 screen.md_source.set_text('')
226 screen.md_segment_start.set_text('%0.6f' % (self.current_frame.segment.start))
227 screen.md_segment_end.set_text('%0.6f' % (self.current_frame.segment.end))
228 screen.md_segment_duration.set_text('%0.6f' % (self.current_frame.segment.duration))
229 screen.md_frame.set_text(str(self.current_frame.frame_index+1))
230 screen.md_cursor.set_text(self.current_frame.segment.visible_cursor and 'visible' or 'hidden')
231
232 def setScale(self, scale):
233 self.scale = scale
234 count = int(math.ceil(self.program.length / self.scale))
235 self.framecounts = [0 for x in range(count)]
236 self.framecolors = ['' for x in range(count)]
237 elapsed = 0.0
238 for fi in self.program:
239 pos = int(math.floor(fi.timecode / self.scale))
240 self.framecounts[pos] += 1
241 self.framecolors[pos] = fi.frame.timeline_color
242 for i in range(len(self.framecolors)):
243 if i and not self.framecolors[i]:
244 self.framecolors[i] = self.framecolors[i-1]
245 self.set_text('')
246
247 def pack(self, size, focus=False):
248 self.log.debug("pack %s %s", size, focus)
249
250 def rows(self, size, focus=False):
251 return 1
252
253 def render(self, size, focus=False):
254 title = '%-10s' % self.program.title[:10]
255 title_len = len(title)
256 text_attr = self.color + '-text'
257 s = [(text_attr, title)]
258 current_pos = int(math.floor(self.current_time / self.scale))
259 start_pos = end_pos = current_pos
260
261 range_points = 0
262 if self.start_time is not None:
263 start_pos = int(math.floor(self.start_time / self.scale))
264 range_points += 1
265 if self.end_time is not None:
266 end_pos = int(math.floor(self.end_time / self.scale))
267 range_points += 1
268 selection_range = sorted([start_pos, end_pos])
269
270 self.current_width = size[0]-title_len-2
271 if current_pos > self.current_width+self.hoffset-1:
272 self.hoffset = current_pos-self.current_width+1
273 if current_pos < self.hoffset:
274 self.hoffset = current_pos
275 left_arrow = self.hoffset>0 and '<' or ' '
276 right_arrow = self.hoffset+self.current_width<len(self.framecounts) and '>' or ' '
277 s.append((text_attr, left_arrow))
278 for i in range(self.hoffset, self.hoffset+self.current_width):
279 if i < len(self.framecounts):
280 if self.framecounts[i] > 1:
281 char = '•' #▪▬■·•‧
282 elif self.framecounts[i] > 0:
283 char = '‧'
284 else:
285 char = ' '
286 attr = self.framecolors[i] or self.color
287 else:
288 char = ' '
289 attr = ''
290 if selection_range[0] <= i < selection_range[1]:
291 if range_points == 2 and i == current_pos:
292 pass
293 else:
294 attr += '-selection'
295 elif i == current_pos:
296 attr += '-selection'
297 s.append((attr, char))
298 s.append((text_attr, right_arrow))
299 self.set_text(s)
300 return super(Timeline, self).render(size, focus)
301
302 def updateCurrentFrame(self):
303 pass
304
305 def move(self, offset):
306 self.log.debug('move %s time %s', offset, self.current_time)
307 if abs(offset) >= 1:
308 new_time = self.current_time + (offset * self.scale)
309 else:
310 new_time = self.current_time + offset
311 new_time = max(0.0, min(self.program.length, new_time))
312 if abs(offset) >= 1:
313 new_time = math.floor(new_time / self.scale) * self.scale
314 # Find the closest frame no later than the new time
315 (prv, cur, nxt) = self.program.getFramesAtTimecode(new_time)
316 self.log.debug('move %s %s %s', prv, cur, nxt)
317 self.current_time = new_time
318 if cur is None:
319 if prv is None:
320 self.current_frame = None
321 else:
322 self.current_frame = prv
323 else:
324 self.current_frame = cur
325 # If our cell has at least one frame in it, jump ahead to the
326 # first frame.
327 if abs(offset) >= 1 and self.current_time > cur.timecode and nxt:
328 if self.current_time + self.scale > nxt.timecode:
329 self.current_time = nxt.timecode
330 self.current_frame = nxt
331 self.updateMonitor()
332 self.set_text('')
333 self.log.debug('move %s time %s', offset, self.current_time)
334
335 def setStart(self):
336 self.start_time = self.current_time
337 self.set_text('')
338 self.updateMonitor(update_frame=False)
339
340 def setEnd(self):
341 if self.current_time + self.scale > self.program.length:
342 # If we're in the last cell, the user probably meant to go
343 # to the end.
344 self.end_time = self.program.length
345 else:
346 self.end_time = self.current_time
347 self.set_text('')
348 self.updateMonitor(update_frame=False)
349
350 def clearSelection(self):
351 self.start_time = None
352 self.end_time = None
353 self.set_text('')
354 self.updateMonitor(update_frame=False)
355
356 # Edit
357 def cut(self):
358 self.log.debug("cut")
359 if self.start_time is None or self.end_time is None:
360 return
361 self.monitor.editor.saveUndo("Cut", (self.program,))
362 saved = self.program.cut(self.start_time, self.end_time)
363 self.monitor.editor.setClipboard(saved)
364 if self.current_time > self.end_time:
365 self.current_time -= (self.end_time - self.start_time)
366 elif self.current_time > self.start_time:
367 self.current_time = self.start_time
368 self.clearSelection()
369 self.setScale(self.scale)
370 self.move(0)
371
372 def insert(self):
373 self.log.debug("insert")
374 if self.monitor.editor.clipboard is None:
375 return
376 self.monitor.editor.saveUndo("Insert", (self.program,))
377 self.program.insert(self.current_time, self.monitor.editor.clipboard)
378 self.setScale(self.scale)
379 self.move(0)
380
381 def append(self):
382 self.log.debug("append")
383 if self.monitor.editor.clipboard is None:
384 return
385 self.monitor.editor.saveUndo("Append", (self.program,))
386 self.program.append(self.monitor.editor.clipboard)
387 self.setScale(self.scale)
388 self.move(0)
389
390 def dissolve(self):
391 self.log.debug("dissolve")
392 if self.start_time is None or self.end_time is None:
393 return
394 self.monitor.editor.saveUndo("Dissolve", (self.program,))
395 try:
396 self.program.dissolve(self.start_time, self.end_time)
397 self.clearSelection()
398 self.setScale(self.scale)
399 self.move(0)
400 except Exception as e:
401 self.monitor.editor.undo()
402 self.monitor.editor.message("Error", str(e))
403
404 def toggleCursor(self):
405 self.log.debug("cursor")
406 self.monitor.editor.saveUndo("Toggle Cursor", (self.program,))
407 self.current_frame.segment.visible_cursor = not self.current_frame.segment.visible_cursor
408 self.move(0)
409
410 # Playback
411 def play(self):
412 self.log.debug("play")
413 self.playing = True
414 if self.end_time is not None:
415 end = self.end_time
416 else:
417 end = self.program.length
418 if self.start_time is not None:
419 start = self.start_time
420 else:
421 if self.current_time == end:
422 start = 0.0
423 else:
424 start = self.current_time
425 self.play_queue.put((start, end))
426
427 def stop(self):
428 self.log.debug("stop")
429 self.playing = False
430
431 # Playback thread
432 def run(self):
433 while True:
434 item = self.play_queue.get()
435 if item is None:
436 return
437 self._runPlay(item)
438 self.playing = False
439
440 def _runClock(self, start_wallclock_time, start_timecode, end_delay):
441 while True:
442 if not self.playing: return
443 cur_wallclock_time = time.time()
444 if cur_wallclock_time >= end_delay: break
445 elapsed_wallclock_time = cur_wallclock_time - start_wallclock_time
446 self.current_time = start_timecode + elapsed_wallclock_time
447 self.updateMonitor(update_frame=False)
448 self.set_text('')
449 self.monitor.editor.loop.draw_screen()
450 time.sleep(0)
451
452 def _runPlay(self, item):
453 start_timecode, end_timecode = item
454 self.log.debug("play %s %s", start_timecode, end_timecode)
455 self.current_time = start_timecode
456 self.move(0)
457 self.monitor.editor.loop.draw_screen()
458 start_wallclock_time = time.time()
459 first = True
460 for fi in self.program.getFrames(start_timecode, end_timecode):
461 end_delay = start_wallclock_time + fi.timecode - start_timecode
462 self._runClock(start_wallclock_time, start_timecode, end_delay)
463 if not self.playing: return
464 self.current_frame = fi
465 if not first:
466 self.monitor.setFrame(self.current_frame.frame)
467 self.monitor.editor.loop.draw_screen()
468 else:
469 first = False
470 end_delay = start_wallclock_time + end_timecode - start_timecode
471 self._runClock(start_wallclock_time, start_timecode, end_delay)
472 if not self.playing: return
473 self.current_time = end_timecode
474 self.updateMonitor()
475 self.monitor.editor.loop.draw_screen()
476 self.log.debug("done play %s", self.current_time)
477
478 def keypress(self, size, key):
479 self.log.debug(repr(key))
480 if self.playing and key != ' ':
481 return None
482 if key == (' '):
483 if self.playing:
484 self.stop()
485 else:
486 self.play()
487 elif key == '[':
488 self.setStart()
489 elif key == ']':
490 self.setEnd()
491 elif key == 'esc':
492 self.clearSelection()
493 elif key == 'right':
494 self.move(1)
495 elif key == 'meta right':
496 self.move(10)
497 elif key == 'shift right':
498 self.move(0.01)
499 elif key == 'left':
500 self.move(-1)
501 elif key == 'meta left':
502 self.move(-10)
503 elif key == 'shift left':
504 self.move(-0.01)
505 elif key == '=':
506 self.setScale(self.scale / 2)
507 elif key == '-':
508 self.setScale(self.scale * 2)
509 elif key == 'x':
510 self.cut()
511 elif key == 'i':
512 self.insert()
513 elif key == 'a':
514 self.append()
515 elif key == 'd':
516 self.dissolve()
517 elif key == 'C':
518 self.toggleCursor()
519 elif key == 'ctrl r':
520 self.monitor.editor.render(self.program)
521 return key
522
523class Screen(urwid.WidgetWrap):
524 def __init__(self, editor, size):
525 super(Screen, self).__init__(urwid.Pile([]))
526 self.log = logging.getLogger('screen')
527 self.editor = editor
528 self.size = size
529
530 self.monitor = Monitor(editor, self.size)
531 self.start_timecode = urwid.Text('00:00:00.000000', align='center')
532 self.timecode = urwid.Text('00:00:00.000000', align='center')
533 self.end_timecode = urwid.Text('00:00:00.000000', align='center')
534 self.timecode_cols = urwid.Columns([
535 ('weight', 1, urwid.Text(' ')),
536 (15, self.start_timecode),
537 (17, urwid.Text(' ')),
538 (15, self.timecode),
539 (17, urwid.Text(' ')),
540 (15, self.end_timecode),
541 ('weight', 1, urwid.Text(' ')),
542 ])
543 self.timeline = None
544 self.md_segment = urwid.Text('')
545 self.md_type = urwid.Text('')
546 self.md_source = urwid.Text('')
547 self.md_segment_start = urwid.Text('')
548 self.md_segment_end = urwid.Text('')
549 self.md_segment_duration = urwid.Text('')
550 self.md_frame = urwid.Text('')
551 self.md_cursor = urwid.Text('')
552 metadata_pile = urwid.Pile([
553 ('pack', urwid.Columns([(11, urwid.Text('Segment: ')), ('weight', 1, self.md_segment)])),
554 ('pack', urwid.Columns([(11, urwid.Text('Type: ')), ('weight', 1, self.md_type)])),
555 ('pack', urwid.Columns([(11, urwid.Text('Source: ')), ('weight', 1, self.md_source)])),
556 ('pack', urwid.Columns([(11, urwid.Text('Start: ')), ('weight', 1, self.md_segment_start)])),
557 ('pack', urwid.Columns([(11, urwid.Text('End: ')), ('weight', 1, self.md_segment_end)])),
558 ('pack', urwid.Columns([(11, urwid.Text('Duration: ')), ('weight', 1, self.md_segment_duration)])),
559 ('pack', urwid.Columns([(11, urwid.Text('Frame: ')), ('weight', 1, self.md_frame)])),
560 ('pack', urwid.Columns([(11, urwid.Text('Cursor: ')), ('weight', 1, self.md_cursor)])),
561 ])
562 program_box = urwid.LineBox(self.monitor, "Monitor")
563 metadata_box = urwid.LineBox(metadata_pile)
564 border = 2
565 monitor_columns = urwid.Columns([
566 #('weight', 1, urwid.Filler(urwid.Text(''))),
567 (self.size[0]+border, program_box),
568 metadata_box,
569 #('weight', 1, urwid.Filler(urwid.Text(''))),
570 ])
571 lw = urwid.SimpleFocusListWalker([])
572 self.timelines = urwid.ListBox(lw)
573 urwid.connect_signal(lw, 'modified', self._updateFocus)
574 self.main = urwid.Pile([])
575 self.main.contents.append((monitor_columns, ('given', self.size[1]+border)))
576 self.main.contents.append((self.timecode_cols, ('pack', None)))
577 self.main.contents.append((self.timelines, ('weight', 1)))
578 self.main.contents.append((urwid.Filler(urwid.Text(u'')), ('weight', 1)))
579 self.main.set_focus(2)
580
581 self._w.contents.append((self.main, ('weight', 1)))
582 self._w.set_focus(0)
583
584 def addTimeline(self, program, timeline=None):
585 if timeline is None:
586 timeline = Timeline()
587 timeline.setProgram(program)
588 timeline.setMonitor(self.monitor)
589 w = urwid.AttrMap(timeline, None, focus_map=FOCUS_MAP)
590 self.timelines.body.append(w)
591 self.timeline = timeline
592 self.timelines.set_focus(len(self.timelines.body)-1)
593
594 def getTimelines(self):
595 for w in self.timelines.body:
596 yield w.original_widget
597
598 def removeTimeline(self, timeline):
599 if timeline == self.timeline:
600 self.timeline = None
601 for w in self.timelines.body:
602 if w.original_widget == timeline:
603 self.timelines.body.remove(w)
604 return
605
606 def _updateFocus(self):
607 self.log.debug("update focus")
608 if self.timeline:
609 self.log.debug("clear monitor %s", self.timeline)
610 self.timeline.setMonitor(None)
611 if self.timelines.focus:
612 self.timeline = self.timelines.focus.original_widget
613 self.log.debug("set focus %s", self.timeline)
614 self.timeline.setMonitor(self.monitor)
615 self.log.debug("set monitor %s", self.timeline)
616 else:
617 self.log.debug("no timeline in focus")
618
619 def keypress(self, size, key):
620 self.log.debug(repr(key))
621 if self.timeline and self.timeline.playing and key != ' ':
622 return None
623 if key == 'ctrl l':
624 self.editor.load()
625 elif key == 'ctrl o':
626 self.editor.open()
627 elif key == 'ctrl _':
628 self.editor.undo()
629 elif key == 'ctrl s':
630 self.editor.save()
631 return super(Screen, self).keypress(size, key)
632
633class MainLoop(urwid.MainLoop):
634 def __init__(self, *args, **kw):
635 self._screen_lock = threading.RLock()
636 super(MainLoop, self).__init__(*args, **kw)
637
638 def draw_screen(self):
639 with self._screen_lock:
640 super(MainLoop, self).draw_screen()
641
642class UndoRecord:
643 def __init__(self, description, programs):
644 self.description = description
645 self.programs = programs
646
647class FixedButton(urwid.Button):
648 def sizing(self):
649 return frozenset([urwid.FIXED])
650
651 def pack(self, size, focus=False):
652 return (len(self.get_label())+4, 1)
653
654class LoadDialog(urwid.WidgetWrap):
655 def __init__(self, editor):
656 self.editor = editor
657 self.ok = FixedButton("Load")
658 self.cancel = FixedButton("Cancel")
659 urwid.connect_signal(self.ok, 'click', editor._finishLoad, self)
660 urwid.connect_signal(self.cancel, 'click', editor._clearPopup, self)
661
662 file_types = []
663 self.type_buttons = []
664 self.button_types = {}
665 for ft in editty.source.all_types:
666 b = urwid.RadioButton(file_types, ft.name)
667 self.type_buttons.append(b)
668 self.button_types[b] = ft
669 for b in self.type_buttons:
670 urwid.connect_signal(b, 'postchange', self.setType)
671 self.current_type = None
672
673 buttons = urwid.Columns([
674 ('pack', self.ok),
675 ('pack', urwid.Text(' ')),
676 ('pack', self.cancel),
677 ])
678 self.stream_file = urwid.Edit("")
679 self.timing_file = urwid.Edit("Timing file: ")
680 self.listbox = urwid.ListBox([
681 urwid.Text('File type:'),
682 ] + self.type_buttons + [
683 urwid.Text(''),
684 self.stream_file,
685 urwid.Text(''),
686 buttons,
687 ])
688 self.setType(self.current_type, self)
689 super(LoadDialog, self).__init__(urwid.LineBox(self.listbox, 'Load'))
690
691 def setType(self, button, dialog):
692 # Only handle the second event
693 selected = [b for b in self.type_buttons if b.state]
694 if len(selected) > 1:
695 return
696
697 selected = selected[0]
698 self.current_type = self.button_types[selected]
699
700 if not self.current_type.timing:
701 if self.timing_file in self.listbox.body:
702 self.listbox.body.remove(self.timing_file)
703 else:
704 if self.timing_file not in self.listbox.body:
705 loc = self.listbox.body.index(self.stream_file)
706 self.listbox.body.insert(loc+1, self.timing_file)
707 self.stream_file.set_caption('%s file: ' % self.current_type.name)
708
709 def keypress(self, size, key):
710 if key == 'esc':
711 self.editor._clearPopup()
712 return None
713 if key == 'enter':
714 if(self.current_type.timing and
715 self.listbox.focus is self.stream_file):
716 return self.keypress(size, 'down')
717 if(self.listbox.focus is self.timing_file or
718 self.listbox.focus is self.stream_file):
719 return self.ok.keypress(size, key)
720 return super(LoadDialog, self).keypress(size, key)
721
722class SaveDialog(urwid.WidgetWrap):
723 def __init__(self, editor):
724 self.editor = editor
725 self.ok = FixedButton("Save")
726 self.cancel = FixedButton("Cancel")
727 urwid.connect_signal(self.ok, 'click', editor._finishSave, self)
728 urwid.connect_signal(self.cancel, 'click', editor._clearPopup, self)
729 buttons = urwid.Columns([
730 ('pack', self.ok),
731 ('pack', urwid.Text(' ')),
732 ('pack', self.cancel),
733 ])
734 self.project_file = urwid.Edit("Project file: ")
735 self.listbox = urwid.ListBox([
736 self.project_file,
737 urwid.Text(''),
738 buttons,
739 ])
740 super(SaveDialog, self).__init__(urwid.LineBox(self.listbox, 'Save'))
741
742 def keypress(self, size, key):
743 if key == 'esc':
744 self.editor._clearPopup()
745 return None
746 if key == 'enter':
747 if(self.listbox.focus is self.project_file):
748 return self.ok.keypress(size, key)
749 return super(SaveDialog, self).keypress(size, key)
750
751class OpenDialog(urwid.WidgetWrap):
752 def __init__(self, editor):
753 self.editor = editor
754 self.ok = FixedButton("Open")
755 self.cancel = FixedButton("Cancel")
756 urwid.connect_signal(self.ok, 'click', editor._finishOpen, self)
757 urwid.connect_signal(self.cancel, 'click', editor._clearPopup, self)
758 buttons = urwid.Columns([
759 ('pack', self.ok),
760 ('pack', urwid.Text(' ')),
761 ('pack', self.cancel),
762 ])
763 self.project_file = urwid.Edit("Project file: ")
764 self.listbox = urwid.ListBox([
765 self.project_file,
766 urwid.Text(''),
767 buttons,
768 ])
769 super(OpenDialog, self).__init__(urwid.LineBox(self.listbox, 'Open'))
770
771 def keypress(self, size, key):
772 if key == 'esc':
773 self.editor._clearPopup()
774 return None
775 if key == 'enter':
776 if(self.listbox.focus is self.project_file):
777 return self.ok.keypress(size, key)
778 return super(OpenDialog, self).keypress(size, key)
779
780class RenderDialog(urwid.WidgetWrap):
781 def __init__(self, editor, program):
782 self.editor = editor
783 self.program = program
784 self.ok = FixedButton("Render")
785 self.cancel = FixedButton("Cancel")
786 urwid.connect_signal(self.ok, 'click', editor._finishRender, self)
787 urwid.connect_signal(self.cancel, 'click', editor._clearPopup, self)
788 buttons = urwid.Columns([
789 ('pack', self.ok),
790 ('pack', urwid.Text(' ')),
791 ('pack', self.cancel),
792 ])
793 self.ttyrec_file = urwid.Edit("Ttyrec file: ")
794 self.listbox = urwid.ListBox([
795 self.ttyrec_file,
796 urwid.Text(''),
797 buttons,
798 ])
799 super(RenderDialog, self).__init__(urwid.LineBox(self.listbox, 'Render'))
800
801 def keypress(self, size, key):
802 if key == 'esc':
803 self.editor._clearPopup()
804 return None
805 if key == 'enter':
806 if(self.listbox.focus is self.ttyrec_file):
807 return self.ok.keypress(size, key)
808 return super(RenderDialog, self).keypress(size, key)
809
810class QuitDialog(urwid.WidgetWrap):
811 def __init__(self, editor):
812 self.editor = editor
813 self.yes = FixedButton("Yes")
814 self.no = FixedButton("No")
815 urwid.connect_signal(self.yes, 'click', editor._quit, self)
816 urwid.connect_signal(self.no, 'click', editor._clearPopup, self)
817 buttons = urwid.Columns([
818 ('pack', self.yes),
819 ('pack', urwid.Text(' ')),
820 ('pack', self.no),
821 ])
822 self.listbox = urwid.ListBox([
823 urwid.Text('Are you sure you want to quit?\n'),
824 buttons,
825 ])
826 super().__init__(urwid.LineBox(self.listbox, 'Quit'))
827
828 def keypress(self, size, key):
829 if key == 'esc':
830 self.editor._clearPopup()
831 return None
832 return super().keypress(size, key)
833
834class MessageDialog(urwid.WidgetWrap):
835 def __init__(self, editor, title, message):
836 self.editor = editor
837 ok = FixedButton("OK")
838 urwid.connect_signal(ok, 'click', editor._clearPopup, self)
839 buttons = urwid.Columns([
840 ('pack', ok),
841 ])
842 listbox = urwid.ListBox([
843 urwid.Text(message),
844 urwid.Text(''),
845 buttons,
846 ])
847 super(MessageDialog, self).__init__(urwid.LineBox(listbox, title))
848
849 def keypress(self, size, key):
850 if key == 'esc':
851 self.editor._clearPopup()
852 return None
853 return super().keypress(size, key)
854
855class Editor(object):
856 def __init__(self, args):
857 self.size = (args.width, args.height)
858 if args.debuglog:
859 logfile = os.path.expanduser(args.debuglog)
860 logging.basicConfig(filename=logfile, level=logging.DEBUG)
861 self.log = logging.getLogger('editor')
862 self.screen = Screen(self, self.size)
863 self.loop = MainLoop(self.screen, palette=PALETTE,
864 unhandled_input=self.unhandledInput)
865 self.loop.screen.tty_signal_keys(start='undefined', stop='undefined')
866 self.loop.screen.set_terminal_properties(colors=256)
867 self.loop.screen.start()
868 self.clipboard = None
869 self.programs = []
870 self.undo_history = []
871 self.timeline_color_generator = self._timelineColorGenerator()
872 self.output_program = Program('Output')
873 if args.project:
874 self._open(args.project)
875
876 def _timelineColorGenerator(self):
877 while True:
878 for i in range(1, 5):
879 yield 'timeline-%i' % i
880
881 def setSize(self, size):
882 self.size = size
883 self.screen = Screen(self, self.size)
884 self.loop.widget = self.screen
885
886 def help(self):
887 msg = ('Use the arrow keys to move left and right in the timeline. '
888 'Use meta-arrows to move longer distances and shift-arrows '
889 'to move shorter distances.\n\n')
890 for key, desc in [
891 ('CTRL-l', 'Load terminal recording'),
892 ('CTRL-o', 'Open Editty program file'),
893 ('CTRL-s', 'Save Editty program file'),
894 ('CTRL-r', 'Render'),
895 ('CTRL-_', 'Undo'),
896 ('CTRL-q', 'Quit'),
897 ('SPACE', 'Play'),
898 ('[', 'Set selection start'),
899 (']', 'Set selection end'),
900 ('ESC', 'Clear start/end points'),
901 ('ARROW', 'Move left/right'),
902 ('META-ARROW', 'Move 10x left/right'),
903 ('SHIFT-ARROW', 'Move 0.01x left/right'),
904 ('=', 'Zoom in timescale'),
905 ('-', 'Zoom out timescale'),
906 ('x', 'Cut selection to clipboard'),
907 ('a', 'Append clipboard contents to end of timeline'),
908 ('i', 'Insert clipboard contents to end of timeline'),
909 ('d', 'Dissolve between start and end of selection'),
910 ('C', 'Toggle whether cursor is visible in this segment'),
911 ]:
912 msg += '%-11s %s\n' % (key, desc)
913 self.message("Help", msg, min_width=60, width=30, height=50)
914
915 def unhandledInput(self, key):
916 if key == 'f1':
917 self.help()
918 elif key == 'ctrl q':
919 self.quit()
920
921 def quit(self):
922 dialog = QuitDialog(self)
923 self.screen.monitor.off = True
924 overlay = urwid.Overlay(dialog, self.screen,
925 'center', ('relative', 50),
926 'middle', ('relative', 25))
927 self.loop.widget = overlay
928
929 def _quit(self, *args, **kw):
930 raise urwid.ExitMainLoop()
931
932 def saveUndo(self, description, programs):
933 # Make an undo record, programs is a list of programs which
934 # are about to change.
935 saved_programs = []
936 program_timelines = {}
937 for timeline in self.screen.getTimelines():
938 self.log.debug("save undo %s %s", timeline.program, timeline.uuid)
939 if timeline.program:
940 program_timelines[timeline.program] = timeline.uuid
941 for p in self.programs:
942 timeline_uuid = program_timelines.get(p)
943 if p in programs:
944 p = p.copy()
945 saved_programs.append((p, timeline_uuid))
946 ur = UndoRecord(description, saved_programs)
947 self.undo_history.append(ur)
948
949 def undo(self):
950 undorecord = self.undo_history.pop()
951 current_timelines = set(self.screen.getTimelines())
952 self.programs = []
953 self.log.debug("undo %s", undorecord.description)
954 self.log.debug("undo %s", undorecord.programs)
955 self.log.debug("current %s", current_timelines)
956 for (program, timeline_uuid) in undorecord.programs:
957 self.programs.append(program)
958 for t in current_timelines:
959 self.log.debug("undo %s %s", t.uuid, timeline_uuid)
960 if t.uuid == timeline_uuid:
961 self.log.debug("found")
962 current_timelines.remove(t)
963 t.setProgram(program)
964 break
965 else:
966 self.log.debug("undo added new timeline for %s", program)
967 self.screen.addTimeline(program)
968 for t in current_timelines:
969 self.log.debug("removed unused timeline %s", t)
970 self.screen.removeTimeline(t)
971
972 def setClipboard(self, program):
973 self.clipboard = program
974
975 def run(self):
976 self.loop.run()
977
978 def _clearPopup(self, *args, **kw):
979 self.screen.monitor.off = False
980 self.loop.widget = self.screen
981 self.loop.draw_screen()
982
983 def message(self, title, message, width=50, height=25,
984 min_width=None, min_height=None):
985 dialog = MessageDialog(self, title, message)
986 self.screen.monitor.off = True
987 overlay = urwid.Overlay(dialog, self.screen,
988 'center', ('relative', width),
989 'middle', ('relative', height),
990 min_width=min_width,
991 min_height=min_height)
992
993 self.loop.widget = overlay
994
995 def load(self):
996 dialog = LoadDialog(self)
997 self.screen.monitor.off = True
998 overlay = urwid.Overlay(dialog, self.screen,
999 'center', ('relative', 50),
1000 'middle', ('relative', 25))
1001 self.loop.widget = overlay
1002
1003 def _load(self, file_type, stream_file, timing_file, program=None):
1004 color = next(self.timeline_color_generator)
1005 source = file_type.load(self.size, stream_file, timing_file, color)
1006 if program is None:
1007 program = Program(source.title)
1008 program.append(Clip(source, 0.0, source.length))
1009 self.saveUndo("Load %s" % stream_file, [])
1010 self.programs.append(program)
1011 self.screen.addTimeline(program)
1012
1013 def _finishLoad(self, button, dialog):
1014 self._clearPopup()
1015 try:
1016 self._load(dialog.current_type,
1017 dialog.stream_file.edit_text,
1018 dialog.timing_file.edit_text)
1019 except Exception as e:
1020 self.message("Error", str(e))
1021
1022 def save(self):
1023 dialog = SaveDialog(self)
1024 self.screen.monitor.off = True
1025 overlay = urwid.Overlay(dialog, self.screen,
1026 'center', ('relative', 50),
1027 'middle', ('relative', 25))
1028 self.loop.widget = overlay
1029
1030 def _save(self, project_file):
1031 data = {'version': 1}
1032 data['size'] = self.size
1033 data['sources'] = []
1034 data['programs'] = []
1035 data['timelines'] = []
1036 sources = set()
1037 programs = set()
1038 for timeline in self.screen.getTimelines():
1039 data['timelines'].append(timeline.toJSON())
1040 if timeline.program:
1041 programs.add(timeline.program)
1042 for program in programs:
1043 data['programs'].append(program.toJSON())
1044 for segment in program.segments:
1045 if hasattr(segment, 'source'):
1046 sources.add(segment.source)
1047 for source in sources:
1048 data['sources'].append(source.toJSON())
1049 with open(project_file, 'w') as f:
1050 f.write(json.dumps(data))
1051
1052 def _finishSave(self, button, dialog):
1053 pf = dialog.project_file.edit_text
1054 self._clearPopup()
1055 self.log.debug("save %s", pf)
1056 try:
1057 self._save(pf)
1058 except Exception as e:
1059 self.message("Error", str(e))
1060
1061 def open(self):
1062 dialog = OpenDialog(self)
1063 self.screen.monitor.off = True
1064 overlay = urwid.Overlay(dialog, self.screen,
1065 'center', ('relative', 50),
1066 'middle', ('relative', 25))
1067 self.loop.widget = overlay
1068
1069 def _open(self, project_file):
1070 with open(project_file, 'r') as f:
1071 data = json.loads(f.read())
1072
1073 for timeline in list(self.screen.getTimelines()):
1074 self.screen.removeTimeline(timeline)
1075 size = data['size']
1076 self.setSize(size)
1077 sources = {}
1078 for source in data['sources']:
1079 sc = editty.source.SourceClip.fromJSON(source)
1080 sources[sc.uuid] = sc
1081 programs = {}
1082 for program in data['programs']:
1083 p = Program.fromJSON(program, sources)
1084 programs[p.uuid] = p
1085 self.programs.append(p)
1086 for timeline in data['timelines']:
1087 t = Timeline.fromJSON(timeline)
1088 program = programs.get(timeline['program'])
1089 self.screen.addTimeline(program, timeline=t)
1090
1091 def _finishOpen(self, button, dialog):
1092 pf = dialog.project_file.edit_text
1093 self._clearPopup()
1094 self.log.debug("open %s", pf)
1095 try:
1096 self._open(pf)
1097 except Exception as e:
1098 self.message("Error", str(e))
1099
1100 def render(self, program):
1101 dialog = RenderDialog(self, program)
1102 self.screen.monitor.off = True
1103 overlay = urwid.Overlay(dialog, self.screen,
1104 'center', ('relative', 50),
1105 'middle', ('relative', 25))
1106 self.loop.widget = overlay
1107
1108 def _render(self, ttyrec, program):
1109 program.render_ttyrec(self.size, ttyrec)
1110
1111 def _finishRender(self, button, dialog):
1112 tf = dialog.ttyrec_file.edit_text
1113 program = dialog.program
1114 self._clearPopup()
1115 self.log.debug("render %s", tf)
1116 try:
1117 self._render(tf, program)
1118 except Exception as e:
1119 self.message("Error", str(e))
1120
1121def main():
1122 parser = argparse.ArgumentParser(
1123 description='')
1124 parser.add_argument('--width', type=int, default=80,
1125 help='Screen width')
1126 parser.add_argument('--height', type=int, default=24,
1127 help='Screen height')
1128 parser.add_argument('--debuglog',
1129 help='Debug log file path')
1130 parser.add_argument('project', nargs='?',
1131 help='project file')
1132 args = parser.parse_args()
1133 e = Editor(args)
1134 e.run()