1 #from gui_tools import *
2 from gi
.repository
import GObject
, Gdk
, Gtk
, GooCanvas
, GLib
6 value
= GObject
.Value()
7 value
.init(GObject
.TYPE_UINT
)
13 def standard_filter(patterns
, name
):
21 citem
.visibility
= GooCanvas
.CanvasItemVisibility
.HIDDEN
24 citem
.visibility
= GooCanvas
.CanvasItemVisibility
.VISIBLE
26 def polygon_to_path(points
):
27 assert len(points
) % 2 == 0, "Invalid number of points (%d) in %s" % (len(points
), repr(points
))
30 path
+= "M %s %s" % (points
[0], points
[1])
32 for i
in range(2, len(points
), 2):
33 path
+= " L %s %s" % (points
[i
], points
[i
+ 1])
36 class NoteModel(object):
37 def __init__(self
, pos
, channel
, row
, vel
, len = 1):
39 self
.channel
= int(channel
)
46 return "pos=%s row=%s vel=%s len=%s" % (self
.pos
, self
.row
, self
.vel
, self
.len)
48 # This is stupid and needs rewriting using a faster data structure
49 class DrumPatternModel(GObject
.GObject
):
50 def __init__(self
, beats
, bars
):
51 GObject
.GObject
.__init
__(self
)
57 def import_data(self
, data
):
62 if len(t
) == 4 and (cmd
== 0x90) and (t
[3] > 0):
63 note
= NoteModel(t
[0], (t
[1] & 15) + 1, t
[2], t
[3])
64 active_notes
[t
[2]] = note
66 if len(t
) == 4 and ((cmd
== 0x90 and t
[3] == 0) or cmd
== 0x80):
67 if t
[2] in active_notes
:
68 active_notes
[t
[2]].len = t
[0] - active_notes
[t
[2]].pos
69 del active_notes
[t
[2]]
70 end
= self
.get_length()
71 for n
in active_notes
.values():
78 def add_note(self
, note
, send_changed
= True):
79 self
.notes
.append(note
)
83 def remove_note(self
, pos
, row
, channel
):
84 self
.notes
= [note
for note
in self
.notes
if note
.pos
!= pos
or note
.row
!= row
or (channel
is not None and note
.channel
!= channel
)]
87 def set_note_vel(self
, note
, vel
):
91 def set_note_len(self
, note
, len):
95 def has_note(self
, pos
, row
, channel
):
97 if n
.pos
== pos
and n
.row
== row
and (channel
is None or n
.channel
== channel
):
101 def get_note(self
, pos
, row
, channel
):
103 if n
.pos
== pos
and n
.row
== row
and (channel
is None or n
.channel
== channel
):
110 def get_length(self
):
111 return int(self
.beats
* self
.bars
* self
.ppqn
)
116 def delete_selected(self
):
117 self
.notes
= [note
for note
in self
.notes
if not note
.selected
]
120 def group_set(self
, **vals
):
124 for k
, v
in vals
.items():
128 def transpose_selected(self
, amount
):
132 if n
.row
+ amount
< 0 or n
.row
+ amount
> 127:
137 GObject
.type_register(DrumPatternModel
)
138 GObject
.signal_new("changed", DrumPatternModel
, GObject
.SIGNAL_RUN_LAST
, GObject
.TYPE_NONE
, ())
140 channel_ls
= Gtk
.ListStore(GObject
.TYPE_STRING
, GObject
.TYPE_INT
)
141 for ch
in range(1, 17):
142 channel_ls
.append((str(ch
), ch
))
143 snap_settings_ls
= Gtk
.ListStore(GObject
.TYPE_STRING
, GObject
.TYPE_INT
)
144 for row
in [("1/4", PPQN
), ("1/8", PPQN
// 2), ("1/8T", PPQN
//3), ("1/16", PPQN
//4), ("1/16T", PPQN
//6), ("1/32", PPQN
//8), ("1/32T", PPQN
//12), ("1/64", PPQN
//4)]:
145 snap_settings_ls
.append(row
)
146 edit_mode_ls
= Gtk
.ListStore(GObject
.TYPE_STRING
, GObject
.TYPE_STRING
)
147 for row
in [("Drum", "D"), ("Melodic", "M")]:
148 edit_mode_ls
.append(row
)
150 class DrumEditorToolbox(Gtk
.HBox
):
151 def __init__(self
, canvas
):
152 Gtk
.HBox
.__init
__(self
, spacing
= 5)
154 self
.vel_adj
= Gtk
.Adjustment(100, 1, 127, 1, 10, 0)
156 self
.pack_start(Gtk
.Label("Channel:"), False, False, 0)
157 self
.channel_setting
= gui_tools
.standard_combo(channel_ls
, active_item_lookup
= self
.canvas
.channel
, lookup_column
= 1)
158 self
.channel_setting
.connect('changed', lambda w
: self
.canvas
.set_channel(w
.get_model()[w
.get_active()][1]))
159 self
.pack_start(self
.channel_setting
, False, True, 0)
161 self
.pack_start(Gtk
.Label("Mode:"), False, False, 0)
162 self
.mode_setting
= gui_tools
.standard_combo(edit_mode_ls
, active_item_lookup
= self
.canvas
.edit_mode
, lookup_column
= 1)
163 self
.mode_setting
.connect('changed', lambda w
: self
.canvas
.set_edit_mode(w
.get_model()[w
.get_active()][1]))
164 self
.pack_start(self
.mode_setting
, False, True, 0)
166 self
.pack_start(Gtk
.Label("Snap:"), False, False, 0)
167 self
.snap_setting
= gui_tools
.standard_combo(snap_settings_ls
, active_item_lookup
= self
.canvas
.grid_unit
, lookup_column
= 1)
168 self
.snap_setting
.connect('changed', lambda w
: self
.canvas
.set_grid_unit(w
.get_model()[w
.get_active()][1]))
169 self
.pack_start(self
.snap_setting
, False, True, 0)
171 self
.pack_start(Gtk
.Label("Velocity:"), False, False, 0)
172 self
.pack_start(Gtk
.SpinButton(adjustment
= self
.vel_adj
, climb_rate
= 0, digits
= 0), False, False, 0)
173 button
= Gtk
.Button("Load")
174 button
.connect('clicked', self
.load_pattern
)
175 self
.pack_start(button
, True, True, 0)
176 button
= Gtk
.Button("Save")
177 button
.connect('clicked', self
.save_pattern
)
178 self
.pack_start(button
, True, True, 0)
179 button
= Gtk
.Button("Double")
180 button
.connect('clicked', self
.double_pattern
)
181 self
.pack_start(button
, True, True, 0)
182 self
.pack_start(Gtk
.Label("--"), False, False, 0)
184 def update_edit_mode(self
):
185 self
.mode_setting
.set_active(gui_tools
.ls_index(edit_mode_ls
, self
.canvas
.edit_mode
, 1))
187 def load_pattern(self
, w
):
188 dlg
= Gtk
.FileChooserDialog('Open a drum pattern', self
.get_toplevel(), Gtk
.FileChooserAction
.OPEN
,
189 (Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_OPEN
, Gtk
.ResponseType
.APPLY
))
190 dlg
.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
191 dlg
.add_filter(standard_filter(["*"], "All files"))
193 if dlg
.run() == Gtk
.ResponseType
.APPLY
:
194 pattern
= self
.canvas
.pattern
195 f
= file(dlg
.get_filename(), "r")
197 pattern
.beats
, pattern
.bars
= [int(v
) for v
in f
.readline().strip().split(";")]
198 for line
in f
.readlines():
200 if not line
.startswith("n:"):
201 pos
, row
, vel
= line
.split(";")
206 pos
, channel
, row
, vel
, len = line
[2:].split(";")
207 self
.canvas
.pattern
.add_note(NoteModel(pos
, channel
, row
, vel
, len), send_changed
= False)
209 self
.canvas
.pattern
.changed()
210 self
.canvas
.update_grid()
211 self
.canvas
.update_notes()
214 def save_pattern(self
, w
):
215 dlg
= Gtk
.FileChooserDialog('Save a drum pattern', self
.get_toplevel(), Gtk
.FileChooserAction
.SAVE
,
216 (Gtk
.STOCK_CANCEL
, Gtk
.ResponseType
.CANCEL
, Gtk
.STOCK_SAVE
, Gtk
.ResponseType
.APPLY
))
217 dlg
.add_filter(standard_filter(["*.cbdp"], "Drum patterns"))
218 dlg
.add_filter(standard_filter(["*"], "All files"))
219 dlg
.set_current_name("pattern.cbdp")
221 if dlg
.run() == Gtk
.ResponseType
.APPLY
:
222 pattern
= self
.canvas
.pattern
223 f
= file(dlg
.get_filename(), "w")
224 f
.write("%s;%s\n" % (pattern
.beats
, pattern
.bars
))
225 for i
in self
.canvas
.pattern
.items():
226 f
.write("n:%s;%s;%s;%s;%s\n" % (i
.pos
, i
.channel
, i
.row
, i
.vel
, i
.len))
230 def double_pattern(self
, w
):
231 len = self
.canvas
.pattern
.get_length()
232 self
.canvas
.pattern
.bars
*= 2
234 for note
in self
.canvas
.pattern
.items():
235 new_notes
.append(NoteModel(note
.pos
+ len, note
.channel
, note
.row
, note
.vel
, note
.len))
236 for note
in new_notes
:
237 self
.canvas
.pattern
.add_note(note
, send_changed
= False)
238 self
.canvas
.pattern
.changed()
239 self
.canvas
.update_size()
240 self
.canvas
.update_grid()
241 self
.canvas
.update_notes()
243 class DrumCanvasCursor(object):
244 def __init__(self
, canvas
):
246 self
.canvas_root
= canvas
.get_root_item()
247 self
.frame
= GooCanvas
.CanvasRect(parent
= self
.canvas_root
, x
= -6, y
= -6, width
= 12, height
= 12 , stroke_color
= "gray", line_width
= 1)
248 hide_item(self
.frame
)
249 self
.vel
= GooCanvas
.CanvasText(parent
= self
.canvas_root
, x
= 0, y
= 0, fill_color
= "blue", stroke_color
= "blue", anchor
= GooCanvas
.CanvasAnchorType
.S
)
251 self
.rubberband
= False
252 self
.rubberband_origin
= None
253 self
.rubberband_current
= None
256 hide_item(self
.frame
)
260 show_item(self
.frame
)
263 def move_to_note(self
, note
):
264 self
.move(note
.pos
, note
.row
, note
)
266 def move(self
, pulse
, row
, note
):
267 x
= self
.canvas
.pulse_to_screen_x(pulse
)
268 y
= self
.canvas
.row_to_screen_y(row
) + self
.canvas
.row_height
/ 2
271 dx
= self
.canvas
.pulse_to_screen_x(pulse
+ note
.len) - x
272 self
.frame
.set_properties(x
= x
- 6, width
= 12 + dx
, y
= y
- 6, height
= 12)
273 cy
= y
- self
.canvas
.row_height
* 1.5 if y
>= self
.canvas
.rows
* self
.canvas
.row_height
/ 2 else y
+ self
.canvas
.row_height
* 1.5
277 text
= "[%s] %s" % (note
.channel
, note
.vel
)
278 self
.vel
.set_properties(x
= x
, y
= cy
, text
= text
)
280 def start_rubberband(self
, x
, y
):
281 self
.rubberband
= True
282 self
.rubberband_origin
= (x
, y
)
283 self
.rubberband_current
= (x
, y
)
284 self
.update_rubberband_frame()
285 show_item(self
.frame
)
287 def update_rubberband(self
, x
, y
):
288 self
.rubberband_current
= (x
, y
)
289 self
.update_rubberband_frame()
291 def end_rubberband(self
, x
, y
):
292 self
.rubberband_current
= (x
, y
)
293 self
.update_rubberband_frame()
294 hide_item(self
.frame
)
295 self
.rubberband
= False
297 def cancel_rubberband(self
):
298 hide_item(self
.frame
)
299 self
.rubberband
= False
301 def update_rubberband_frame(self
):
302 self
.frame
.set_properties(x
= self
.rubberband_origin
[0],
303 y
= self
.rubberband_origin
[1],
304 width
= self
.rubberband_current
[0] - self
.rubberband_origin
[0],
305 height
= self
.rubberband_current
[1] - self
.rubberband_origin
[1])
307 def get_rubberband_box(self
):
308 x1
, y1
= self
.rubberband_origin
309 x2
, y2
= self
.rubberband_current
310 return (min(x1
, x2
), min(y1
, y2
), max(x1
, x2
), max(y1
, y2
))
312 class DrumCanvas(GooCanvas
.Canvas
):
313 def __init__(self
, rows
, pattern
):
314 GooCanvas
.Canvas
.__init
__(self
)
316 self
.pattern
= pattern
318 self
.grid_unit
= PPQN
/ 4 # unit in pulses
320 self
.instr_width
= 120
321 self
.edited_note
= None
322 self
.orig_velocity
= None
323 self
.orig_length
= None
325 self
.channel_modes
= ['D' if ch
== 10 else 'M' for ch
in range(1, 17)]
329 self
.grid
= GooCanvas
.CanvasGroup(parent
= self
.get_root_item(), x
= self
.instr_width
)
332 self
.notes
= GooCanvas
.CanvasGroup(parent
= self
.get_root_item(), x
= self
.instr_width
)
334 self
.names
= GooCanvas
.CanvasGroup(parent
= self
.get_root_item(), x
= 0)
337 self
.cursor
= DrumCanvasCursor(self
)
338 hide_item(self
.cursor
)
340 self
.connect('event', self
.on_grid_event
)
343 self
.edit_mode
= self
.channel_modes
[self
.channel
- 1]
344 self
.toolbox
= DrumEditorToolbox(self
)
346 self
.add_events(Gdk
.EventMask
.POINTER_MOTION_HINT_MASK
)
348 self
.grab_focus(self
.grid
)
351 def set_edit_mode(self
, mode
):
352 self
.edit_mode
= mode
353 self
.channel_modes
[self
.channel
- 1] = mode
356 return (self
.instr_width
+ self
.pattern
.get_length() * self
.zoom_in
+ 1, self
.rows
* self
.row_height
+ 1)
358 def set_grid_unit(self
, grid_unit
):
359 self
.grid_unit
= grid_unit
362 def set_channel(self
, channel
):
363 self
.channel
= channel
364 self
.set_edit_mode(self
.channel_modes
[self
.channel
- 1])
366 self
.toolbox
.update_edit_mode()
368 def update_size(self
):
369 sx
, sy
= self
.calc_size()
370 self
.set_bounds(0, 0, sx
, self
.rows
* self
.row_height
)
371 self
.set_size_request(sx
, sy
)
373 def update_names(self
):
374 for i
in self
.names
.items
:
376 for i
in range(0, self
.rows
):
377 #GooCanvas.CanvasText(parent = self.names, text = gui_tools.note_to_name(i), x = self.instr_width - 10, y = (i + 0.5) * self.row_height, anchor = Gtk.AnchorType.E, size_points = 10, font = "Sans", size_set = True)
378 GooCanvas
.CanvasText(parent
= self
.names
, text
= gui_tools
.note_to_name(i
), x
= self
.instr_width
- 10, y
= (i
+ 0.5) * self
.row_height
, anchor
= GooCanvas
.CanvasAnchorType
.E
, font
= "Sans")
380 def update_grid(self
):
381 for i
in self
.grid
.items
:
383 bg
= GooCanvas
.CanvasRect(parent
= self
.grid
, x
= 0, y
= 0, width
= self
.pattern
.get_length() * self
.zoom_in
, height
= self
.rows
* self
.row_height
, fill_color
= "white")
386 grid_fg
= "lightgray"
387 row_grid_fg
= "lightgray"
389 for i
in range(0, self
.rows
+ 1):
390 color
= row_ext_fg
if (i
== 0 or i
== self
.rows
) else row_grid_fg
391 GooCanvas
.CanvasPath(parent
= self
.grid
, data
= "M %s %s L %s %s" % (0, i
* self
.row_height
, self
.pattern
.get_length() * self
.zoom_in
, i
* self
.row_height
), stroke_color
= color
, line_width
= 1)
392 for i
in range(0, self
.pattern
.get_length() + 1, int(self
.grid_unit
)):
394 if i
% self
.pattern
.ppqn
== 0:
396 if (i
% (self
.pattern
.ppqn
* self
.pattern
.beats
)) == 0:
398 GooCanvas
.CanvasPath(parent
= self
.grid
, data
= "M %s %s L %s %s" % (i
* self
.zoom_in
, 1, i
* self
.zoom_in
, self
.rows
* self
.row_height
- 1), stroke_color
= color
, line_width
= 1)
400 def update_notes(self
):
401 while self
.notes
.get_n_children() > 0:
402 self
.notes
.remove_child(0)
403 for item
in self
.pattern
.items():
404 x
= self
.pulse_to_screen_x(item
.pos
) - self
.instr_width
405 y
= self
.row_to_screen_y(item
.row
+ 0.5)
406 if item
.channel
== self
.channel
:
407 fill_color
= 0xC0C0C0 - int(item
.vel
* 1.5) * 0x000101
408 stroke_color
= 0x808080
410 stroke_color
= 0xFF8080
412 fill_color
= 0xE0E0E0
413 stroke_color
= 0xE0E0E0
415 x2
= self
.pulse_to_screen_x(item
.pos
+ item
.len) - self
.pulse_to_screen_x(item
.pos
)
416 polygon
= [-2, 0, 0, -5, x2
- 5, -5, x2
, 0, x2
- 5, 5, 0, 5]
418 polygon
= [-5, 0, 0, -5, 5, 0, 0, 5, -5, 0]
419 item
.item
= GooCanvas
.CanvasPath(parent
= self
.notes
, data
= polygon_to_path(polygon
), line_width
= 1, fill_color
= ("#%06x" % fill_color
), stroke_color
= ("#%06x" % stroke_color
))
420 #item.item.set_property('stroke_color_rgba', guint(stroke_color))
421 item
.item
.translate(x
, y
)
423 def set_selection_from_rubberband(self
):
424 sx
, sy
, ex
, ey
= self
.cursor
.get_rubberband_box()
425 for item
in self
.pattern
.items():
426 x
= self
.pulse_to_screen_x(item
.pos
)
427 y
= self
.row_to_screen_y(item
.row
+ 0.5)
428 item
.selected
= (x
>= sx
and x
<= ex
and y
>= sy
and y
<= ey
)
431 def on_grid_event(self
, item
, event
):
432 if event
.type == Gdk
.EventType
.KEY_PRESS
:
433 return self
.on_key_press(item
, event
)
434 if event
.type in [Gdk
.EventType
.BUTTON_PRESS
, Gdk
.EventType
._2BUTTON
_PRESS
, Gdk
.EventType
.LEAVE_NOTIFY
, Gdk
.EventType
.MOTION_NOTIFY
, Gdk
.EventType
.BUTTON_RELEASE
]:
435 return self
.on_mouse_event(item
, event
)
437 def on_key_press(self
, item
, event
):
438 keyval
, state
= event
.keyval
, event
.state
439 kvname
= Gdk
.keyval_name(keyval
)
440 if kvname
== 'Delete':
441 self
.pattern
.delete_selected()
444 self
.pattern
.group_set(channel
= self
.channel
)
447 self
.pattern
.group_set(vel
= int(self
.toolbox
.vel_adj
.get_value()))
449 elif kvname
== 'plus':
450 self
.pattern
.transpose_selected(1)
452 elif kvname
== 'minus':
453 self
.pattern
.transpose_selected(-1)
458 def on_mouse_event(self
, item
, event
):
459 ex
, ey
= self
.convert_to_item_space(self
.get_root_item(), event
.x
, event
.y
)
460 column
= self
.screen_x_to_column(ex
)
461 row
= self
.screen_y_to_row(ey
)
462 pulse
= column
* self
.grid_unit
463 epulse
= (ex
- self
.instr_width
) / self
.zoom_in
464 unit
= self
.grid_unit
* self
.zoom_in
465 if self
.cursor
.rubberband
:
466 if event
.type == Gdk
.EventType
.MOTION_NOTIFY
:
467 self
.cursor
.update_rubberband(ex
, ey
)
469 button
= event
.get_button()[1]
470 if event
.type == Gdk
.EventType
.BUTTON_RELEASE
and button
== 1:
471 self
.cursor
.end_rubberband(ex
, ey
)
472 self
.set_selection_from_rubberband()
473 self
.request_update()
476 if event
.type == Gdk
.EventType
.BUTTON_PRESS
:
477 button
= event
.get_button()[1]
478 self
.grab_focus(self
.grid
)
479 if ((event
.state
& Gdk
.ModifierType
.SHIFT_MASK
) == Gdk
.ModifierType
.SHIFT_MASK
) and button
== 1:
480 self
.cursor
.start_rubberband(ex
, ey
)
482 if pulse
< 0 or pulse
>= self
.pattern
.get_length():
484 note
= self
.pattern
.get_note(pulse
, row
, self
.channel
)
487 vel
= int(self
.toolbox
.vel_adj
.get_value())
488 self
.pattern
.set_note_vel(note
, vel
)
489 self
.cursor
.move(pulse
, row
, note
)
492 self
.toolbox
.vel_adj
.set_value(note
.vel
)
494 note
= NoteModel(pulse
, self
.channel
, row
, int(self
.toolbox
.vel_adj
.get_value()), self
.grid_unit
if self
.edit_mode
== 'M' else 1)
495 self
.pattern
.add_note(note
)
496 self
.edited_note
= note
497 self
.orig_length
= note
.len
498 self
.orig_velocity
= note
.vel
501 self
.cursor
.move(self
.edited_note
.pos
, self
.edited_note
.row
, note
)
504 if event
.type == Gdk
.EventType
._2BUTTON
_PRESS
:
505 if pulse
< 0 or pulse
>= self
.pattern
.get_length():
507 if self
.pattern
.has_note(pulse
, row
, self
.channel
):
508 self
.pattern
.remove_note(pulse
, row
, self
.channel
)
509 self
.cursor
.move(pulse
, row
, None)
511 if self
.edited_note
is not None:
513 self
.edited_note
= None
515 if event
.type == Gdk
.EventType
.LEAVE_NOTIFY
and self
.edited_note
is None:
516 hide_item(self
.cursor
)
518 if event
.type == Gdk
.EventType
.MOTION_NOTIFY
and self
.edited_note
is None:
519 if pulse
< 0 or pulse
>= self
.pattern
.get_length():
520 hide_item(self
.cursor
)
523 if abs(pulse
- epulse
) > 5:
524 hide_item(self
.cursor
)
526 note
= self
.pattern
.get_note(column
* self
.grid_unit
, row
, self
.channel
)
527 self
.cursor
.move(pulse
, row
, note
)
528 show_item(self
.cursor
)
530 if event
.type == Gdk
.EventType
.MOTION_NOTIFY
and self
.edited_note
is not None:
531 vel
= int(self
.orig_velocity
- self
.snap(ey
- self
.orig_y
) / 2)
533 if vel
> 127: vel
= 127
534 self
.pattern
.set_note_vel(self
.edited_note
, vel
)
535 len = pulse
- self
.edited_note
.pos
536 if self
.edit_mode
== 'D':
538 elif len <= -self
.grid_unit
:
539 len = self
.orig_length
540 elif len <= self
.grid_unit
:
543 len = int((len + 1) / self
.grid_unit
) * self
.grid_unit
- 1
544 self
.pattern
.set_note_len(self
.edited_note
, len)
545 self
.toolbox
.vel_adj
.set_value(vel
)
546 self
.cursor
.move_to_note(self
.edited_note
)
548 self
.request_update()
550 if event
.type == Gdk
.EventType
.BUTTON_RELEASE
and self
.edited_note
is not None:
551 self
.edited_note
= None
555 def screen_x_to_column(self
, x
):
556 unit
= self
.grid_unit
* self
.zoom_in
557 return int((x
- self
.instr_width
+ unit
/ 2) / unit
)
559 def screen_y_to_row(self
, y
):
560 return int((y
- 1) / self
.row_height
)
562 def pulse_to_screen_x(self
, pulse
):
563 return pulse
* self
.zoom_in
+ self
.instr_width
565 def column_to_screen_x(self
, column
):
566 unit
= self
.grid_unit
* self
.zoom_in
567 return column
* unit
+ self
.instr_width
569 def row_to_screen_y(self
, row
):
570 return row
* self
.row_height
+ 1
573 if val
> -10 and val
< 10:
581 class DrumSeqWindow(Gtk
.Window
):
582 def __init__(self
, length
, pat_data
):
583 Gtk
.Window
.__init
__(self
, Gtk
.WindowType
.TOPLEVEL
)
584 self
.vbox
= Gtk
.VBox(spacing
= 5)
585 self
.pattern
= DrumPatternModel(4, length
/ (4 * PPQN
))
586 if pat_data
is not None:
587 self
.pattern
.import_data(pat_data
)
589 self
.canvas
= DrumCanvas(128, self
.pattern
)
590 sw
= Gtk
.ScrolledWindow()
591 sw
.set_size_request(640, 400)
592 sw
.add_with_viewport(self
.canvas
)
593 self
.vbox
.pack_start(sw
, True, True, 0)
594 self
.vbox
.pack_start(self
.canvas
.toolbox
, False, False, 0)
597 if __name__
== "__main__":
599 w
.set_title("Drum pattern editor")
601 w
.connect('destroy', lambda w
: Gtk
.main_quit())