-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtkui.py
More file actions
1079 lines (895 loc) · 36.4 KB
/
Copy pathtkui.py
File metadata and controls
1079 lines (895 loc) · 36.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import abc
from enum import Enum
from heapq import merge
from midi import Event
from modes import MidiState
from tkinter import Button, Entry, Frame, Label, Listbox, Menu, Menubutton, \
Text, Tk, Toplevel, Widget, BOTH, LEFT, NORMAL, NSEW, RAISED, W
from typing import Callable, List, Tuple
from awb_client import offsetEventTimes, AWBClient, CallableEvent, ACTIVE, \
NONEMPTY, RECORD, STICKY
from commands import Program, ProgramCommands, ScriptInterpreter
import time
import traceback
class Channel(Frame):
"""These are the "classic channels", audio config and recording.
Stuff a channel tracks:
- The track number
- Recording
- Active
- Muted
- Sticky
- Non-empty
"""
def __init__(self, parent, trackNumber):
Frame.__init__(self, parent, relief = RAISED, borderwidth = 3,
background = 'black')
self.channel = trackNumber
self.record = False
self.nonEmpty = False
self.active = False
self.sticky = False
self.label = Label(self)
self.label.pack()
self.__updateStatus()
self.configure(relief = RAISED)
def __updateStatus(self):
# First and last characters are brackets for the active track.
# Second character is 'R' for recording, 'P' for playing and ' ' for
# empty. Third character is '*' for sticky, ' ' if not.
self.label.configure(text = '%d: %s%s%s%s' % (
self.channel,
self.active and '[' or ' ',
self.record and 'R' or
self.nonEmpty and 'P' or
' ',
self.sticky and '*' or ' ',
self.active and ']' or ' '
))
def changeStatus(self, status):
self.nonEmpty = status & NONEMPTY
self.record = status & RECORD
self.sticky = status & STICKY
self.active = status & ACTIVE
print('changing status of %d, nonempty = %s, record = %s, '
'sticky = %s, active = %s' % (self.channel, self.nonEmpty,
self.record, self.sticky,
self.active))
self.__updateStatus()
class SelectionClient(abc.ABC):
"""Interface for a window that initiates a TextSelect popup.
Contains callbacks for when the popup completes.
"""
@abc.abstractmethod
def selected(self, selection):
"""Called when an item is selected."""
@abc.abstractmethod
def aborted(self):
"""Called when the text selection is aborted (user has pressed
"Escape")
"""
class ProgramChanger(Frame, SelectionClient):
"""Widget to specify a bank/program change event (a MidiState object)."""
def __init__(self, parent: 'Widget', client: 'AWBClient',
model: MidiState,
onCommit: Callable[[MidiState], None],
**kwargs):
super(ProgramChanger, self).__init__(parent, **kwargs)
self.__parent = parent
self.__awb = client
self.__orgModel = model
self.__newModel = model.clone()
self.__onCommit = onCommit
lbl = Label(self, text = 'Port:')
lbl.grid(row = 0, column = 0)
self.__port = Button(self, text=model.portName,
command=self.__selectPort
)
self.__port.grid(row = 0, column = 1)
def validateInt(val):
if val == '':
# Allow empty string so that we can comfortably edit this
# field.
return True
try:
val = int(val)
return True
except ValueError:
return False
validateIntCB = self.register(validateInt)
lbl = Label(self, text='Bank:')
lbl.grid(row = 1, column = 0)
self.__bank = Entry(self, validate='key',
validatecommand=(validateIntCB, '%P')
)
self.__bank.insert(0, str(model.bank))
self.__bank.grid(row = 1, column = 1)
lbl = Label(self, text='Program:')
lbl.grid(row = 2, column = 0)
self.__program = Entry(self, validate='key',
validatecommand=(validateIntCB, '%P')
)
self.__program.insert(0, str(model.program))
self.__program.grid(row=2, column=1)
btn = Button(self, text='-', command=self.__programDec)
btn.grid(row=2, column=2)
btn = Button(self, text='+', command=self.__programInc)
btn.grid(row=2, column=3)
btnRow = Frame(self)
btn = Button(btnRow, text='Save', command=self.__save)
btn.pack(side=LEFT)
btn = Button(btnRow, text='Cancel', command=self.__cancel)
btnRow.grid(row=3, column=0, columnspan=4)
def __save(self, *event):
self.__newModel.bank = self.__getIntValue(self.__bank)
self.__newModel.program = self.__getIntValue(self.__program)
self.__onCommit(self.__newModel)
# TODO: We destroy the parent, making the assumption that it is a
# toplevel, but this class was structured to be usable as an inline
# window. We should probably let the parent decide how to destroy it.
self.__parent.destroy()
def __cancel(self, *event):
self.__parent.destroy()
def selected(self, selection):
self.__port.configure(text = selection)
self.__newModel.portName = selection
def aborted(self):
pass
def __selectPort(self, *event):
selector = TextSelect(self, self.__port, _getPorts(self.__awb))
@staticmethod
def __getIntValue(control: Entry):
result = control.get()
return 0 if result == '' else int(result)
def __getProgramValue(self):
return self.__getIntValue(self.__program)
def __getBankValue(self):
return self.__getIntValue(self.__bank)
def __fillNewModel(self):
self.__newModel.bank = self.__getBankValue()
self.__newModel.program = self.__getProgramValue()
def __setProgramValue(self, val: int):
self.__program.delete(0, 'end')
self.__program.insert(0, str(val))
def __changeProgram(self):
"""Change the program of the port's midi device to the current program.
"""
self.__fillNewModel()
if self.__newModel.portName != 'port':
self.__newModel.activate(self.__awb)
def __programDec(self):
val = self.__getProgramValue() - 1
if val == -1:
val = 127
self.__setProgramValue(val)
self.__changeProgram()
def __programInc(self):
val = self.__getProgramValue() + 1
if val == 128:
val = 0
self.__setProgramValue(val)
self.__changeProgram()
class ConfigPresetEditor(Toplevel):
# TODO: follow the mutation pattern used by ProgramChanger: pass in the
# original object and a commit method.
def __init__(self, client: 'AWBClient', preset: 'StateVec'):
super(ConfigPresetEditor, self).__init__()
self.__preset = preset
self.__list = Listbox(self)
self.__list.grid(columnspan=2)
for attr in dir(self.__preset):
if attr.startswith('prog:'):
self.__list.insert('end', attr)
buttonPnl = Frame(self)
button = Button(buttonPnl, text='New Program',
command=self.__newProgram
)
button.pack(side=LEFT)
button = Button(buttonPnl, text = 'Edit', command=self.__edit)
button.pack(side=LEFT)
button = Button(buttonPnl, text='Delete', command=self.__delete)
button.pack(side=LEFT)
buttonPnl.grid(columnspan = 2)
self.__awb = client
def __newProgram(self):
def addProgram(state: MidiState):
# store the preset. TODO: add channel to the name.
name = 'prog:%s' % state.portName
setattr(self.__preset, name, state)
self.__list.insert('end', name)
print(dir(self.__preset))
c = ProgramChanger(Toplevel(), self.__awb, MidiState('port', 0, 0),
addProgram
)
c.grid(sticky=NSEW)
def __edit(self):
selIndex = self.__list.curselection()
if len(selIndex) != 1:
# TODO: show error
return
selIndex = selIndex[0]
oldName = self.__list.get(selIndex)
def updateProgram(state: MidiState):
self.__list.delete(selIndex)
name = 'prog:%s' % state.portName
delattr(self.__preset, oldName)
setattr(self.__preset, name, state)
self.__list.insert(selIndex, name)
c = ProgramChanger(Toplevel(), self.__awb,
getattr(self.__preset, oldName),
updateProgram
)
c.grid(sticky=NSEW)
def __delete(self):
selIndex = self.__list.curselection()
if len(selIndex) != 1:
# TODO: show error
return
selIndex = selIndex[0]
name = self.__list.get(selIndex)
self.__list.delete(selIndex)
del self.__items[selIndex]
delattr(self.__preset, name)
class ConfigPreset(Button):
"""The button that selects a config preset.
Can also be right-clicked to edit the configuration.
"""
def __init__(self, parent: 'Widget', client: 'AWBClient', text: str,
index: int,
**kwargs
):
"""
Args:
text: Name to display on the button.
index: index of the config preset button in the ConfigPresetRow.
"""
super(ConfigPreset, self).__init__(parent, text=text)
self.bind('<Button-3>', self.__menu)
self.__awb = client
self.__index = index
# Create a pop-up menu.
self.__popup = Menu(self, tearoff = False)
self.__popup.add_command(label = 'Configure',
command = self.__configure
)
def activate(self, active: bool):
"""Activate/deactivate the button based on the value of 'active'."""
if active:
self.configure(background = 'DarkRed')
else:
self.configure(background = 'DarkGray')
def __menu(self, event):
self.__popup.tk_popup(event.x_root, event.y_root, 0)
def __configure(self):
"""Starts the configuration window."""
config = ConfigPresetEditor(self.__awb, self.__awb.voices[self.__index])
class ConfigPresetRow(Frame):
"""A row of config preset buttons."""
def __init__(self, parent: 'Widget', client: 'AWBClient', count: int,
**kwargs
):
super(ConfigPresetRow, self).__init__(**kwargs)
assert count > 0 # We assume __current is valid below.
self.__buttons = []
self.__current = 0
self.__client = client
for i in range(count):
button = ConfigPreset(self, client, str(i), i)
button.pack(side = LEFT)
self.__buttons.append(button)
# Button press callback.
def pressed(index=i, *args):
self.__activate(index)
self.__client.activate(index)
button.configure(command = pressed)
client.addChannelSubscriber(i, self.__statusCallback)
def __activate(self, index):
self.__buttons[self.__current].activate(False)
self.__current = index
self.__buttons[index].activate(True)
def __statusCallback(self, channel: int, modes: int):
if modes & ACTIVE:
self.__activate(channel)
class Counter:
def __init__(self):
self.val = 0
def __call__(self):
result = self.val
self.val += 1
return result
class EventMultiplexer:
"""Sends an event to multiple handlers."""
def __init__(self, *handlers):
self.handlers = handlers
def __call__(self):
for handler in self.handlers:
handler()
class RecordingInfo:
"""Tracks information on a recorded midi-macro."""
def __init__(self, name, events):
self.name = name
self.events = events
def __str__(self):
return self.name
class EventRecorder:
def __init__(self, name: str):
self.name = name
self.events = []
# __startTime is the time of the first event (seconds since the epoch).
self.__startTime : float = 0
def __call__(self, client: AWBClient, event: Event) -> bool:
if self.__startTime:
event.time = client.getTicks(time.time() - self.__startTime)
else:
self.__startTime = time.time()
event.time = 0
self.events.append(event)
return False
def getRecordingInfo(self) -> RecordingInfo:
return RecordingInfo(self.name, self.events)
class LRState(Enum):
RECORD = 0
PLAYING = 1
IDLE = 2
class LoopMaster:
"""Provides the start time for a new loop.
If there are no other loops, this just provides the current absolute time.
If there are other loops, it provides the start time of the last iteration
of the largest current loop.
"""
def __init__(self, client: AWBClient):
self.__client = client
self.__startTime = None
self.__duration = None
def getStartTime(self) -> Tuple[int, int]:
"""Returns the start time and offset of a new loop.
The start time is either now (in ticks) or the start time of the
current period of all existing loops. The offset is the number of
ticks from that start time to now.
"""
if self.__startTime is None:
self.__startTime = self.__client.getTicks()
print(f'xxx setting master start time to {self.__startTime}')
return self.__startTime, 0
elif self.__duration is None:
return (self.__startTime,
self.__client.getTicks() - self.__startTime
)
else:
# We're currently looping but not recording.
#
# adjust the current start time to the beginning of the last cycle
t = self.__client.getTicks()
self.__startTime += \
((t - self.__startTime) // self.__duration) * self.__duration
return (self.__startTime, (t - self.__startTime) % self.__duration)
def recordEndTime(self, startTime: int) -> Tuple[int, int]:
"""Record the end time of a specific loop.
Returns a tuple of:
- the next time to schedule the beginning of the loop (relative to
now)
- the duration of the new loop
'startTime' is the start time of the new loop. This method may
adjust the duration of the the master loop if the new loop is longer
than the master loop.
"""
if self.__duration is None:
self.__duration = self.__client.getTicks() - self.__startTime
return 0, self.__duration
else:
t = self.__client.getTicks()
d = t - self.__startTime
print(f'xxx t = {t}, d = {d}, duration = {self.__duration}')
if d > self.__duration:
self.__duration = (d // self.__duration + 1) * self.__duration
print(
f'''xxx record end, start time:
{self.__duration - (t - startTime)}
duration: {self.__duration}''')
return self.__duration - (t - startTime), self.__duration
class LoopRegister:
"""Stores a set of events as a loop.
The loop register has three states: recording, playing and idle. It
transitions from recording to playing and then from playing to idle and
then from idle to playing, which works well for a single button control.
"""
def __init__(self, client: AWBClient, master: LoopMaster):
self.__client = client
self.__master = master
self.__events = []
self.__state = LRState.RECORD
self.__start : Optional[int] = None
def nextState(self):
def reschedule(client: AWBClient):
if self.__state == LRState.PLAYING:
print(f'xxx rescheduling events at {self.__client.getTicks()}')
self.__client.scheduleMidiEvents(self.__events)
else:
print(f'xxx not rescheduling events')
if self.__state == LRState.RECORD:
restartTime, duration = self.__master.recordEndTime(self.__start)
print(f'xxx adding reschedule event at +{duration}')
self.__events.append(CallableEvent(duration, reschedule))
self.__state = LRState.PLAYING
# schedule an event to kick off the first replay loop. If
# restartTime is now just do it without scheduling a CallableEvent.
if not restartTime:
print(f'xxx restarting loop immediately')
reschedule(self.__client)
else:
print(f'xxx restarting loop in +{restartTime}')
self.__client.scheduleMidiEvent(
CallableEvent(restartTime, reschedule)
)
elif self.__state == LRState.PLAYING:
self.__state = LRState.IDLE
elif self.__state == LRState.IDLE:
self.__state = LRState.PLAYING
# TODO: need to schedule this for the next period if any loops are
# currently playing
reschedule(self.__events)
def addEvents(self, events):
# Ignore events added when not recording.
if not self.__state == LRState.RECORD:
return
# Record the start time if there is none, otherwise use it to
# calculate the loop-relative time.
if self.__start is None:
self.__start, t = self.__master.getStartTime()
print(f'xxx getting master start time: {self.__start}, offset = {t}')
else:
t = self.__client.getTicks() - self.__start
self.__events = list(merge(offsetEventTimes(events, t),
self.__events,
key=lambda e: e.time
)
)
@property
def state(self):
return self.__state
class MidiRegisters(Toplevel):
"""Window containing and controlling a set of midi registers.
This has to be a toplevel so it can have toplevel bindings.
"""
STATUS_TEXT = 'Hit unbound key to record'
def __init__(self, client: AWBClient):
super().__init__()
self.client = client
self.__recorder : Optional[EventRecorder] = None
self.__registers : Dict[str, RecordingInfo] = {}
self.__loopers : List[Optional[LoopRegister]] = [None] * 4
self.__recordingLooper : Optional[int] = None
self.__master = LoopMaster(client)
self.frame = Frame(self)
self.status = Label(self.frame, text=self.STATUS_TEXT)
self.status.pack()
self.list = Listbox(self.frame)
self.list.pack(expand=True, fill=BOTH)
# Create the looper status controls
self.__loopStats = []
for i in range(4):
stat = Label(self.frame, text=f'Loop {i + 1}: EMPTY')
self.__loopStats.append(stat)
stat.pack()
lower_frame = Frame(self)
#self.entry = Entry(lower_frame)
#record = Button(lower_frame, text='Record', command=self.__add)
self.frame.grid(row = 0, column = 0, sticky=NSEW)
self.bind('<KeyPress>', self.__keypress)
def __keypress(self, event):
print(f'keysym is {event.keysym}')
# Check for special keys.
if len(event.keysym) > 1:
if event.keysym == 'Delete':
sel = self.list.curselection()
if sel:
key = self.list.get(sel[0])
del self.__registers[key]
self.list.delete(sel[0])
elif event.keysym in ('F1', 'F2', 'F3', 'F4'):
index = int(event.keysym[1]) - 1
looper = self.__loopers[index]
if looper:
print(f'xxx forwarding looper at {index}')
looper.nextState()
self.__loopStats[index].configure(
text=f'Loop {index + 1}: {looper.state}')
else:
print(f'xxx creating looper at {index}')
self.__loopers[index] = \
LoopRegister(self.client, self.__master)
self.__recordingLooper = index
return
# play an existing register.
try:
events = self.__registers[event.keysym].events
self.client.scheduleMidiEvents(events)
if self.__recordingLooper is not None:
self.__loopers[self.__recordingLooper].addEvents(events)
return
except KeyError:
pass
# If the key we're recording was pressed again, end record.
if self.__recorder and self.__recorder.name == event.keysym:
trackInfo = self.__recorder.getRecordingInfo()
self.client.inputProcessors.remove(self.__recorder)
self.__recorder = None
self.list.insert(len(self.__registers), str(trackInfo))
self.__registers[trackInfo.name] = trackInfo
self.status.configure(text=self.STATUS_TEXT)
# If we're not recording, start recording on that key.
elif not self.__recorder:
self.__recorder = EventRecorder(event.keysym)
self.status.configure(
text=f'Recording on {event.keysym}: press again to finish'
)
self.client.inputProcessors.append(self.__recorder)
class MainWin(Tk):
def __init__(self, client):
Tk.__init__(self)
self.title('MAWB')
self.client = client
self.programPanel = ProgramPanel(self, client)
self.programPanel.grid(row = 0, column = 0, sticky = NSEW)
self.frame = Frame(self)
self.frame.grid(row = 0, column = 0, sticky = NSEW)
nextRow = Counter()
# Create the menu.
menubar = Frame(self.frame)
addButton = Menubutton(menubar, text = 'File')
addButton.pack()
menubar.grid(row = nextRow(), column = 0, sticky = W)
fileMenu = Menu(addButton, tearoff = False)
fileMenu.add_command(label='Save', command=self.__save)
fileMenu.add_command(label='Load', command=self.__load)
addButton['menu'] = fileMenu
# Create the program panel.
self.program = ProgramWidget(self.frame, client)
self.program.grid(row = nextRow(), column = 0, columnspan = 2,
sticky = W)
fileMenu.add_command(label='Plugins', command=self.__plugins)
fileMenu.add_command(label='Midi Registers', command=self.__registers)
addButton['menu'] = fileMenu
label = Label(self.frame, text = 'AWB')
label.grid(row = nextRow(), column = 0)
self.recordMode = Button(self.frame, text = 'P',
command = lambda: self.toggleRecord())
modeRow = nextRow()
self.recordMode.grid(row = modeRow, column = 0, sticky = W)
self.status = Label(self.frame, text = 'Idle')
self.status.grid(row = modeRow, column = 1)
self.channels = []
self.channelFrame = Frame(self.frame)
self.channelFrame.grid(row = nextRow(), columnspan = 2)
self.bind('q', self.terminate)
self.bind('f', self.foo)
self.bind('r', self.toggleRecord)
self.bind('k', self.toggleSticky)
self.bind('.', self.nextSection)
self.bind(',', self.prevSection)
self.bind('<space>', self.togglePause)
self.bind('K', self.clearAllState)
self.protocol('WM_DELETE_WINDOW', self.destroy)
# F1 key brings the main panel to the foreground
self.bind('<F1>', lambda evt: self.frame.tkraise())
# F2 brings the Program panel to the foreground.
self.bind('<F2>', lambda evt: self.programPanel.tkraise())
for i in range(0, 8):
# Bind number key.
self.bind(str(i),
lambda evt, channel = i: self.toggleChannel(channel)
)
# Create channel
channel = Channel(self.channelFrame, i)
self.channels.append(channel)
channel.pack(side = LEFT)
client.addChannelSubscriber(
i,
lambda ch, status, channel = channel:
channel.changeStatus(status)
)
client.onProgramChange = EventMultiplexer(
self.program.programChanged,
self.programPanel.programChanged
)
def __save(self, *args):
self.client.writeTo(open('noname.mawb', 'wb'))
def __load(self, *args):
# TODO: display a file selector.
self.client.readFrom(open('noname.mawb', 'rb'))
def __plugins(self, *args):
top = Toplevel()
plugins = Plugins(top, self.client)
plugins.grid()
def __registers(self, *args):
top = MidiRegisters(self.client)
def foo(self, event):
print('got foo')
def terminate(self, event):
self.destroy()
def toggleRecord(self, event=None):
self.client.recordEnabled = not self.client.recordEnabled
self.recordMode.configure(text = self.client.recordEnabled and 'R' or
'P'
)
def togglePause(self, event):
self.client.togglePause()
if self.client.paused:
self.status.configure(text = 'Paused')
else:
self.status.configure(text = 'Playing')
def clearAllState(self, event):
self.client.clearAllState()
self.status.configure(text = 'Idle')
for channel in self.channels:
channel.changeStatus(0)
def toggleChannel(self, channel):
# using "channel" as program
self.client.activate(channel)
if self.client.recording.get(channel):
self.client.endRecord(channel)
elif self.client.recordEnabled:
self.client.startRecord(channel)
self.status.configure(text = 'Recording on %s' % channel)
def __getActiveChannel(self):
# Move this to awb_client
for i, ch in enumerate(self.channels):
if ch.active:
return i
def toggleSticky(self, event):
channel = self.__getActiveChannel()
self.client.toggleChannelSticky(channel)
def nextSection(self, event):
self.client.nextOrNewSection()
def prevSection(self, event):
self.client.prevSection()
def add(self, widget: 'Widget'):
"""Add a widget to the front panel of the window."""
widget.grid(columnspan = 2, sticky = NSEW)
class ProgramWidget(Frame):
"""Widget that displays the main application "program" (script) panel."""
def __init__(self, parent, client):
super(ProgramWidget, self).__init__(parent)
self.client = client
self.programLabel = Label(self, text = 'Program:')
self.programLabel.grid(row = 0, column = 0)
self.programEntry = Entry(self, text = 'Program name',
state = 'readonly')
self.programEntry.grid(row = 0, column = 1)
self.buttonPanel = Frame(self)
self.buttonPanel.grid(row = 1, column = 0, columnspan = 2, sticky = W)
self.newButton = Button(self.buttonPanel, text='New',
command = self.newProgram)
self.newButton.pack(side = LEFT)
def programChanged(self):
self.__setProgramText(str(self.client.state))
def __setProgramText(self, text):
self.programEntry.configure(state = NORMAL)
self.programEntry.delete(0)
self.programEntry.insert(0, text)
self.programEntry.configure(state = 'readonly')
def newProgram(self):
self.client.makeNewProgram()
class TextSelect(Frame):
def __init__(self, client, anchor, items, destroyAnchor=False):
"""
Args:
client: [SelectionClient] The window that text is returned to.
anchor: A window that the text selection popup is created
relative to.
items: [str], items to display in the listbox.
destroyAnchor: [bool] if true, destroy the anchor after
positioning the window.
"""
self.top = Toplevel()
self.anchor = anchor
self.top.overrideredirect(1)
self.top.wm_geometry('+%s+%s' % (anchor.winfo_rootx() + anchor.winfo_x(),
anchor.winfo_rooty() + anchor.winfo_y()
)
)
super(TextSelect, self).__init__(self.top)
self.entry = Entry(self)
self.client = client
self.items = items
self.place(x = 0.5, y = 0.5, height = 100, width = 100)
self.entry.bind('<Return>', self.close)
self.entry.bind('<KeyPress>', self.filter)
self.entry.bind('<Escape>', self.abort)
self.entry.bind('<Up>', self.up)
self.entry.bind('<Down>', self.down)
self.entry.pack()
# Create the list of items.
self.list = Listbox(self)
for item in self.items:
self.list.insert('end', item)
self.list.pack()
self.grid()
self.entry.focus()
# Reposition the select button against the anchor. We defer this
# until after idle so that the anchor has a chance to get rendered.
def reposition(*args):
self.top.wm_geometry('+%s+%s' % (
anchor.winfo_rootx(),
anchor.winfo_rooty())
)
if destroyAnchor:
anchor.destroy()
self.after_idle(reposition)
def close(self, event):
sel = self.list.curselection()
if sel:
item = self.list.get(sel[0])
else:
item = self.entry.get()
# Note that the order of this appears to be significant: destroying
# before selecting leaves the focus in a weird state.
self.client.selected(item)
self.top.destroy()
return 'braek'
def abort(self, event):
self.top.destroy()
self.client.aborted()
return 'break'
def up(self, event):
sel = self.list.curselection()
if not sel:
self.list.selection_set(0)
return 'break'
sel = sel[0]
print('sel is %s size is %s' % (sel, self.list.size()))
if sel > 0:
print('setting selection to %s' % sel)
self.list.selection_clear(sel)
self.list.selection_set(sel - 1)
self.list.see(sel)
return 'break'
def down(self, event):
sel = self.list.curselection()
if not sel:
self.list.selection_set(0)
return 'break'
sel = sel[0]
print('sel is %s size is %s' % (sel, self.list.size()))
if sel < self.list.size() - 1:
print('setting selection to %s' % (sel + 1))
self.list.selection_clear(sel)
self.list.selection_set(sel + 1)
self.list.see(sel)
return 'break'
def filter(self, event):
"""Filter the listbox based on the contents of the entryfield."""
# first add the character to the entry.
currentText = self.entry.get()
print(event.keysym)
if event.keysym == 'BackSpace':
# Handle backspace specially.
if currentText:
currentText = currentText[:-1]
self.entry.delete(0, 'end')
self.entry.insert(0, currentText)
else:
return 'break'
else:
# Assume normal character. Insert it.
self.entry.insert('insert', event.char)
currentText += event.char
self.list.delete(0, 'end')
pattern = currentText.upper()
for item in self.items:
if pattern in item.upper():
self.list.insert('end', item)
return 'break'
def _getPorts(client: 'AWBClient') -> List[str]:
result = []
for port in client.seq.iterPortInfos():
result.append(port.fullName)
return result
class ProgramPanel(Frame):
"""Lets you configure the program."""
def __init__(self, parent, client):
super(ProgramPanel, self).__init__()
self.client = client
self.text = Text(self)
self.text.grid(row = 0, column = 0, sticky = NSEW)
self.text.bind('<Tab>', self.showCompletions)
self.text.bind('<Return>', self.eval)
self.text.focus()
self.info = Label(self)
self.info.grid(row = 1, column = 0, sticky = NSEW)
# Do a programChanged to set the current program.
self.programChanged()
def selected(self, item):
self.__insertWord(item)
self.text.focus()
def aborted(self):
self.text.focus()
def __insertWord(self, word):
if ' ' in word:
self.text.insert('insert', ' "%s"' % word)
else:
self.text.insert('insert', ' ' + word)
def programChanged(self):
"""Handler for program change events."""
program = self.client.state
self.text.delete('1.0', 'end')
if isinstance(program, Program):
print('text is %r' % program.text)
self.text.insert('1.0', program.text)
print('setting interp')
self.interp = ScriptInterpreter(
ProgramCommands(self.client, program).dict,
)
else:
self.text.insert('1.0', '# uneditable program type\n')
def showCompletions(self, event):
selector = None
anchor = None
anchor = Frame(self.text)
self.text.window_create('insert', window=anchor)
selector = TextSelect(self, anchor, _getPorts(self.__client),
destroyAnchor = True)
return 'break'