Improvement to arrange tool.

- Allow selecting multiple measures (within the same track).
- Implement 'normal' pasting, that copies measures.
- Easily triggers a bug in how time signatures are handled.
looper
Ben Niemann 5 years ago
parent c458b93890
commit 13a88f2d2c
  1. 31
      NOTES.org
  2. 8
      bin/runtests.py
  3. 114
      noisicaa/audioproc/vm/engine_perftest.py
  4. 2
      noisicaa/core/__init__.py
  5. 17
      noisicaa/core/model_base.py
  6. 42
      noisicaa/music/project.py
  7. 2
      noisicaa/music/project_process.py
  8. 70
      noisicaa/music/sheet.py
  9. 48
      noisicaa/music/state.py
  10. 185
      noisicaa/music/state_test.py
  11. 24
      noisicaa/ui/editor_window.py
  12. 12
      noisicaa/ui/project_view.py
  13. 3
      noisicaa/ui/selection_set.py
  14. 67
      noisicaa/ui/sheet_view/base_track_item.py
  15. 31
      noisicaa/ui/sheet_view/sheet_view.py

@ -1,18 +1,35 @@
# -*- org-tags-column: -98 -*-
* Rework how time signatures are managed :BUG:
- Currently duration is a property of Measure. It uses the time signature of the measure in the
sheet property track at the same index.
- it references the property measure with its index within the measure_heap, which is basically
a random number. Can cause crashes when pasting a sequence of measures.
- the same measure could be used at different positions with different time signatures.
- time signature is not shared across tracks.
- each track can have a different time signature and change it at arbitrary positions.
- measures do not align vertically.
- how to deal with selecting a block of measures across tracks, if the measures don't line up
vertically?
- the TimeLine should show marks for the current track.
- simplify how to set time signature across multiple tracks.
- should be the default, with some extra step to have a different time signature on certain
tracks.
* Set track length :FR:
- set number of measures for the selected track, extend track with empty measures as needed
- dialog has list of tracks, select which tracks to affect
* Improve ArrangeMeasuresTool :FR:
- remove selection when switching away from tool
- Use QClipboard
- does it make sense?
- only for copy/pasting between projects
- also for selection? support middle-click insert?
- select multiple measures
- only continuous ranges for now
- click and drag
- click first and shift click end
- cut: either remove or clear selection
- paste: either insert or overwrite
- drag'n'drop move, copy, link
- fill time range with repeating pattern
- allow selection spanning different tracks
- what about control/sample tracks?
- just skip for now
@ -25,6 +42,8 @@
Explicit dereference all to create standalone clone for every selected
measure.
* Remember selected tool in session :FR:
* Highlight all linked measures :FR:
When hovering over a measure, highlight this and all other measures referencing the same object.
* Rendering of edit actions :FR:
- separate modelstate object with generator interface for model state
- produces PaintAction objects
@ -62,6 +81,12 @@
- simplify UI code, merge SheetView logic into ProjectView.
- instead think about ways to have different regions in time within the project, e.g. for
experiments, etc.
- tracks can be discontinuous, i.e. measures don't need to line up
- each measure tracks its position in time
- set regions in the time line.
- inserting measures only shifts measures to the right within the current region
- if the end goes past the region, extend the region and move all following regions (across
all tracks) to the right to make room.
* Settings window is sticky :BUG:
Keeps getting reopened on startup.

@ -158,11 +158,13 @@ def main(argv):
cov.start()
for dirpath, dirnames, filenames in os.walk(os.path.join(LIBDIR, 'noisicaa')):
if '__pycache__' in dirnames:
dirnames.remove('__pycache__')
for ignore_dir in ('__pycache__', 'testdata'):
if ignore_dir in dirnames:
dirnames.remove(ignore_dir)
for filename in filenames:
if not (fnmatch.fnmatch(filename, '*.py') or fnmatch.fnmatch(filename, '*.so')):
if (not (fnmatch.fnmatch(filename, '*.py') or fnmatch.fnmatch(filename, '*.so'))
or fnmatch.fnmatch(filename, 'lib*.so')):
continue
filename = os.path.splitext(filename)[0]

@ -27,82 +27,82 @@ import os.path
from noisidev import perf_stats
from noisidev import profutil
from .. import backend
from .. import nodes
from . import engine
# from .. import backend
# from .. import nodes
# from . import engine
logger = logging.getLogger(__name__)
# logger = logging.getLogger(__name__)
class TestBackend(backend.Backend):
def __init__(self, *, num_frames=1000, skip_first=10):
super().__init__()
# class TestBackend(backend.Backend):
# def __init__(self, *, num_frames=1000, skip_first=10):
# super().__init__()
self.__frame_num = 0
self.__num_frames = num_frames
self.__skip_first = skip_first
self.frame_times = []
# self.__frame_num = 0
# self.__num_frames = num_frames
# self.__skip_first = skip_first
# self.frame_times = []
def begin_frame(self, ctxt):
super().begin_frame(ctxt)
ctxt.duration = 128
ctxt.perf.start_span('frame')
if self.__frame_num >= self.__num_frames:
self.stop()
return
self.__frame_num += 1
# def begin_frame(self, ctxt):
# super().begin_frame(ctxt)
# ctxt.duration = 128
# ctxt.perf.start_span('frame')
# if self.__frame_num >= self.__num_frames:
# self.stop()
# return
# self.__frame_num += 1
def end_frame(self):
self.ctxt.perf.end_span()
# def end_frame(self):
# self.ctxt.perf.end_span()
if not self.stopped:
topspan = self.ctxt.perf.serialize().spans[0]
assert topspan.parentId == 0
assert topspan.name == 'frame'
duration = (topspan.endTimeNSec - topspan.startTimeNSec) / 1000.0
if self.__frame_num > self.__skip_first:
self.frame_times.append(duration)
# if not self.stopped:
# topspan = self.ctxt.perf.serialize().spans[0]
# assert topspan.parentId == 0
# assert topspan.name == 'frame'
# duration = (topspan.endTimeNSec - topspan.startTimeNSec) / 1000.0
# if self.__frame_num > self.__skip_first:
# self.frame_times.append(duration)
super().end_frame()
# super().end_frame()
def output(self, channel, samples):
pass
# def output(self, channel, samples):
# pass
class PipelineVMPerfTest(unittest.TestCase):
# class PipelineVMPerfTest(unittest.TestCase):
def test_fluidsynth(self):
vm = engine.PipelineVM()
try:
vm.setup(start_thread=False)
# def test_fluidsynth(self):
# vm = engine.PipelineVM()
# try:
# vm.setup(start_thread=False)
sink = nodes.Sink()
sink.setup()
vm.add_node(sink)
# sink = nodes.Sink()
# sink.setup()
# vm.add_node(sink)
for idx in range(3):
node = nodes.FluidSynthSource(
id='node%d' % idx,
soundfont_path='/usr/share/sounds/sf2/TimGM6mb.sf2', bank=0, preset=idx)
node.setup()
vm.add_node(node)
sink.inputs['in:left'].connect(node.outputs['out:left'])
sink.inputs['in:right'].connect(node.outputs['out:right'])
# for idx in range(3):
# node = nodes.FluidSynthSource(
# id='node%d' % idx,
# soundfont_path='/usr/share/sounds/sf2/TimGM6mb.sf2', bank=0, preset=idx)
# node.setup()
# vm.add_node(node)
# sink.inputs['in:left'].connect(node.outputs['out:left'])
# sink.inputs['in:right'].connect(node.outputs['out:right'])
vm.update_spec()
# vm.update_spec()
be = TestBackend()
vm.setup_backend(be)
# be = TestBackend()
# vm.setup_backend(be)
profutil.profile(self.id(), vm.vm_loop)
# profutil.profile(self.id(), vm.vm_loop)
perf_stats.write_frame_stats(
os.path.splitext(os.path.basename(__file__))[0],
'.'.join(self.id().split('.')[-2:]),
be.frame_times)
# perf_stats.write_frame_stats(
# os.path.splitext(os.path.basename(__file__))[0],
# '.'.join(self.id().split('.')[-2:]),
# be.frame_times)
finally:
vm.cleanup()
# finally:
# vm.cleanup()
if __name__ == '__main__':

@ -21,7 +21,7 @@
from .model_base import (
ObjectBase,
Property, ListProperty, DictProperty,
Property, ListProperty,
ObjectPropertyBase,
ObjectProperty, ObjectListProperty, ObjectReferenceProperty,

@ -193,19 +193,6 @@ class ListProperty(PropertyBase):
raise RuntimeError("ListProperty cannot be assigned.")
class DictProperty(PropertyBase):
def __get__(self, instance, owner):
value = super().__get__(instance, owner)
if value is None:
value = {}
super().__set__(instance, value)
return value
def __set__(self, instance, value):
raise RuntimeError("DictProperty cannot be assigned.")
class ObjectPropertyBase(PropertyBase):
def __init__(self, cls):
super().__init__()
@ -282,6 +269,10 @@ class ObjectList(object):
while len(self._objs) > 0:
self.__delitem__(0)
def extend(self, value):
for v in value:
self.append(v)
class ObjectListProperty(ObjectPropertyBase):
def __get__(self, instance, owner):

@ -613,11 +613,7 @@ class Project(BaseProject):
raise storage.CorruptedProjectError(
"Unexpected content type %s" % message.get_content_type())
serialized_checkpoint = message.get_payload()
checkpoint = json.loads(serialized_checkpoint, cls=JSONDecoder)
self.deserialize(checkpoint)
self.deserialize_object_into(message.get_payload(), self)
self.init_references()
def validate_node(root, parent, node):
@ -640,21 +636,35 @@ class Project(BaseProject):
message['Version'] = str(self.VERSION)
message['Content-Type'] = 'application/json; charset=utf-8'
checkpoint = json.dumps(
self.serialize(),
ensure_ascii=False, indent=' ', sort_keys=True,
cls=JSONEncoder)
serialized_checkpoint = checkpoint.encode('utf-8')
message.set_payload(serialized_checkpoint)
message.set_payload(self.serialize_object(self))
checkpoint_data = message.as_bytes()
self.storage.add_checkpoint(checkpoint_data)
def serialize_object(self, obj):
state = obj.serialize()
dump = json.dumps(state, ensure_ascii=False, indent=' ', sort_keys=True, cls=JSONEncoder)
return dump.encode('utf-8')
def deserialize_object_into(self, data, target):
if isinstance(data, bytes):
data = data.decode('utf-8')
state = json.loads(data, cls=JSONDecoder)
target.deserialize(state)
def deserialize_object(self, data):
if isinstance(data, bytes):
data = data.decode('utf-8')
state = json.loads(data, cls=JSONDecoder)
cls_name = state['__class__']
cls = self.cls_map[cls_name]
obj = cls(state=state)
return obj
def serialize_command(self, cmd, target_id, now):
serialized = json.dumps(
cmd.serialize(),
ensure_ascii=False, indent=' ', sort_keys=True,
cls=JSONEncoder)
state = cmd.serialize()
dump = json.dumps(state, ensure_ascii=False, indent=' ', sort_keys=True, cls=JSONEncoder)
serialized = dump.encode('utf-8')
policy = email.policy.compat32.clone(
linesep='\n',
@ -667,7 +677,7 @@ class Project(BaseProject):
message['Target'] = target_id
message['Time'] = time.ctime(now)
message['Timestamp'] = '%d' % now
message.set_payload(serialized.encode('utf-8'))
message.set_payload(serialized)
return message.as_bytes()

@ -433,7 +433,7 @@ class ProjectProcess(core.ProcessBase):
assert self.project is not None
obj = self.project.get_object(obj_id)
return obj.serialize()
return self.project.serialize_object(obj)
async def handle_create_player(
self, session_id, client_address, sheet_id):

@ -193,14 +193,48 @@ class RemoveMeasure(commands.Command):
commands.Command.register_command(RemoveMeasure)
class PasteMeasuresAsLink(commands.Command):
src_ids = core.ListProperty(str)
class ClearMeasures(commands.Command):
measure_ids = core.ListProperty(str)
def __init__(self, measure_ids=None, state=None):
super().__init__(state=state)
if state is None:
self.measure_ids.extend(measure_ids)
def run(self, sheet):
assert isinstance(sheet, Sheet)
root = sheet.root
measure_references = [
root.get_object(obj_id) for obj_id in self.measure_ids]
assert all(
isinstance(obj, base_track.MeasureReference) for obj in measure_references)
affected_track_ids = set(obj.track.id for obj in measure_references)
for mref in measure_references:
track = mref.track
measure = track.create_empty_measure(mref.measure)
track.measure_heap.append(measure)
mref.measure_id = measure.id
for track_id in affected_track_ids:
root.get_object(track_id).garbage_collect_measures()
commands.Command.register_command(ClearMeasures)
class PasteMeasures(commands.Command):
mode = core.Property(str)
src_objs = core.ListProperty(bytes)
target_ids = core.ListProperty(str)
def __init__(self, src_ids=None, target_ids=None, state=None):
def __init__(self, mode=None, src_objs=None, target_ids=None, state=None):
super().__init__(state=state)
if state is None:
self.src_ids.extend(src_ids)
self.mode = mode
self.src_objs.extend(src_objs)
self.target_ids.extend(target_ids)
def run(self, sheet):
@ -208,7 +242,7 @@ class PasteMeasuresAsLink(commands.Command):
root = sheet.root
src_measures = [root.get_object(obj_id) for obj_id in self.src_ids]
src_measures = [root.deserialize_object(obj) for obj in self.src_objs]
assert all(isinstance(obj, base_track.Measure) for obj in src_measures)
target_measures = [
@ -216,18 +250,32 @@ class PasteMeasuresAsLink(commands.Command):
assert all(
isinstance(obj, base_track.MeasureReference) for obj in target_measures)
affected_track_ids = set(
obj.track.id for obj in src_measures + target_measures)
affected_track_ids = set(obj.track.id for obj in target_measures)
assert len(affected_track_ids) == 1
for target, src in zip(
target_measures, itertools.cycle(src_measures)):
target.measure_id = src.id
if self.mode == 'link':
for target, src in zip(target_measures, itertools.cycle(src_measures)):
assert(any(src.id == m.id for m in target.track.measure_heap))
target.measure_id = src.id
elif self.mode == 'overwrite':
measure_map = {}
for target, src in zip(target_measures, itertools.cycle(src_measures)):
try:
measure = measure_map[src.id]
except KeyError:
measure = measure_map[src.id] = src.clone()
target.track.measure_heap.append(measure)
target.measure_id = measure.id
else:
raise ValueError(mode)
for track_id in affected_track_ids:
root.get_object(track_id).garbage_collect_measures()
commands.Command.register_command(PasteMeasuresAsLink)
commands.Command.register_command(PasteMeasures)
class AddPipelineGraphNode(commands.Command):

@ -20,6 +20,7 @@
#
# @end:license
import copy
import logging
import traceback
import uuid
@ -97,8 +98,6 @@ class StateBase(model_base.ObjectBase):
state = getattr(self, prop.name, prop.default)
elif isinstance(prop, model_base.ListProperty):
state = list(getattr(self, prop.name, []))
elif isinstance(prop, model_base.DictProperty):
state = dict(getattr(self, prop.name, {}))
elif isinstance(prop, model_base.ObjectProperty):
obj = getattr(self, prop.name, None)
if obj is not None:
@ -134,10 +133,6 @@ class StateBase(model_base.ObjectBase):
lst = getattr(self, prop.name)
lst.clear()
lst.extend(value)
elif isinstance(prop, model_base.DictProperty):
dct = getattr(self, prop.name)
dct.clear()
dct.update(value)
elif isinstance(prop, model_base.ObjectProperty):
if value is not None:
cls_name = value['__class__']
@ -163,6 +158,47 @@ class StateBase(model_base.ObjectBase):
else:
raise TypeError("Unknown property type %s" % type(prop))
def clone(self):
cls = type(self)
obj = cls(state={'id': uuid.uuid4().hex})
obj.copy_from(self)
return obj
def copy_from(self, src):
assert isinstance(src, type(self))
for prop in src.list_properties():
if prop.name == 'id':
continue
if isinstance(prop, model_base.Property):
value = prop.__get__(src, src.__class__)
prop.__set__(self, value)
elif isinstance(prop, model_base.ListProperty):
lst = prop.__get__(self, self.__class__)
lst.clear()
for value in prop.__get__(src, src.__class__):
lst.append(copy.deepcopy(value))
elif isinstance(prop, model_base.ObjectProperty):
obj = prop.__get__(src, src.__class__)
if obj is not None:
prop.__set__(self, obj.clone())
else:
prop.__set__(self, None)
elif isinstance(prop, model_base.ObjectListProperty):
lst = prop.__get__(self, self.__class__)
lst.clear()
for obj in prop.__get__(src, src.__class__):
lst.append(obj.clone())
else:
assert isinstance(prop, model_base.ObjectReferenceProperty)
obj = prop.__get__(src, src.__class__)
prop.__set__(self, obj)
class RootMixin(object):
def __init__(self, state=None):

@ -97,13 +97,6 @@ from noisicaa import core
# with self.assertRaises(TypeError):
# l[0] = "str"
# def testDict(self):
# p = state.DictProperty()
# p.name = 'a'
# self.assertEqual(p.__get__(self.obj, self.obj.__class__), {})
# d = p.__get__(self.obj, self.obj.__class__)
# d['foo'] = 1
# self.assertEqual(p.__get__(self.obj, self.obj.__class__), {'foo': 1})
class TestStateBase(state.StateBase):
cls_map = {}
@ -113,23 +106,23 @@ class StateTest(unittest.TestCase):
def tearDown(self):
TestStateBase.clear_class_registry()
def validateNode(self, root, parent, node):
def _validate_node(self, root, parent, node):
self.assertIs(node.parent, parent)
self.assertIs(node.root, root)
for c in node.list_children():
self.validateNode(root, node, c)
self._validate_node(root, node, c)
def validateTree(self, root):
self.validateNode(root, None, root)
def _validate_tree(self, root):
self._validate_node(root, None, root)
def testListChildrenLeaf(self):
def test_list_children_leaf(self):
class Leaf(state.RootMixin, TestStateBase):
pass
a = Leaf()
self.assertEqual(list(a.list_children()), [])
def testListChildrenObjectProperty(self):
def test_list_children_object_property(self):
class Leaf(TestStateBase):
pass
class Root(state.RootMixin, TestStateBase):
@ -138,7 +131,7 @@ class StateTest(unittest.TestCase):
b = Root()
b.child = a
def testListChildrenObjectListProperty(self):
def test_list_children_object_list_property(self):
class Leaf(state.StateBase):
pass
class Root(state.RootMixin, TestStateBase):
@ -150,7 +143,7 @@ class StateTest(unittest.TestCase):
c.children.append(b)
self.assertEqual(list(c.list_children()), [a, b])
def testSerialize(self):
def test_serialize(self):
class Root(state.RootMixin, TestStateBase):
name = core.Property(str, default='')
a1 = core.Property(int, default=2)
@ -158,7 +151,7 @@ class StateTest(unittest.TestCase):
a = Root()
a.id = 'id1'
a.name = 'foo'
self.validateTree(a)
self._validate_tree(a)
self.assertEqual(
a.serialize(),
{'__class__': 'Root',
@ -166,17 +159,17 @@ class StateTest(unittest.TestCase):
'name': 'foo',
'a1': 2})
def testDeserialize(self):
def test_deserialize(self):
class Root(state.RootMixin, TestStateBase):
name = core.Property(str, default='')
a1 = core.Property(int, default=2)
a = Root(state={'name': 'foo'})
self.validateTree(a)
self._validate_tree(a)
self.assertEqual(a.name, 'foo')
self.assertEqual(a.a1, 2)
def testAttr(self):
def test_attr(self):
class Root(state.RootMixin, TestStateBase):
name = core.Property(str, default='')
a1 = core.Property(int, default=2)
@ -185,16 +178,16 @@ class StateTest(unittest.TestCase):
a.id = 'id1'
a.name = 'a'
a.a1 = 4
self.validateTree(a)
self._validate_tree(a)
serialized = json.loads(json.dumps(a.serialize()))
b = Root(state=serialized)
self.validateTree(b)
self._validate_tree(b)
self.assertEqual(b.id, 'id1')
self.assertEqual(b.name, 'a')
self.assertEqual(b.a1, 4)
def testChildObject(self):
def test_child_object(self):
class Leaf(TestStateBase):
name = core.Property(str, default='')
a1 = core.Property(int, default=2)
@ -210,7 +203,7 @@ class StateTest(unittest.TestCase):
b = Root()
b.id = 'id2'
b.child = a
self.validateTree(b)
self._validate_tree(b)
self.assertEqual(
b.serialize(),
{'__class__': 'Root',
@ -222,19 +215,19 @@ class StateTest(unittest.TestCase):
serialized = json.loads(json.dumps(b.serialize()))
c = Root(state=serialized)
self.validateTree(c)
self._validate_tree(c)
self.assertEqual(c.id, 'id2')
self.assertIsInstance(c.child, Leaf)
self.assertEqual(c.child.id, 'id1')
self.assertEqual(c.child.name, 'a')
self.assertEqual(c.child.a1, 4)
# def testInherit(self):
# def test_inherit(self):
# a = LeafNodeSub1()
# a.id = 'id1'
# a.name = 'foo'
# a.a2 = 13
# self.validateTree(a)
# self._validate_tree(a)
# self.assertEqual(
# a.serialize(),
# {'__class__': 'LeafNodeSub1',
@ -245,12 +238,12 @@ class StateTest(unittest.TestCase):
# state = json.loads(json.dumps(a.serialize()))
# b = LeafNodeSub1(state=state)
# self.validateTree(b)
# self._validate_tree(b)
# self.assertEqual(b.name, 'foo')
# self.assertEqual(b.a1, 2)
# self.assertEqual(b.a2, 13)
# def testChildObjectSubclass(self):
# def test_child_object_subclass(self):
# a = LeafNodeSub2()
# a.id = 'id1'
# a.name = 'a'
@ -258,7 +251,7 @@ class StateTest(unittest.TestCase):
# b = NodeWithSubclassChild()
# b.id = 'id2'
# b.child = a
# self.validateTree(b)
# self._validate_tree(b)
# self.assertEqual(
# b.serialize(),
# {'__class__': 'NodeWithSubclassChild',
@ -271,7 +264,7 @@ class StateTest(unittest.TestCase):
# state = json.loads(json.dumps(b.serialize()))
# c = NodeWithSubclassChild(state=state)
# self.validateTree(c)
# self._validate_tree(c)
# self.assertEqual(c.id, 'id2')
# self.assertIsInstance(c.child, LeafNodeSub2)
# self.assertEqual(c.child.id, 'id1')
@ -279,7 +272,7 @@ class StateTest(unittest.TestCase):
# self.assertEqual(c.child.a1, 2)
# self.assertEqual(c.child.a3, 17)
# def testChildObjectList(self):
# def test_child_object_list(self):
# a = LeafNode()
# a.id = 'id1'
# a.name = 'a'
@ -292,7 +285,7 @@ class StateTest(unittest.TestCase):
# c.id = 'id3'
# c.children.append(a)
# c.children.append(b)
# self.validateTree(c)
# self._validate_tree(c)
# self.assertEqual(
# c.serialize(),
# {'__class__': 'NodeWithChildren',
@ -308,7 +301,7 @@ class StateTest(unittest.TestCase):
# state = json.loads(json.dumps(c.serialize()))
# d = NodeWithChildren(state=state)
# self.validateTree(d)
# self._validate_tree(d)
# self.assertEqual(d.id, 'id3')
# self.assertIsInstance(d.children[0], LeafNode)
# self.assertEqual(d.children[0].id, 'id1')
@ -319,7 +312,7 @@ class StateTest(unittest.TestCase):
# self.assertEqual(d.children[1].name, 'b')
# self.assertEqual(d.children[1].a1, 5)
def testObjectReference(self):
def test_object_reference(self):
class Leaf(TestStateBase):
name = core.Property(str)
TestStateBase.register_class(Leaf)
@ -342,7 +335,7 @@ class StateTest(unittest.TestCase):
c.id = 'id3'
c.children.append(a)
c.children.append(b)
self.validateTree(c)
self._validate_tree(c)
self.assertEqual(
c.serialize(),
{'__class__': 'Root',
@ -358,7 +351,7 @@ class StateTest(unittest.TestCase):
serialized = json.loads(json.dumps(c.serialize()))
d = Root(state=serialized)
d.init_references()
self.validateTree(d)
self._validate_tree(d)
self.assertEqual(d.id, 'id3')
self.assertIsInstance(d.children[0], Leaf)
self.assertEqual(d.children[0].id, 'id1')
@ -375,7 +368,7 @@ class StateTest(unittest.TestCase):
d.children.append(e)
self.assertIs(d.children[1].other, d.children[0])
# def testChangeListener(self):
# def test_change_listener(self):
# a = LeafNode()
# a.id = 'id1'
# a.name = 'a'
@ -390,7 +383,7 @@ class StateTest(unittest.TestCase):
# a.name = 'c'
# self.assertEqual(changes, [('a', 'b'), ('b', 'c')])
# def testChangeListenerList(self):
# def test_change_listener_list(self):
# a = NodeWithChildren()
# n1 = LeafNode()
@ -423,7 +416,7 @@ class StateTest(unittest.TestCase):
# ('clear', ()),
# ])
# def testObjectListIndex(self):
# def test_object_list_index(self):
# a = LeafNode()
# with self.assertRaises(ObjectNotAttachedError):
# a.index
@ -446,7 +439,7 @@ class StateTest(unittest.TestCase):
# self.assertEqual(a.index, 0)
# self.assertEqual(b.index, 1)
# def testObjectListSiblings(self):
# def test_object_list_siblings(self):
# l = NodeWithChildren()
# l.children.append(LeafNode())
# l.children.append(LeafNode())
@ -470,6 +463,118 @@ class StateTest(unittest.TestCase):
# self.assertIs(l.children[1].prev_sibling, l.children[0])
# self.assertIs(l.children[2].prev_sibling, l.children[1])
def test_clone(self):
class Child(state.StateBase):
name = core.Property(str)
def __init__(self, *, name=None, state=None):
super().__init__(state=state)
if state is None:
self.name = name
class Object(state.RootMixin, state.StateBase):
name = core.Property(str)
lst = core.ListProperty(int)
child = core.ObjectProperty(Child)
none = core.ObjectProperty(Child)
children = core.ObjectListProperty(Child)
ref = core.ObjectReferenceProperty(Child)
def __init__(self, *,
name=None, lst=None, child=None, children=None, ref=None, state=None):
super().__init__(state=state)
if state is None:
self.name = name
self.lst.extend(lst)
self.child = child
self.none = None
self.children.extend(children)
self.ref = ref
r2 = Child(name='r2')
o2 = Object(
name='bar',
lst=[4, 5, 6],
child=Child(name='c2'),
children=[Child(name='cl3'), Child(name='cl4')],
ref=r2,
)
o1 = o2.clone()
self.assertNotEqual(o1.id, o2.id)
self.assertEqual(o1.name, 'bar')
self.assertEqual(o1.lst, [4, 5, 6])
self.assertEqual(o1.child.name, 'c2')
self.assertNotEqual(o1.child.id, o2.child.id)
self.assertEqual(o1.children[0].name, 'cl3')
self.assertNotEqual(o1.children[0].id, o2.children[0].id)
self.assertEqual(o1.children[1].name, 'cl4')
self.assertNotEqual(o1.children[1].id, o2.children[1].id)
self.assertEqual(o1.ref.name, 'r2')
self.assertEqual(o1.ref.id, r2.id)
def test_copy_from(self):
class Child(state.StateBase):
name = core.Property(str)
def __init__(self, *, name=None, state=None):
super().__init__(state=state)
if state is None:
self.name = name
class Object(state.RootMixin, state.StateBase):
name = core.Property(str)
lst = core.ListProperty(int)
child = core.ObjectProperty(Child)
none = core.ObjectProperty(Child)
children = core.ObjectListProperty(Child)
ref = core.ObjectReferenceProperty(Child)
def __init__(self, *,
name=None, lst=None, child=None, children=None, ref=None, state=None):
super().__init__(state=state)
if state is None:
self.name = name
self.lst.extend(lst)
self.child = child
self.none = None
self.children.extend(children)
self.ref = ref
r1 = Child(name='r1')
o1 = Object(
name='foo',
lst=[1, 2, 3],
child=Child(name='c1'),
children=[Child(name='cl1'), Child(name='cl2')],
ref=r1,
)
r2 = Child(name='r2')
o2 = Object(
name='bar',
lst=[4, 5, 6],
child=Child(name='c2'),
children=[Child(name='cl3'), Child(name='cl4')],
ref=r2,
)
id1 = o1.id
o1.copy_from(o2)
self.assertEqual(o1.id, id1)
self.assertEqual(o1.name, 'bar')
self.assertEqual(o1.lst, [4, 5, 6])
self.assertEqual(o1.child.name, 'c2')
self.assertNotEqual(o1.child.id, o2.child.id)
self.assertEqual(o1.children[0].name, 'cl3')
self.assertNotEqual(o1.children[0].id, o2.children[0].id)
self.assertEqual(o1.children[1].name, 'cl4')
self.assertNotEqual(o1.children[1].id, o2.children[1].id)
self.assertEqual(o1.ref.name, 'r2')
self.assertEqual(o1.ref.id, r2.id)
if __name__ == '__main__':
unittest.main()

@ -198,6 +198,11 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
statusTip="Redo most recently undone action",
triggered=self.onRedo)
self._clear_selection_action = QtWidgets.QAction(
"Clear", self,
statusTip="Clear the selected items",
triggered=self.onClearSelection)
self._copy_action = QtWidgets.QAction(
"Copy", self,
shortcut=QtGui.QKeySequence.Copy,
@ -206,11 +211,16 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self._paste_as_link_action = QtWidgets.QAction(
"Paste as link", self,
shortcut=QtGui.QKeySequence.Paste,
statusTip=("Paste items from clipboard to current location as"
" linked items"),
triggered=self.onPasteAsLink)
self._paste_action = QtWidgets.QAction(
"Paste", self,
shortcut=QtGui.QKeySequence.Paste,
statusTip=("Paste items from clipboard to current location"),
triggered=self.onPaste)
self._restart_action = QtWidgets.QAction(
"Restart", self,
shortcut="F5", shortcutContext=Qt.ApplicationShortcut,
@ -342,7 +352,9 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
self._edit_menu.addAction(self._undo_action)
self._edit_menu.addAction(self._redo_action)
self._project_menu.addSeparator()
self._edit_menu.addAction(self._clear_selection_action)
self._edit_menu.addAction(self._copy_action)
self._edit_menu.addAction(self._paste_action)
self._edit_menu.addAction(self._paste_as_link_action)
self._view_menu = menu_bar.addMenu("View")
@ -604,13 +616,21 @@ class EditorWindow(ui_base.CommonMixin, QtWidgets.QMainWindow):
project_view = self.getCurrentProjectView()
self.call_async(project_view.project_client.redo())
def onClearSelection(self):
view = self._project_tabs.currentWidget()
view.onClearSelection()
def onCopy(self):
view = self._project_tabs.currentWidget()
view.onCopy()
def onPaste(self):
view = self._project_tabs.currentWidget()
view.onPaste(mode='overwrite')
def onPasteAsLink(self):
view = self._project_tabs.currentWidget()
view.onPasteAsLink()
view.onPaste(mode='link')
def onPlaybackStateChanged(self, state):
if state == 'playing':

@ -258,6 +258,10 @@ class ProjectViewImpl(QtWidgets.QMainWindow):
view = self.currentSheetView()
view.onRender()
def onClearSelection(self):
view = self.currentSheetView()
view.onClearSelection()
def onCopy(self):
if self.selection_set.empty():
return
@ -266,15 +270,15 @@ class ProjectViewImpl(QtWidgets.QMainWindow):
async def onCopyAsync(self):
data = []
for obj in self.selection_set:
data.append(await obj.getCopy())
for item in sorted(self.selection_set, key=lambda item: item.measure_reference.index):
data.append(await item.getCopy())
self.app.setClipboardContent(
{'type': 'measures', 'data': data})
def onPasteAsLink(self):
def onPaste(self, *, mode):
view = self.currentSheetView()
view.onPasteAsLink()
view.onPaste(mode=mode)
class ProjectView(ui_base.ProjectMixin, ProjectViewImpl):

@ -50,9 +50,6 @@ class SelectionSet(object):
logger.info("Adding to selection: %s", obj)
# TODO: Allow multiple selection.
self.clear()
assert obj.selection_class is not None
self.__selection_set.add(obj)

@ -21,6 +21,7 @@
# @end:license
from fractions import Fraction
import itertools
import logging
import enum
import time
@ -633,25 +634,75 @@ class MeasuredToolBase(tools.ToolBase):
evt.setAccepted(measure_evt.isAccepted())
class ArrangeMeasuresTool(MeasuredToolBase):
class ArrangeMeasuresTool(tools.ToolBase):
def __init__(self, **kwargs):
super().__init__(
type=tools.ToolType.ARRANGE_MEASURES,
group=tools.ToolGroup.ARRANGE,
**kwargs)
self.__selection_first = None
self.__selection_last = None
def iconName(self):
return 'pointer'
def mousePressEvent(self, target, evt):
assert isinstance(target, MeasureEditorItem), type(target).__name__
track_item = target.track_item
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
if target.selected():
track_item.selection_set.remove(target)
else:
track_item.selection_set.add(target)
evt.accept()
if (evt.button() == Qt.LeftButton
and evt.modifiers() == Qt.NoModifier):
measure_item = target.measureItemAt(evt.pos())
if isinstance(measure_item, MeasureEditorItem):
self.selection_set.clear()
self.selection_set.add(measure_item)
self.__selection_first = measure_item
self.__selection_last = None
evt.accept()
return
# TODO: handle click on appendix
super().mousePressEvent(target, evt)
def mouseReleaseEvent(self, target, evt):
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
if evt.button() == Qt.LeftButton:
measure_item = target.measureItemAt(evt.pos())
self.__selection_first = None
self.__selection_last = None
evt.accept()
return
super().mouseReleaseEvent(target, evt)
def mouseMoveEvent(self, target, evt):
assert isinstance(target, MeasuredTrackEditorItem), type(target).__name__
measure_item = target.measureItemAt(evt.pos())
if self.__selection_first is not None and isinstance(measure_item, MeasureEditorItem):
start_idx = self.__selection_first.measure_reference.index
last_idx = measure_item.measure_reference.index
if start_idx > last_idx:
start_idx, last_idx = last_idx, start_idx
for mitem in itertools.islice(
target.measure_items(), start_idx, last_idx + 1):
if not mitem.selected():
self.selection_set.add(mitem)
for mitem in list(self.selection_set):
if (not (start_idx <= mitem.measure_reference.index <= last_idx)
and mitem.selected()):
self.selection_set.remove(mitem)
self.__selection_last = measure_item
evt.accept()
class MeasuredTrackEditorItem(BaseTrackEditorItem):

@ -37,7 +37,6 @@ from noisicaa.music import model
from noisicaa.music import time_mapper
from noisicaa.ui import tools
from noisicaa.ui import ui_base
from noisicaa.ui import selection_set
from . import base_track_item
from . import score_track_item
from . import beat_track_item
@ -295,8 +294,6 @@ class SheetEditor(TrackViewMixin, ui_base.ProjectMixin, AsyncSetupBase, QtWidget
self.__time_mapper = music.TimeMapper(self.__sheet)
self.__selection_set = selection_set.SelectionSet()
super().__init__(sheet, **kwargs)
self.setAttribute(Qt.WA_OpaquePaintEvent)
@ -465,15 +462,30 @@ class SheetEditor(TrackViewMixin, ui_base.ProjectMixin, AsyncSetupBase, QtWidget
for track_item in self.tracks():
track_item.setPlaybackPos(timepos)
def onPasteAsLink(self):
def onClearSelection(self):
if self.selection_set.empty():
return
self.send_command_async(
self.__sheet.id, 'ClearMeasures',
measure_ids=[
mref.id for mref in sorted(
(measure_item.measure_reference
for measure_item in self.selection_set),
key=lambda mref: mref.index)])
def onPaste(self, *, mode):
assert mode in ('overwrite', 'link')
if self.selection_set.empty():
return
clipboard = self.app.clipboardContent()
if clipboard['type'] == 'measures':
self.send_command_async(
self.__sheet.id, 'PasteMeasuresAsLink',
src_ids=[copy['id'] for copy in clipboard['data']],
self.__sheet.id, 'PasteMeasures',
mode=mode,
src_objs=[copy['data'] for copy in clipboard['data']],
target_ids=[
mref.id for mref in sorted(
(measure_item.measure_reference
@ -1397,8 +1409,11 @@ class SheetViewImpl(AsyncSetupBase, QtWidgets.QWidget):
await self.project_client.undo()
await self.project_client.restart_player_pipeline(self.__player_id)
def onPasteAsLink(self):
self.__sheet_editor.onPasteAsLink()
def onClearSelection(self):
self.__sheet_editor.onClearSelection()
def onPaste(self, *, mode):
self.__sheet_editor.onPaste(mode=mode)
class SheetView(ui_base.ProjectMixin, SheetViewImpl):

Loading…
Cancel
Save