| ... | ... | @@ -29,6 +29,7 @@ private func json(_ value: some Encodable) -> String { |
| 29 | 29 | /// either list. |
| 30 | 30 | enum Arranging { |
| 31 | 31 | static let history = UndoManager() |
| 32 | private(set) static var busy = false |
| 32 | 33 | private static let owner = NSObject() |
| 33 | 34 | |
| 34 | 35 | /// ⌘Z and ⇧⌘Z in a list, which, unlike a text view, has none of its own. |
| ... | ... | @@ -55,11 +56,26 @@ enum Arranging { |
| 55 | 56 | } |
| 56 | 57 | private static let queue = DispatchQueue(label: "Arranging") |
| 57 | 58 | |
| 58 | | private static func failed(_ title: String) { |
| 59 | private static func failed(_ title: String, _ message: String = "Check that the notebook can be reached, then try again.") { |
| 59 | 60 | let window = UIApplication.shared.connectedScenes.lazy.compactMap { ($0 as? UIWindowScene)?.keyWindow }.first |
| 60 | 61 | var top = window?.rootViewController |
| 61 | 62 | while let presented = top?.presentedViewController { top = presented } |
| 62 | | top?.alert(title, "Check that the notebook can be reached, then try again.") |
| 63 | top?.alert(title, message) |
| 64 | } |
| 65 | |
| 66 | private static func available() -> Bool { |
| 67 | guard !busy else { |
| 68 | failed("Notebook Is Busy", "Wait for the current notebook change to finish, then try again.") |
| 69 | return false |
| 70 | } |
| 71 | return true |
| 72 | } |
| 73 | |
| 74 | /// Excludes queued moves and filesystem mutations through their asynchronous outcomes. |
| 75 | static func reserve() -> (() -> Void)? { |
| 76 | guard available() else { return nil } |
| 77 | busy = true |
| 78 | return { busy = false } |
| 63 | 79 | } |
| 64 | 80 | |
| 65 | 81 | /// The pages `rows` lists with page `id` moved to `index` of them, at its level or under |
| ... | ... | @@ -76,6 +92,7 @@ enum Arranging { |
| 76 | 92 | |
| 77 | 93 | /// Puts the pages of `section` in `order`, page `id` the one moved, as one edit. |
| 78 | 94 | static func arrange(_ section: Section, _ order: [Placed], moving id: String) { |
| 95 | guard available() else { return } |
| 79 | 96 | let before = section.rows.map { Placed(id: $0.id, level: $0.level) } |
| 80 | 97 | guard order != before, section.arrange(order, moving: id) else { return } |
| 81 | 98 | let (notebook, path) = (section.notebook, section.tab.path) |
| ... | ... | @@ -87,6 +104,7 @@ enum Arranging { |
| 87 | 104 | /// Gives page `id` of `section` `level` where it stands, as Make Subpage and Promote |
| 88 | 105 | /// Subpage do. |
| 89 | 106 | static func indent(_ section: Section, _ id: String, to level: Int) { |
| 107 | guard available() else { return } |
| 90 | 108 | var order = section.rows.map { Placed(id: $0.id, level: $0.level) } |
| 91 | 109 | guard let at = order.firstIndex(where: { $0.id == id }), order[at].level != level else { return } |
| 92 | 110 | let was = order[at].level |
| ... | ... | @@ -110,7 +128,7 @@ enum Arranging { |
| 110 | 128 | page id: String, of notebook: Notebook, from: String, to: String, before: String? = nil, level: Int = 1, |
| 111 | 129 | discard: String? = nil |
| 112 | 130 | ) { |
| 113 | | guard let library = notebook.handle else { return } |
| 131 | guard let library = notebook.handle, let release = reserve() else { return } |
| 114 | 132 | let pointer = Int(bitPattern: library) |
| 115 | 133 | let author = Author.name ?? "" |
| 116 | 134 | let (date, time) = titleDate() |
| ... | ... | @@ -130,6 +148,7 @@ enum Arranging { |
| 130 | 148 | time)) |
| 131 | 149 | }) { moved in |
| 132 | 150 | reported = moved |
| 151 | release() |
| 133 | 152 | for section in Section.all where section.notebook === notebook && [from, to].contains(section.tab.path) { |
| 134 | 153 | section.listed() |
| 135 | 154 | } |
| ... | ... | @@ -137,10 +156,104 @@ enum Arranging { |
| 137 | 156 | } |
| 138 | 157 | } |
| 139 | 158 | |
| 159 | /// Moves only the selected pages, retaining their ancestry among the selection. |
| 160 | static func move(pages ids: [String], from source: Section, to tab: Tab) { |
| 161 | guard let release = reserve() else { return } |
| 162 | Section.open(tab, of: source.notebook) { target, _ in |
| 163 | guard let target else { |
| 164 | release() |
| 165 | return failed("Can’t Move Pages") |
| 166 | } |
| 167 | var ancestors: [Int] = [] |
| 168 | var selected: [Placed] = [] |
| 169 | for row in source.rows { |
| 170 | ancestors.removeAll { $0 >= row.level } |
| 171 | guard ids.contains(row.id) else { continue } |
| 172 | selected.append(Placed(id: row.id, level: ancestors.count + 1)) |
| 173 | ancestors.append(row.level) |
| 174 | } |
| 175 | let order = target.rows.map { Placed(id: $0.id, level: $0.level) } + selected |
| 176 | // Hand off the reservation without yielding the main thread. |
| 177 | release() |
| 178 | transfer(selected.map { (page: $0.id, discard: nil) }, |
| 179 | from: source, to: target, order: order) |
| 180 | } |
| 181 | } |
| 182 | |
| 183 | private static func transfer( |
| 184 | _ pages: [(page: String, discard: String?)], from source: Section, to target: Section, order: [Placed], |
| 185 | repairs: [(id: String, expected: Int, restore: Int)] = [] |
| 186 | ) { |
| 187 | guard !pages.isEmpty, let library = source.notebook.handle, let release = reserve() else { return } |
| 188 | let before = source.rows.map { Placed(id: $0.id, level: $0.level) } |
| 189 | let pointer = Int(bitPattern: library) |
| 190 | let sourcePointer = Int(bitPattern: source.handle) |
| 191 | let targetPointer = Int(bitPattern: target.handle) |
| 192 | let author = Author.name ?? "" |
| 193 | let (date, time) = titleDate() |
| 194 | var undo: () -> Void = {} |
| 195 | let undoing = history.isUndoing |
| 196 | let reciprocal = undoing || history.isRedoing |
| 197 | if reciprocal { done("Move Pages") { undo() } } |
| 198 | later({ () -> ([MovedPage], [Placed], [(id: String, expected: Int, restore: Int)], Bool) in |
| 199 | var reported: [MovedPage] = [] |
| 200 | var arrived: [String: String] = [:] |
| 201 | var promotions: [(id: String, expected: Int, restore: Int)] = [] |
| 202 | // Removing a parent can promote its first remaining child. |
| 203 | for page in pages.reversed() { |
| 204 | guard let listed = decode([Row].self, sb_section_pages(OpaquePointer(bitPattern: sourcePointer))) else { break } |
| 205 | let next = listed.first?.id == page.page ? listed.dropFirst().first : nil |
| 206 | guard let moved = decode(MovedPage.self, sb_library_move_page( |
| 207 | OpaquePointer(bitPattern: pointer), source.tab.path, page.page, target.tab.path, |
| 208 | nil, 1, page.discard, author, date, time)) else { break } |
| 209 | reported.append(moved) |
| 210 | arrived[page.page] = moved.page |
| 211 | if let next, next.level > 1, moved.before == next.id { |
| 212 | promotions.removeAll { $0.id == next.id } |
| 213 | promotions.append((id: next.id, expected: 1, restore: next.level)) |
| 214 | } |
| 215 | } |
| 216 | promotions.removeAll { arrived[$0.id] != nil } |
| 217 | let restore = before.map { Placed(id: arrived[$0.id] ?? $0.id, level: $0.level) } |
| 218 | guard !reported.isEmpty else { return (reported, restore, promotions, false) } |
| 219 | let placed = order.map { Placed(id: arrived[$0.id] ?? $0.id, level: $0.level) } |
| 220 | guard sb_section_arrange(OpaquePointer(bitPattern: targetPointer), json(placed), json(Array(arrived.values))) else { |
| 221 | return (reported, restore, promotions, false) |
| 222 | } |
| 223 | if !repairs.isEmpty { |
| 224 | guard let present = decode([Row].self, sb_section_pages(OpaquePointer(bitPattern: targetPointer))) else { |
| 225 | return (reported, restore, promotions, false) |
| 226 | } |
| 227 | let applicable = repairs.filter { repair in present.contains { $0.id == repair.id && $0.level == repair.expected } } |
| 228 | let order = present.map { row in Placed(id: row.id, level: applicable.first { $0.id == row.id }?.restore ?? row.level) } |
| 229 | if !applicable.isEmpty && !sb_section_arrange(OpaquePointer(bitPattern: targetPointer), json(order), json(applicable.map(\.id))) { |
| 230 | return (reported, restore, promotions, false) |
| 231 | } |
| 232 | } |
| 233 | return (reported, restore, promotions, reported.count == pages.count) |
| 234 | }) { reported, restore, promotions, succeeded in |
| 235 | undo = { transfer(reported.map { (page: $0.page, discard: $0.added) }, from: target, to: source, order: restore, repairs: promotions) } |
| 236 | if reciprocal && reported.isEmpty { |
| 237 | // A rejected Undo or Redo must leave its original action retryable. |
| 238 | undo = { done("Move Pages") { transfer(pages, from: source, to: target, order: order, repairs: repairs) } } |
| 239 | if undoing { history.redo() } else { history.undo() } |
| 240 | } |
| 241 | if !reciprocal && !reported.isEmpty { done("Move Pages") { undo() } } |
| 242 | release() |
| 243 | source.listed() |
| 244 | target.listed() |
| 245 | if !succeeded { |
| 246 | failed("Can’t Move All Pages", reported.isEmpty |
| 247 | ? "Check both sections, then try moving the remaining pages again." |
| 248 | : "Check both sections. Use \(undoing ? "Redo" : "Undo") to reverse completed moves, or try moving the remaining pages again.") |
| 249 | } |
| 250 | } |
| 251 | } |
| 252 | |
| 140 | 253 | /// Puts the section or group at `path` of `notebook` into `folder`, ordered as `order` |
| 141 | 254 | /// names its entries once moved; a section shown opens again where it went. |
| 142 | | static func place(_ path: String, of notebook: Notebook, in folder: String, order: [String] = []) { |
| 143 | | guard let library = notebook.handle else { return } |
| 255 | static func place(_ path: String, of notebook: Notebook, in folder: String, order: [String] = [], then: @escaping () -> Void = {}) { |
| 256 | guard let library = notebook.handle, let release = reserve() else { return } |
| 144 | 257 | let home = parentFolder(of: path) |
| 145 | 258 | let name = path.split(separator: "/").last.map(String.init) ?? path |
| 146 | 259 | let placed = home == folder ? path : folder.isEmpty ? name : folder + "/" + name |
| ... | ... | @@ -152,6 +265,7 @@ enum Arranging { |
| 152 | 265 | let paths = json(order) |
| 153 | 266 | later({ take(sb_library_place(OpaquePointer(bitPattern: pointer), path, folder, paths)) != nil }) { moved in |
| 154 | 267 | notebook.reload { |
| 268 | release() |
| 155 | 269 | NotificationCenter.default.post(name: Notebook.listed, object: notebook) |
| 156 | 270 | for (scene, shown) in released { |
| 157 | 271 | let section = moved ? follow(shown.section, from: path, to: placed) : shown.section |
| ... | ... | @@ -159,7 +273,7 @@ enum Arranging { |
| 159 | 273 | scene.open(tab, of: notebook, page: shown.page) |
| 160 | 274 | } |
| 161 | 275 | } |
| 162 | | if !moved { failed("Can’t Move Section") } |
| 276 | if moved { then() } else { failed("Can’t Move Section") } |
| 163 | 277 | } |
| 164 | 278 | } |
| 165 | 279 | } |
| ... | ... | @@ -171,6 +285,7 @@ enum Arranging { |
| 171 | 285 | |
| 172 | 286 | /// Puts `notebook` at `index` among the notebooks of its place in the list. |
| 173 | 287 | static func move(_ notebook: Notebook, to index: Int) { |
| 288 | guard available() else { return } |
| 174 | 289 | guard let from = Notebooks.siblings(of: notebook).firstIndex(where: { $0 === notebook }), from != index else { |
| 175 | 290 | return |
| 176 | 291 | } |
| ... | ... | @@ -181,8 +296,8 @@ enum Arranging { |
| 181 | 296 | } |
| 182 | 297 | |
| 183 | 298 | extension UIResponder { |
| 184 | | @objc func undoArranging() { Arranging.history.undo() } |
| 185 | | @objc func redoArranging() { Arranging.history.redo() } |
| 299 | @objc func undoArranging() { if !Arranging.busy { Arranging.history.undo() } } |
| 300 | @objc func redoArranging() { if !Arranging.busy { Arranging.history.redo() } } |
| 186 | 301 | } |
| 187 | 302 | |
| 188 | 303 | extension Section { |
| ... | ... | @@ -246,7 +361,9 @@ final class PageDrag { |
| 246 | 361 | private func sheet(_ title: String, _ actions: [UIAction], from view: UIView, in controller: UIViewController) { |
| 247 | 362 | let sheet = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) |
| 248 | 363 | for action in actions { |
| 249 | | sheet.addAction(UIAlertAction(title: action.title, style: .default) { _ in action.performWithSender(nil, target: nil) }) |
| 364 | let item = UIAlertAction(title: action.title, style: .default) { _ in action.performWithSender(nil, target: nil) } |
| 365 | item.isEnabled = !action.attributes.contains(.disabled) |
| 366 | sheet.addAction(item) |
| 250 | 367 | } |
| 251 | 368 | sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) |
| 252 | 369 | sheet.popoverPresentationController?.sourceView = view |
| ... | ... | @@ -266,7 +383,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 266 | 383 | } |
| 267 | 384 | |
| 268 | 385 | override var canBecomeFirstResponder: Bool { true } |
| 269 | | override var undoManager: UndoManager? { Arranging.history } |
| 386 | override var undoManager: UndoManager? { !Arranging.busy ? Arranging.history : nil } |
| 270 | 387 | override var keyCommands: [UIKeyCommand]? { Arranging.keyCommands } |
| 271 | 388 | |
| 272 | 389 | /// Undo in the list takes back its moves. |
| ... | ... | @@ -300,7 +417,8 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 300 | 417 | return section.notebook.sections(besides: section.tab.path).map { tab in |
| 301 | 418 | UIAction( |
| 302 | 419 | title: tab.name, subtitle: tab.group.isEmpty ? nil : tab.group, |
| 303 | | image: UIImage(systemName: "rectangle.portrait.fill")?.withTintColor(tab.uiColor, renderingMode: .alwaysOriginal) |
| 420 | image: UIImage(systemName: "rectangle.portrait.fill")?.withTintColor(tab.uiColor, renderingMode: .alwaysOriginal), |
| 421 | attributes: !Arranging.busy ? [] : .disabled |
| 304 | 422 | ) { [weak self] _ in |
| 305 | 423 | Arranging.move(page: item.row.id, of: section.notebook, from: section.tab.path, to: tab.path) |
| 306 | 424 | self?.becomeFirstResponder() |
| ... | ... | @@ -315,11 +433,11 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 315 | 433 | var actions: [UIMenuElement] = [ |
| 316 | 434 | UIAction( |
| 317 | 435 | title: "Make Subpage", image: UIImage(systemName: "increase.indent"), |
| 318 | | attributes: indentable(rows, at) ? [] : .disabled |
| 436 | attributes: !Arranging.busy && indentable(rows, at) ? [] : .disabled |
| 319 | 437 | ) { [weak self] _ in self?.indent(id, by: 1) }, |
| 320 | 438 | UIAction( |
| 321 | 439 | title: "Promote Subpage", image: UIImage(systemName: "decrease.indent"), |
| 322 | | attributes: rows[at].level > 1 ? [] : .disabled |
| 440 | attributes: !Arranging.busy && rows[at].level > 1 ? [] : .disabled |
| 323 | 441 | ) { [weak self] _ in self?.indent(id, by: -1) }, |
| 324 | 442 | ] |
| 325 | 443 | let moves = moveActions(item) |
| ... | ... | @@ -331,7 +449,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 331 | 449 | |
| 332 | 450 | /// VoiceOver's ways to move a page without dragging it. |
| 333 | 451 | func arrangeActions(_ item: Item) -> [UIAccessibilityCustomAction]? { |
| 334 | | guard item.version == nil, let rows = section?.rows, let at = rows.firstIndex(where: { $0.id == item.row.id }) |
| 452 | guard !Arranging.busy, item.version == nil, let rows = section?.rows, let at = rows.firstIndex(where: { $0.id == item.row.id }) |
| 335 | 453 | else { return nil } |
| 336 | 454 | let id = item.row.id |
| 337 | 455 | var actions: [UIAccessibilityCustomAction] = [] |
| ... | ... | @@ -357,7 +475,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 357 | 475 | _ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath |
| 358 | 476 | ) -> UISwipeActionsConfiguration? { |
| 359 | 477 | // A swipe across the list changes section instead. |
| 360 | | guard !Prototype.sectionStrip, let rows = section?.rows, items[indexPath.row].version == nil, |
| 478 | guard !Arranging.busy, !Prototype.sectionStrip, let rows = section?.rows, items[indexPath.row].version == nil, |
| 361 | 479 | let at = rows.firstIndex(where: { $0.id == items[indexPath.row].row.id }) |
| 362 | 480 | else { return nil } |
| 363 | 481 | let id = rows[at].id |
| ... | ... | @@ -384,7 +502,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 384 | 502 | } |
| 385 | 503 | |
| 386 | 504 | override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { |
| 387 | | items[indexPath.row].version == nil |
| 505 | !Arranging.busy && items[indexPath.row].version == nil |
| 388 | 506 | } |
| 389 | 507 | |
| 390 | 508 | override func tableView(_ tableView: UITableView, moveRowAt from: IndexPath, to: IndexPath) { |
| ... | ... | @@ -404,7 +522,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 404 | 522 | _ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath |
| 405 | 523 | ) -> [UIDragItem] { |
| 406 | 524 | let item = items[indexPath.row] |
| 407 | | guard let section, item.version == nil else { return [] } |
| 525 | guard !Arranging.busy, let section, item.version == nil else { return [] } |
| 408 | 526 | let drag = UIDragItem(itemProvider: NSItemProvider()) |
| 409 | 527 | drag.localObject = PageDrag(section: section, id: item.row.id) |
| 410 | 528 | return [drag] |
| ... | ... | @@ -431,7 +549,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 431 | 549 | _ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath: IndexPath? |
| 432 | 550 | ) -> UITableViewDropProposal { |
| 433 | 551 | let page = session.localDragSession?.items.first?.localObject as? PageDrag |
| 434 | | guard let page, page.section === section else { |
| 552 | guard !Arranging.busy, let page, page.section === section else { |
| 435 | 553 | return UITableViewDropProposal(operation: .forbidden) |
| 436 | 554 | } |
| 437 | 555 | return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) |
| ... | ... | @@ -457,7 +575,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 457 | 575 | } |
| 458 | 576 | // Rows dragged between rows, and by Edit's reorder handles, move through the data source. |
| 459 | 577 | dataSource.reorderingHandlers.canReorderItem = { [weak self] item in |
| 460 | | self?.movable(item) == true && (self?.collectionView.indexPathsForSelectedItems?.count ?? 0) < 2 |
| 578 | !Arranging.busy && self?.movable(item) == true && (self?.collectionView.indexPathsForSelectedItems?.count ?? 0) < 2 |
| 461 | 579 | } |
| 462 | 580 | // The data source is still applying the move when this runs. |
| 463 | 581 | dataSource.reorderingHandlers.didReorder = { [weak self] transaction in |
| ... | ... | @@ -466,7 +584,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 466 | 584 | } |
| 467 | 585 | |
| 468 | 586 | override var canBecomeFirstResponder: Bool { true } |
| 469 | | override var undoManager: UndoManager? { Arranging.history } |
| 587 | override var undoManager: UndoManager? { !Arranging.busy ? Arranging.history : nil } |
| 470 | 588 | override var keyCommands: [UIKeyCommand]? { Arranging.keyCommands } |
| 471 | 589 | |
| 472 | 590 | /// Undo in the list takes back its moves. |
| ... | ... | @@ -512,7 +630,8 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 512 | 630 | notebook.destinations(of: path).map { folder in |
| 513 | 631 | UIAction( |
| 514 | 632 | title: folder.isEmpty ? notebook.name : folder.split(separator: "/").joined(separator: " › "), |
| 515 | | image: UIImage(systemName: folder.isEmpty ? "book.closed" : "folder") |
| 633 | image: UIImage(systemName: folder.isEmpty ? "book.closed" : "folder"), |
| 634 | attributes: !Arranging.busy ? [] : .disabled |
| 516 | 635 | ) { [weak self] _ in |
| 517 | 636 | Arranging.place(path, of: notebook, in: folder) |
| 518 | 637 | self?.becomeFirstResponder() |
| ... | ... | @@ -528,12 +647,12 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 528 | 647 | guard let at = siblings.firstIndex(where: { $0 === notebook }), siblings.count > 1 else { return [] } |
| 529 | 648 | return [ |
| 530 | 649 | UIMenu(options: .displayInline, children: [ |
| 531 | | UIAction(title: "Move Up", image: UIImage(systemName: "arrow.up"), attributes: at > 0 ? [] : .disabled) { |
| 650 | UIAction(title: "Move Up", image: UIImage(systemName: "arrow.up"), attributes: !Arranging.busy && at > 0 ? [] : .disabled) { |
| 532 | 651 | _ in Arranging.move(notebook, to: at - 1) |
| 533 | 652 | }, |
| 534 | 653 | UIAction( |
| 535 | 654 | title: "Move Down", image: UIImage(systemName: "arrow.down"), |
| 536 | | attributes: at < siblings.count - 1 ? [] : .disabled |
| 655 | attributes: !Arranging.busy && at < siblings.count - 1 ? [] : .disabled |
| 537 | 656 | ) { _ in Arranging.move(notebook, to: at + 1) }, |
| 538 | 657 | ]) |
| 539 | 658 | ] |
| ... | ... | @@ -603,7 +722,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 603 | 722 | func collectionView( |
| 604 | 723 | _ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath |
| 605 | 724 | ) -> [UIDragItem] { |
| 606 | | guard let item = dataSource.itemIdentifier(for: indexPath), movable(item) else { return [] } |
| 725 | guard !Arranging.busy, let item = dataSource.itemIdentifier(for: indexPath), movable(item) else { return [] } |
| 607 | 726 | let drag = UIDragItem(itemProvider: NSItemProvider()) |
| 608 | 727 | drag.localObject = item |
| 609 | 728 | return [drag] |
| ... | ... | @@ -640,6 +759,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 640 | 759 | _ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, |
| 641 | 760 | withDestinationIndexPath destinationIndexPath: IndexPath? |
| 642 | 761 | ) -> UICollectionViewDropProposal { |
| 762 | guard !Arranging.busy else { return UICollectionViewDropProposal(operation: .forbidden) } |
| 643 | 763 | let dragged = session.localDragSession?.items.first?.localObject |
| 644 | 764 | if let page = dragged as? PageDrag { |
| 645 | 765 | guard section(for: page, under: session) != nil else { return UICollectionViewDropProposal(operation: .forbidden) } |