/// <summary> /// Moves an event to a new start date (preserving duration) and updates labels. /// </summary> /// <param name="ev">Event to move</param> /// <param name="newStartDate">New start date for event</param> private void MoveEvent(Event ev, DateTime newStartDate) { var newRange = new TimeRange(ev.Start, ev.End); newRange.MoveToDate(newStartDate); UpdateEvent(ev, newRange); }
/// <summary> /// Highlights destination cells during drag /// </summary> private void HandleDragging(object sender, DragEventArgs args) { var label = sender as DraggableView; if (label == null) { return; } var ev = label.BindingContext as Event; if (ev == null) { return; } var destinationPoint = GetDragDestinationPoint(args.Center, label); var newDate = GetDateAtPoint(destinationPoint); if (newDate.HasValue) { // Offset for this not being the first label of the event newDate = newDate.Value.AddDays(-_dragLabelDayOffset); var newRange = new TimeRange(ev.Start, ev.End); newRange.MoveToDate(newDate.Value); // Highlight target cells, un-highlight previously-targeted cells // HighlightCells(newRange.StartDate, newRange.EndDate); } }
/// <summary> /// Remove resize mode views /// </summary> /// <param name="overlay">The resize mode overlay that triggered this, so we can remove it</param> private void EndResizeMode(View overlay) { _grid.Children.Remove(overlay); _grid.Children.Remove(_rightResizeHandle); _grid.Children.Remove(_rightResizeHandleDragger); _grid.Children.Remove(_leftResizeHandle); _grid.Children.Remove(_leftResizeHandleDragger); ((TappableView)overlay).Tapped -= HandleOverlayTapped; _rightResizeHandleDragger.Dragging -= HandleResizeDragging; _rightResizeHandleDragger.Dragged -= HandleResizeDragged; _rightResizeHandleDragger.DragCanceled -= HandleResizeCanceled; _leftResizeHandleDragger.Dragging -= HandleResizeDragging; _leftResizeHandleDragger.Dragged -= HandleResizeDragged; _leftResizeHandleDragger.DragCanceled -= HandleResizeCanceled; // Reset opacity foreach (var label in _labels) { label.Opacity = 1.0; } UnhighlightCells(); _resizingEvent = null; _rightResizeHandle = null; _rightResizeHandleDragger = null; _leftResizeHandle = null; _leftResizeHandleDragger = null; _resizeRange = null; }
/// <summary> /// Creates overlay and tracking handles, highlights cells, and dims other events. /// </summary> /// <param name="ev">Event to resize</param> private void BeginResizeMode(Event ev) { _resizingEvent = ev; _resizeRange = new TimeRange(ev.Start, ev.End); // Create overlay to capture taps for dismissing resize mode // var overlay = new TappableView(); Grid.SetRowSpan(overlay, _rows + 1); Grid.SetColumnSpan(overlay, _cols); overlay.BackgroundColor = Color.Transparent; overlay.Tapped += HandleOverlayTapped; _grid.Children.Add(overlay); // Dim everything but this event // foreach (var label in _labels.Where(l => l.BindingContext != _resizingEvent)) { label.Opacity = 0.5; } HighlightCells(_resizingEvent.StartDate, _resizingEvent.EndDate); var cells = GetCellsForRange(_resizingEvent.StartDate, _resizingEvent.EndDate); var firstCell = cells.First(); var lastCell = cells.Last(); _leftResizeHandle = CreateResizeHandleVisual(Grid.GetColumn(firstCell), Grid.GetRow(firstCell), false); _grid.Children.Add(_leftResizeHandle); _rightResizeHandle = CreateResizeHandleVisual(Grid.GetColumn(lastCell), Grid.GetRow(lastCell), true); _grid.Children.Add(_rightResizeHandle); _leftResizeHandleDragger = CreateResizeHandleDraggerForVisual(_leftResizeHandle); _grid.Children.Add(_leftResizeHandleDragger); _rightResizeHandleDragger = CreateResizeHandleDraggerForVisual(_rightResizeHandle); _grid.Children.Add(_rightResizeHandleDragger); }
/// <summary> /// Applies the new range to the event and updates its label(s) /// </summary> /// <param name="ev">Event to update</param> /// <param name="newRange">Desired time range for the event</param> private void UpdateEvent(Event ev, TimeRange newRange) { // Q: Why don't we just update the properties on the Event and let the property change events handle this? // A: Mainly to avoid having to deal with multiple notifications firing for a single change (e.g., moving an event) // Updating the calendar labels is a bit more expensive than just updating a text box. if (newRange.Start == ev.Start && newRange.End == ev.End) { // no change return; } var newCells = GetCellsForRange(newRange.StartDate, newRange.EndDate); if (!newCells.Any()) { // Don't allow moving off the calendar entirely // return; } _updatingEvent = true; ev.Start = newRange.Start; ev.End = newRange.End; _updatingEvent = false; var labels = _labels.Where(l => l.BindingContext == ev).ToList(); System.Diagnostics.Debug.Assert(labels.Any()); var label = labels.First(); bool wasMultiRow = labels.Count > 1; var newRows = newCells.Select(cell => Grid.GetRow(cell)).Distinct().ToList(); bool willBeMultiRow = newRows.Count > 1; // If the event fit on a single row before and will still fit on a single row, // then we can "simply" move the existing label. Otherwise, we need to recreate, // because multiple labels are involved with changing widths etc. // if (!wasMultiRow && !willBeMultiRow && GetEventContainerForRow(newRows.First()) == label.ParentView) { var container = label.ParentView as AutoStackGrid; // So basically all the previous complexity is being turned into: // AutoStackGrids span whole week (row). If the event is being moved // within a row, we just reposition it in the stack grid. Otherwise // re-create. // Note that even with the stack-per-row model, moving items between // layout containers is problematic on WinPhone (hence re-creating instead) var newCols = newCells.Select(cell => Grid.GetColumn(cell)).OrderBy(col => col).ToList(); // Defer layout until we're done updating properties // container.DeferLayout = true; AutoStackGrid.SetColumn(label, newCols.First()); AutoStackGrid.SetColumnSpan(label, newCols.Last() - newCols.First() + 1); // Update margin. Even though we're staying on one row, there's the possibility that // the event actually stretches before and/or after that row and those parts just aren't visible // bool truncatedStart = ev.Start < (newCells.First().BindingContext as DateTime?).Value; // Subtracting 1 second because an end time of midnight is exclusive and should only // label the previous day // bool truncatedEnd = ev.End.AddSeconds(-1).Date > (newCells.Last().BindingContext as DateTime?).Value; var margin = new Thickness(!truncatedStart ? _defaultMargin : 0, _defaultMargin, !truncatedEnd ? _defaultMargin : 0, _defaultMargin); AutoStackGrid.SetMargin(label, margin); container.DeferLayout = false; container.ForceLayout(); } else { // Recreate labels // Removing a label just to create a new one in its place caused movement flicker // on WinPhone if we rely on adding/removing to automatically update layout. // So that behavior is temporarily disabled on AutoStackGrid, and instead we // explicitly call ForceLayout on grids that have been modified. // var oldContainers = labels.Select(l => l.ParentView).OfType<AutoStackGrid>().ToList(); foreach (var container in oldContainers) { container.DeferLayout = true; } ClearEventLabels(labels); _labels = _labels.Except(labels).ToList(); var newLabels = PlaceEvent(ev); foreach (var container in oldContainers.Union(newLabels.Select(l => l.ParentView)).OfType<AutoStackGrid>()) { container.ForceLayout(); container.DeferLayout = false; } } }
/// <summary> /// Don't exit resize mode, but reset any pending resize visuals /// </summary> private void ResetResizeMode() { _resizeRange = new TimeRange(_resizingEvent.Start, _resizingEvent.End); HighlightCells(_resizingEvent.StartDate, _resizingEvent.EndDate); var cells = GetCellsForRange(_resizingEvent.StartDate, _resizingEvent.EndDate); var firstCell = cells.First(); var lastCell = cells.Last(); Grid.SetColumn(_leftResizeHandle, Grid.GetColumn(firstCell)); Grid.SetRow(_leftResizeHandle, Grid.GetRow(firstCell)); Grid.SetColumn(_rightResizeHandle, Grid.GetColumn(lastCell)); Grid.SetRow(_rightResizeHandle, Grid.GetRow(lastCell)); SynchronizeResizeHandleDraggersWithVisuals(); }