public void GroupSelectedBlocks()
        {
            if (!SelectedBlocks.Any())
            {
                return;
            }

            var          relatedSegments = Enumerable.Select(SelectedBlocks, b => b.SegmentContext).Distinct().ToList();
            MusicSegment segmentForGroup = (relatedSegments.Count == 1 ? relatedSegments.Single().GetModel() : model.MusicSegments[0]);

            var group = new LoopBlock(model);

            group.SegmentContext = segmentForGroup;
            group.StartTime      = SelectedBlocks.Min(b => b.StartTime);
            foreach (var b in SelectedBlocks)
            {
                // create an independent copy of the block so transforming its time to local reference frame does not screw up undo
                Block newChild = Block.FromXML(model, b.GetModel().ToXML());

                group.AddChild(newChild, true);
            }

            using (ActionManager.CreateTransaction())
            {
                DeleteSelectedBlocks();
                ActionManager.RecordAdd(model.Blocks, group);
            }

            SelectBlock(BlockViewModel.FromModel(this, group), CompositionMode.None);
        }
        public void UngroupSelectedBlocks()
        {
            using (ActionManager.CreateTransaction())
            {
                foreach (var groupVM in SelectedBlocks.OfType <GroupBlockViewModel>().ToArray())
                {
                    SelectedBlocks.Remove(groupVM);

                    GroupBlock group = (GroupBlock)groupVM.GetModel();
                    ActionManager.RecordRemove(model.Blocks, group);

                    foreach (Block b in group.Children.ToArray())
                    {
                        // create an independent copy of the block so transforming its time back to global reference frame does not screw up undo
                        Block independentBlock = Block.FromXML(model, b.ToXML());

                        independentBlock.StartTime += group.StartTime;
                        ActionManager.RecordRemove(group.Children, b);
                        ActionManager.RecordAdd(model.Blocks, independentBlock);

                        SelectBlock(BlockViewModel.FromModel(this, independentBlock), CompositionMode.Additive);
                    }
                }
            }
        }
        public TrackViewModel(SequencerViewModel sequencer, Model.Track model)
        {
            this.sequencer = sequencer;
            this.model     = model;

            ForwardPropertyEvents(nameof(model.Label), model, nameof(Label));
            ForwardPropertyEvents(nameof(sequencer.SelectedTrack), sequencer, nameof(IsSelected));

            Blocks = model.Blocks.Select(b => BlockViewModel.FromModel(sequencer, b));
        }
        // type may be null, then the target will be auto-inferred based on currently selected blocks
        public void ConvertSelectedBlocksTo(string type)
        {
            bool rampsToColors = (type == "color");
            bool colorsToRamps = (type == "ramp");

            if (!rampsToColors && !colorsToRamps)
            {
                colorsToRamps = SelectedBlocks.Any(b => b is ColorBlockViewModel);
                rampsToColors = SelectedBlocks.Any(b => b is RampBlockViewModel);
                if (colorsToRamps && rampsToColors)
                {
                    throw new ArgumentException("type needs to be given when both color and ramp blocks are selected");
                }
            }

            using (ActionManager.CreateTransaction())
            {
                for (int i = 0; i < SelectedBlocks.Count; i++)
                {
                    var   block = SelectedBlocks[i].GetModel();
                    Block convertedBlock;
                    if (rampsToColors && block is RampBlock rampBlock)
                    {
                        convertedBlock = new ColorBlock(model, rampBlock.Tracks.ToArray())
                        {
                            Color = (rampBlock.StartColor == GloColor.Black ? rampBlock.EndColor : rampBlock.StartColor)
                        };
                    }
                    else if (colorsToRamps && block is ColorBlock colorBlock)
                    {
                        convertedBlock = new RampBlock(model, colorBlock.Tracks.ToArray())
                        {
                            StartColor = colorBlock.Color,
                            EndColor   = (colorBlock.Color == GloColor.White ? GloColor.Black : GloColor.White)
                        };
                    }
                    else
                    {
                        continue;
                    }

                    // copy generic properties
                    convertedBlock.StartTime      = block.StartTime;
                    convertedBlock.Duration       = block.Duration;
                    convertedBlock.SegmentContext = block.SegmentContext;

                    ActionManager.RecordReplace(model.Blocks, block, convertedBlock);
                    SelectedBlocks[i] = BlockViewModel.FromModel(this, convertedBlock);
                }
            }
        }
        public void SplitBlocksAtCursor()
        {
            var blocksUnderCursor = SelectedBlocks
                                    .AsEnumerable()
                                    .Where(bvm => bvm.StartTime <CursorPosition && bvm.EndTime> CursorPosition)
                                    .Select(bvm => bvm.GetModel())
                                    .ToList();

            using (ActionManager.CreateTransaction(false))
            {
                foreach (var block in blocksUnderCursor)
                {
                    // Generate 2 exact copies of the block.
                    XElement serializedBlock = block.ToXML();
                    var      newBlockLeft    = Block.FromXML(model, serializedBlock);
                    var      newBlockRight   = Block.FromXML(model, serializedBlock);

                    // Adjust ramps if necessary.
                    if (block is RampBlock ramp)
                    {
                        GloColor splitColor = ramp.GetColorAtTime(CursorPosition, ramp.Tracks[0]);
                        ((RampBlock)newBlockLeft).EndColor    = splitColor;
                        ((RampBlock)newBlockRight).StartColor = splitColor;
                    }
                    else if (block is LoopBlock)
                    {
                        // TODO: Loops are unsupported when splitting.
                        continue;
                    }

                    // Generically adjust times.
                    newBlockLeft.Duration   = CursorPosition - block.StartTime;
                    newBlockRight.StartTime = CursorPosition;
                    newBlockRight.Duration  = block.Duration - (CursorPosition - block.StartTime);

                    // Replace in collections.
                    int index = model.Blocks.IndexOf(block);
                    ActionManager.RecordReplace(model.Blocks, index, newBlockLeft);
                    ActionManager.RecordInsert(model.Blocks, index + 1, newBlockRight);

                    SelectBlock(BlockViewModel.FromModel(this, block), CompositionMode.Subtractive);
                    SelectBlock(BlockViewModel.FromModel(this, newBlockLeft), CompositionMode.Additive);
                    SelectBlock(BlockViewModel.FromModel(this, newBlockRight), CompositionMode.Additive);
                }
            }
        }
        public void InsertBlock(string type)
        {
            // inherit color and tracks from previous block, if applicable
            GloColor prevColor = GloColor.White;

            Track[] prevTracks = { _selectedTrack.GetModel() };

            if (EnableSmartInsert)
            {
                Func <BlockViewModel, bool> fnIsBlockApplicable =
                    (bl => bl.StartTime < CursorPosition && (bl is ColorBlockViewModel || bl is RampBlockViewModel));

                var prevBlocks = ((IEnumerable <BlockViewModel>)_selectedTrack.Blocks).Where(fnIsBlockApplicable);
                if (prevBlocks.Any())
                {
                    BlockViewModel prevBlock = prevBlocks.MaxBy(bl => bl.EndTimeOccupied);

                    // inherit color
                    if (prevBlock is ColorBlockViewModel)
                    {
                        prevColor = ((ColorBlockViewModel)prevBlock).GetModel().Color;
                    }
                    else
                    {
                        prevColor = ((RampBlockViewModel)prevBlock).GetModel().EndColor;
                    }

                    // inherit tracks, but only if the last block on the selected track is also the last block on all other tracks of the block
                    bool lastOfAllTracks = prevBlock.GetModel().Tracks.All(t => t.Blocks
                                                                           .Select(bl => BlockViewModel.FromModel(this, bl))
                                                                           .Where(fnIsBlockApplicable)
                                                                           .MaxBy(bl => bl.EndTimeOccupied)
                                                                           == prevBlock);
                    if (lastOfAllTracks)
                    {
                        prevTracks = prevBlock.GetModel().Tracks.ToArray();
                    }
                }
            }

            Block b;

            switch (type)
            {
            case "color":
                b = new ColorBlock(model, prevTracks)
                {
                    Color = prevColor
                };
                break;

            case "ramp":
                b = new RampBlock(model, prevTracks)
                {
                    StartColor = prevColor,
                    EndColor   = (prevColor == GloColor.White ? GloColor.Black : GloColor.White)
                };
                break;

            default:
                throw new ArgumentException("unsupported block type " + type);
            }

            b.SegmentContext = ActiveMusicSegment.GetModel();
            b.StartTime      = CursorPosition;
            b.Duration       = GridInterval;

            ActionManager.RecordAdd(model.Blocks, b);
        }
        public SequencerViewModel(Timeline model)
        {
            this.model = model;

            ActionManager  = new GuiLabs.Undo.ActionManager();
            SelectedBlocks = new ObservableCollection <BlockViewModel>();
            SelectionData  = new SelectionProperties(this);
            Tracks         = model.Tracks.Select(g => new TrackViewModel(this, g));
            MusicSegments  = model.MusicSegments.Select(seg => new MusicSegmentViewModel(this, seg));
            AllBlocks      = model.Blocks.Select(b => BlockViewModel.FromModel(this, b));

            if (Tracks.Count > 0)
            {
                SelectedTrack = Tracks[0];
            }

            ActiveMusicSegment = MusicSegments[model.DefaultMusicSegment.GetIndex()];
            Playback           = new PlaybackViewModel(this);
            Visualization      = new VisualizationViewModel(this);
            Notes = new NotesViewModel(this);

            if (model.MusicFileName != null)
            {
                Playback.LoadFileAsync(model.MusicFileName).Forget();
            }

            Action <BlockViewModel> fn_SubscribeToBlock = bvm => ForwardPropertyEvents(nameof(bvm.EndTimeOccupied), bvm, nameof(TimelineLength));

            AllBlocks.ToList().ForEach(fn_SubscribeToBlock);
            AllBlocks.CollectionChanged += (_, e) =>
            {
                if (e.NewItems != null)
                {
                    e.NewItems.Cast <BlockViewModel>().ToList().ForEach(fn_SubscribeToBlock);
                }
                Notify(nameof(TimelineLength));
            };

            ForwardPropertyEvents(nameof(PipetteTarget), this, nameof(IsPipetteActive));
            ForwardPropertyEvents(nameof(CursorPosition), this,
                                  nameof(CursorPixelPosition), nameof(CursorPixelPositionOnViewport));
            ForwardPropertyEvents(nameof(TimePixelScale), this,
                                  nameof(CursorPixelPosition), nameof(CursorPixelPositionOnViewport),
                                  nameof(CurrentViewLeftPositionTime), nameof(CurrentViewRightPositionTime),
                                  nameof(TimelineWidth), nameof(GridInterval));
            ForwardPropertyEvents(nameof(ActiveMusicSegment), this,
                                  nameof(GridInterval));

            ForwardPropertyEvents(nameof(Playback.MusicDuration), Playback, nameof(TimelineLength));
            ForwardPropertyEvents(nameof(TimelineLength), this, nameof(TimelineWidth));

            ForwardCollectionEvents(SelectedBlocks,
                                    nameof(CanConvertToColor), nameof(CanConvertToRamp),
                                    nameof(CanConvertToAutoDeduced), nameof(ConvertAutoDeduceGestureText));

            Tracks.CollectionChanged += (_, e) =>
            {
                foreach (var b in AllBlocks)
                {
                    b.OnTracksCollectionChanged();
                }
            };

            // Disable pipette whenever the selection is modified.
            SelectedBlocks.CollectionChanged += (_, __) => PipetteTarget = null;
        }
 protected GroupBlockViewModel(SequencerViewModel sequencer, Model.GroupBlock model, string typeLabel)
     : base(sequencer, model, typeLabel)
 {
     this.model = model;
     _children  = model.Children.Select(b => BlockViewModel.FromModel(sequencer, b));
 }