| ... | @@ -29,6 +29,7 @@ private func json(_ value: some Encodable) -> String { | ... | @@ -29,6 +29,7 @@ private func json(_ value: some Encodable) -> String { |
| 29 | /// either list. | 29 | /// either list. |
| 30 | enum Arranging { | 30 | enum Arranging { |
| 31 | static let history = UndoManager() | 31 | static let history = UndoManager() |
| | 32 | private(set) static var busy = false |
| 32 | private static let owner = NSObject() | 33 | private static let owner = NSObject() |
| 33 | | 34 | |
| 34 | /// ⌘Z and ⇧⌘Z in a list, which, unlike a text view, has none of its own. | 35 | /// ⌘Z and ⇧⌘Z in a list, which, unlike a text view, has none of its own. |
| ... | @@ -55,11 +56,26 @@ enum Arranging { | ... | @@ -55,11 +56,26 @@ enum Arranging { |
| 55 | } | 56 | } |
| 56 | private static let queue = DispatchQueue(label: "Arranging") | 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 | let window = UIApplication.shared.connectedScenes.lazy.compactMap { ($0 as? UIWindowScene)?.keyWindow }.first | 60 | let window = UIApplication.shared.connectedScenes.lazy.compactMap { ($0 as? UIWindowScene)?.keyWindow }.first |
| 60 | var top = window?.rootViewController | 61 | var top = window?.rootViewController |
| 61 | while let presented = top?.presentedViewController { top = presented } | 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 | /// The pages `rows` lists with page `id` moved to `index` of them, at its level or under | 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,6 +92,7 @@ enum Arranging { |
| 76 | | 92 | |
| 77 | /// Puts the pages of `section` in `order`, page `id` the one moved, as one edit. | 93 | /// Puts the pages of `section` in `order`, page `id` the one moved, as one edit. |
| 78 | static func arrange(_ section: Section, _ order: [Placed], moving id: String) { | 94 | static func arrange(_ section: Section, _ order: [Placed], moving id: String) { |
| | 95 | guard available() else { return } |
| 79 | let before = section.rows.map { Placed(id: $0.id, level: $0.level) } | 96 | let before = section.rows.map { Placed(id: $0.id, level: $0.level) } |
| 80 | guard order != before, section.arrange(order, moving: id) else { return } | 97 | guard order != before, section.arrange(order, moving: id) else { return } |
| 81 | let (notebook, path) = (section.notebook, section.tab.path) | 98 | let (notebook, path) = (section.notebook, section.tab.path) |
| ... | @@ -87,6 +104,7 @@ enum Arranging { | ... | @@ -87,6 +104,7 @@ enum Arranging { |
| 87 | /// Gives page `id` of `section` `level` where it stands, as Make Subpage and Promote | 104 | /// Gives page `id` of `section` `level` where it stands, as Make Subpage and Promote |
| 88 | /// Subpage do. | 105 | /// Subpage do. |
| 89 | static func indent(_ section: Section, _ id: String, to level: Int) { | 106 | static func indent(_ section: Section, _ id: String, to level: Int) { |
| | 107 | guard available() else { return } |
| 90 | var order = section.rows.map { Placed(id: $0.id, level: $0.level) } | 108 | var order = section.rows.map { Placed(id: $0.id, level: $0.level) } |
| 91 | guard let at = order.firstIndex(where: { $0.id == id }), order[at].level != level else { return } | 109 | guard let at = order.firstIndex(where: { $0.id == id }), order[at].level != level else { return } |
| 92 | let was = order[at].level | 110 | let was = order[at].level |
| ... | @@ -110,7 +128,7 @@ enum Arranging { | ... | @@ -110,7 +128,7 @@ enum Arranging { |
| 110 | page id: String, of notebook: Notebook, from: String, to: String, before: String? = nil, level: Int = 1, | 128 | page id: String, of notebook: Notebook, from: String, to: String, before: String? = nil, level: Int = 1, |
| 111 | discard: String? = nil | 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 | let pointer = Int(bitPattern: library) | 132 | let pointer = Int(bitPattern: library) |
| 115 | let author = Author.name ?? "" | 133 | let author = Author.name ?? "" |
| 116 | let (date, time) = titleDate() | 134 | let (date, time) = titleDate() |
| ... | @@ -130,6 +148,7 @@ enum Arranging { | ... | @@ -130,6 +148,7 @@ enum Arranging { |
| 130 | time)) | 148 | time)) |
| 131 | }) { moved in | 149 | }) { moved in |
| 132 | reported = moved | 150 | reported = moved |
| | 151 | release() |
| 133 | for section in Section.all where section.notebook === notebook && [from, to].contains(section.tab.path) { | 152 | for section in Section.all where section.notebook === notebook && [from, to].contains(section.tab.path) { |
| 134 | section.listed() | 153 | section.listed() |
| 135 | } | 154 | } |
| ... | @@ -137,10 +156,104 @@ enum Arranging { | ... | @@ -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 | /// Puts the section or group at `path` of `notebook` into `folder`, ordered as `order` | 253 | /// Puts the section or group at `path` of `notebook` into `folder`, ordered as `order` |
| 141 | /// names its entries once moved; a section shown opens again where it went. | 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] = []) { | 255 | static func place(_ path: String, of notebook: Notebook, in folder: String, order: [String] = [], then: @escaping () -> Void = {}) { |
| 143 | guard let library = notebook.handle else { return } | 256 | guard let library = notebook.handle, let release = reserve() else { return } |
| 144 | let home = parentFolder(of: path) | 257 | let home = parentFolder(of: path) |
| 145 | let name = path.split(separator: "/").last.map(String.init) ?? path | 258 | let name = path.split(separator: "/").last.map(String.init) ?? path |
| 146 | let placed = home == folder ? path : folder.isEmpty ? name : folder + "/" + name | 259 | let placed = home == folder ? path : folder.isEmpty ? name : folder + "/" + name |
| ... | @@ -152,6 +265,7 @@ enum Arranging { | ... | @@ -152,6 +265,7 @@ enum Arranging { |
| 152 | let paths = json(order) | 265 | let paths = json(order) |
| 153 | later({ take(sb_library_place(OpaquePointer(bitPattern: pointer), path, folder, paths)) != nil }) { moved in | 266 | later({ take(sb_library_place(OpaquePointer(bitPattern: pointer), path, folder, paths)) != nil }) { moved in |
| 154 | notebook.reload { | 267 | notebook.reload { |
| | 268 | release() |
| 155 | NotificationCenter.default.post(name: Notebook.listed, object: notebook) | 269 | NotificationCenter.default.post(name: Notebook.listed, object: notebook) |
| 156 | for (scene, shown) in released { | 270 | for (scene, shown) in released { |
| 157 | let section = moved ? follow(shown.section, from: path, to: placed) : shown.section | 271 | let section = moved ? follow(shown.section, from: path, to: placed) : shown.section |
| ... | @@ -159,7 +273,7 @@ enum Arranging { | ... | @@ -159,7 +273,7 @@ enum Arranging { |
| 159 | scene.open(tab, of: notebook, page: shown.page) | 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,6 +285,7 @@ enum Arranging { |
| 171 | | 285 | |
| 172 | /// Puts `notebook` at `index` among the notebooks of its place in the list. | 286 | /// Puts `notebook` at `index` among the notebooks of its place in the list. |
| 173 | static func move(_ notebook: Notebook, to index: Int) { | 287 | static func move(_ notebook: Notebook, to index: Int) { |
| | 288 | guard available() else { return } |
| 174 | guard let from = Notebooks.siblings(of: notebook).firstIndex(where: { $0 === notebook }), from != index else { | 289 | guard let from = Notebooks.siblings(of: notebook).firstIndex(where: { $0 === notebook }), from != index else { |
| 175 | return | 290 | return |
| 176 | } | 291 | } |
| ... | @@ -181,8 +296,8 @@ enum Arranging { | ... | @@ -181,8 +296,8 @@ enum Arranging { |
| 181 | } | 296 | } |
| 182 | | 297 | |
| 183 | extension UIResponder { | 298 | extension UIResponder { |
| 184 | @objc func undoArranging() { Arranging.history.undo() } | 299 | @objc func undoArranging() { if !Arranging.busy { Arranging.history.undo() } } |
| 185 | @objc func redoArranging() { Arranging.history.redo() } | 300 | @objc func redoArranging() { if !Arranging.busy { Arranging.history.redo() } } |
| 186 | } | 301 | } |
| 187 | | 302 | |
| 188 | extension Section { | 303 | extension Section { |
| ... | @@ -246,7 +361,9 @@ final class PageDrag { | ... | @@ -246,7 +361,9 @@ final class PageDrag { |
| 246 | private func sheet(_ title: String, _ actions: [UIAction], from view: UIView, in controller: UIViewController) { | 361 | private func sheet(_ title: String, _ actions: [UIAction], from view: UIView, in controller: UIViewController) { |
| 247 | let sheet = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) | 362 | let sheet = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) |
| 248 | for action in actions { | 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 | sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) | 368 | sheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) |
| 252 | sheet.popoverPresentationController?.sourceView = view | 369 | sheet.popoverPresentationController?.sourceView = view |
| ... | @@ -266,7 +383,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -266,7 +383,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 266 | } | 383 | } |
| 267 | | 384 | |
| 268 | override var canBecomeFirstResponder: Bool { true } | 385 | override var canBecomeFirstResponder: Bool { true } |
| 269 | override var undoManager: UndoManager? { Arranging.history } | 386 | override var undoManager: UndoManager? { !Arranging.busy ? Arranging.history : nil } |
| 270 | override var keyCommands: [UIKeyCommand]? { Arranging.keyCommands } | 387 | override var keyCommands: [UIKeyCommand]? { Arranging.keyCommands } |
| 271 | | 388 | |
| 272 | /// Undo in the list takes back its moves. | 389 | /// Undo in the list takes back its moves. |
| ... | @@ -300,7 +417,8 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -300,7 +417,8 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 300 | return section.notebook.sections(besides: section.tab.path).map { tab in | 417 | return section.notebook.sections(besides: section.tab.path).map { tab in |
| 301 | UIAction( | 418 | UIAction( |
| 302 | title: tab.name, subtitle: tab.group.isEmpty ? nil : tab.group, | 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 | ) { [weak self] _ in | 422 | ) { [weak self] _ in |
| 305 | Arranging.move(page: item.row.id, of: section.notebook, from: section.tab.path, to: tab.path) | 423 | Arranging.move(page: item.row.id, of: section.notebook, from: section.tab.path, to: tab.path) |
| 306 | self?.becomeFirstResponder() | 424 | self?.becomeFirstResponder() |
| ... | @@ -315,11 +433,11 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -315,11 +433,11 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 315 | var actions: [UIMenuElement] = [ | 433 | var actions: [UIMenuElement] = [ |
| 316 | UIAction( | 434 | UIAction( |
| 317 | title: "Make Subpage", image: UIImage(systemName: "increase.indent"), | 435 | title: "Make Subpage", image: UIImage(systemName: "increase.indent"), |
| 318 | attributes: indentable(rows, at) ? [] : .disabled | 436 | attributes: !Arranging.busy && indentable(rows, at) ? [] : .disabled |
| 319 | ) { [weak self] _ in self?.indent(id, by: 1) }, | 437 | ) { [weak self] _ in self?.indent(id, by: 1) }, |
| 320 | UIAction( | 438 | UIAction( |
| 321 | title: "Promote Subpage", image: UIImage(systemName: "decrease.indent"), | 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 | ) { [weak self] _ in self?.indent(id, by: -1) }, | 441 | ) { [weak self] _ in self?.indent(id, by: -1) }, |
| 324 | ] | 442 | ] |
| 325 | let moves = moveActions(item) | 443 | let moves = moveActions(item) |
| ... | @@ -331,7 +449,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -331,7 +449,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 331 | | 449 | |
| 332 | /// VoiceOver's ways to move a page without dragging it. | 450 | /// VoiceOver's ways to move a page without dragging it. |
| 333 | func arrangeActions(_ item: Item) -> [UIAccessibilityCustomAction]? { | 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 | else { return nil } | 453 | else { return nil } |
| 336 | let id = item.row.id | 454 | let id = item.row.id |
| 337 | var actions: [UIAccessibilityCustomAction] = [] | 455 | var actions: [UIAccessibilityCustomAction] = [] |
| ... | @@ -357,7 +475,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -357,7 +475,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 357 | _ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath | 475 | _ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath |
| 358 | ) -> UISwipeActionsConfiguration? { | 476 | ) -> UISwipeActionsConfiguration? { |
| 359 | // A swipe across the list changes section instead. | 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 | let at = rows.firstIndex(where: { $0.id == items[indexPath.row].row.id }) | 479 | let at = rows.firstIndex(where: { $0.id == items[indexPath.row].row.id }) |
| 362 | else { return nil } | 480 | else { return nil } |
| 363 | let id = rows[at].id | 481 | let id = rows[at].id |
| ... | @@ -384,7 +502,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -384,7 +502,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 384 | } | 502 | } |
| 385 | | 503 | |
| 386 | override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { | 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 | override func tableView(_ tableView: UITableView, moveRowAt from: IndexPath, to: IndexPath) { | 508 | override func tableView(_ tableView: UITableView, moveRowAt from: IndexPath, to: IndexPath) { |
| ... | @@ -404,7 +522,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -404,7 +522,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 404 | _ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath | 522 | _ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath |
| 405 | ) -> [UIDragItem] { | 523 | ) -> [UIDragItem] { |
| 406 | let item = items[indexPath.row] | 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 | let drag = UIDragItem(itemProvider: NSItemProvider()) | 526 | let drag = UIDragItem(itemProvider: NSItemProvider()) |
| 409 | drag.localObject = PageDrag(section: section, id: item.row.id) | 527 | drag.localObject = PageDrag(section: section, id: item.row.id) |
| 410 | return [drag] | 528 | return [drag] |
| ... | @@ -431,7 +549,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate | ... | @@ -431,7 +549,7 @@ extension PagesViewController: UITableViewDragDelegate, UITableViewDropDelegate |
| 431 | _ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath: IndexPath? | 549 | _ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath: IndexPath? |
| 432 | ) -> UITableViewDropProposal { | 550 | ) -> UITableViewDropProposal { |
| 433 | let page = session.localDragSession?.items.first?.localObject as? PageDrag | 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 | return UITableViewDropProposal(operation: .forbidden) | 553 | return UITableViewDropProposal(operation: .forbidden) |
| 436 | } | 554 | } |
| 437 | return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) | 555 | return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) |
| ... | @@ -457,7 +575,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie | ... | @@ -457,7 +575,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 457 | } | 575 | } |
| 458 | // Rows dragged between rows, and by Edit's reorder handles, move through the data source. | 576 | // Rows dragged between rows, and by Edit's reorder handles, move through the data source. |
| 459 | dataSource.reorderingHandlers.canReorderItem = { [weak self] item in | 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 | // The data source is still applying the move when this runs. | 580 | // The data source is still applying the move when this runs. |
| 463 | dataSource.reorderingHandlers.didReorder = { [weak self] transaction in | 581 | dataSource.reorderingHandlers.didReorder = { [weak self] transaction in |
| ... | @@ -466,7 +584,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie | ... | @@ -466,7 +584,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 466 | } | 584 | } |
| 467 | | 585 | |
| 468 | override var canBecomeFirstResponder: Bool { true } | 586 | override var canBecomeFirstResponder: Bool { true } |
| 469 | override var undoManager: UndoManager? { Arranging.history } | 587 | override var undoManager: UndoManager? { !Arranging.busy ? Arranging.history : nil } |
| 470 | override var keyCommands: [UIKeyCommand]? { Arranging.keyCommands } | 588 | override var keyCommands: [UIKeyCommand]? { Arranging.keyCommands } |
| 471 | | 589 | |
| 472 | /// Undo in the list takes back its moves. | 590 | /// Undo in the list takes back its moves. |
| ... | @@ -512,7 +630,8 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie | ... | @@ -512,7 +630,8 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 512 | notebook.destinations(of: path).map { folder in | 630 | notebook.destinations(of: path).map { folder in |
| 513 | UIAction( | 631 | UIAction( |
| 514 | title: folder.isEmpty ? notebook.name : folder.split(separator: "/").joined(separator: " › "), | 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 | ) { [weak self] _ in | 635 | ) { [weak self] _ in |
| 517 | Arranging.place(path, of: notebook, in: folder) | 636 | Arranging.place(path, of: notebook, in: folder) |
| 518 | self?.becomeFirstResponder() | 637 | self?.becomeFirstResponder() |
| ... | @@ -528,12 +647,12 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie | ... | @@ -528,12 +647,12 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 528 | guard let at = siblings.firstIndex(where: { $0 === notebook }), siblings.count > 1 else { return [] } | 647 | guard let at = siblings.firstIndex(where: { $0 === notebook }), siblings.count > 1 else { return [] } |
| 529 | return [ | 648 | return [ |
| 530 | UIMenu(options: .displayInline, children: [ | 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 | _ in Arranging.move(notebook, to: at - 1) | 651 | _ in Arranging.move(notebook, to: at - 1) |
| 533 | }, | 652 | }, |
| 534 | UIAction( | 653 | UIAction( |
| 535 | title: "Move Down", image: UIImage(systemName: "arrow.down"), | 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 | ) { _ in Arranging.move(notebook, to: at + 1) }, | 656 | ) { _ in Arranging.move(notebook, to: at + 1) }, |
| 538 | ]) | 657 | ]) |
| 539 | ] | 658 | ] |
| ... | @@ -603,7 +722,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie | ... | @@ -603,7 +722,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 603 | func collectionView( | 722 | func collectionView( |
| 604 | _ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath | 723 | _ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath |
| 605 | ) -> [UIDragItem] { | 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 | let drag = UIDragItem(itemProvider: NSItemProvider()) | 726 | let drag = UIDragItem(itemProvider: NSItemProvider()) |
| 608 | drag.localObject = item | 727 | drag.localObject = item |
| 609 | return [drag] | 728 | return [drag] |
| ... | @@ -640,6 +759,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie | ... | @@ -640,6 +759,7 @@ extension NotebooksViewController: UICollectionViewDragDelegate, UICollectionVie |
| 640 | _ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, | 759 | _ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, |
| 641 | withDestinationIndexPath destinationIndexPath: IndexPath? | 760 | withDestinationIndexPath destinationIndexPath: IndexPath? |
| 642 | ) -> UICollectionViewDropProposal { | 761 | ) -> UICollectionViewDropProposal { |
| | 762 | guard !Arranging.busy else { return UICollectionViewDropProposal(operation: .forbidden) } |
| 643 | let dragged = session.localDragSession?.items.first?.localObject | 763 | let dragged = session.localDragSession?.items.first?.localObject |
| 644 | if let page = dragged as? PageDrag { | 764 | if let page = dragged as? PageDrag { |
| 645 | guard section(for: page, under: session) != nil else { return UICollectionViewDropProposal(operation: .forbidden) } | 765 | guard section(for: page, under: session) != nil else { return UICollectionViewDropProposal(operation: .forbidden) } |