diff --git a/Zotero.xcodeproj/project.pbxproj b/Zotero.xcodeproj/project.pbxproj index c75f9ad4e..74540f442 100644 --- a/Zotero.xcodeproj/project.pbxproj +++ b/Zotero.xcodeproj/project.pbxproj @@ -889,6 +889,7 @@ B3A17D1927FC33B800322CAD /* LowPowerModeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A17D1827FC33B800322CAD /* LowPowerModeController.swift */; }; B3A27ACD25BEE91A00DE0BB2 /* TranslationWebViewHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A27ACC25BEE91A00DE0BB2 /* TranslationWebViewHandler.swift */; }; B3A27AD125BEE93400DE0BB2 /* FilenameFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A27AD025BEE93400DE0BB2 /* FilenameFormatter.swift */; }; + B3A297B72D366B23008AD19D /* SegmentedControlCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A297B62D366B23008AD19D /* SegmentedControlCell.swift */; }; B3A2AECC26552248004BF3A4 /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A2AECB26552248004BF3A4 /* SettingsCoordinator.swift */; }; B3A2AECE26553DD8004BF3A4 /* NavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A2AECD26553DD8004BF3A4 /* NavigationViewController.swift */; }; B3A2AEDC2656511D004BF3A4 /* StylesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A2AEDB2656511D004BF3A4 /* StylesRequest.swift */; }; @@ -1913,6 +1914,7 @@ B3A17D1827FC33B800322CAD /* LowPowerModeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LowPowerModeController.swift; sourceTree = ""; }; B3A27ACC25BEE91A00DE0BB2 /* TranslationWebViewHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TranslationWebViewHandler.swift; sourceTree = ""; }; B3A27AD025BEE93400DE0BB2 /* FilenameFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilenameFormatter.swift; sourceTree = ""; }; + B3A297B62D366B23008AD19D /* SegmentedControlCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedControlCell.swift; sourceTree = ""; }; B3A2AECB26552248004BF3A4 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; B3A2AECD26553DD8004BF3A4 /* NavigationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavigationViewController.swift; sourceTree = ""; }; B3A2AEDB2656511D004BF3A4 /* StylesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StylesRequest.swift; sourceTree = ""; }; @@ -3137,6 +3139,7 @@ B398D6BF2A77F9C60049A296 /* FontSizeView.swift */, B34A4B7126E65FC200B3E993 /* LineWidthCell.swift */, B34A4B6F26E63C9900B3E993 /* LineWidthView.swift */, + B3A297B62D366B23008AD19D /* SegmentedControlCell.swift */, B34DF1BB2576956F0019CCD1 /* SwitchCell.swift */, B34DF1BC2576956F0019CCD1 /* SwitchCell.xib */, B398142C257A649D002C755C /* TextContentEditCell.swift */, @@ -4972,6 +4975,7 @@ B30566AF23FC051F003304F2 /* LibraryResponse.swift in Sources */, B33F47732CA1656E00278240 /* BaseItemsActionHandler.swift in Sources */, B3868537270D90640068A022 /* RawDataEncoding.swift in Sources */, + B3A297B72D366B23008AD19D /* SegmentedControlCell.swift in Sources */, B37C5B6D26453C58009A37E5 /* NoteEditorAction.swift in Sources */, B30566C223FC051F003304F2 /* LibraryData.swift in Sources */, B391480726D9093E0016B7B2 /* UIPasteboard+Extensions.swift in Sources */, diff --git a/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift b/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift index aa6c369f8..24cb247af 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/AnnotationPopoverCoordinator.swift @@ -74,7 +74,7 @@ extension AnnotationPopoverCoordinator: AnnotationPopoverAnnotationCoordinatorDe func createShareAnnotationMenu(sender: UIButton) -> UIMenu? { return (parentCoordinator as? PDFCoordinator)?.createShareAnnotationMenuForSelectedAnnotation(sender: sender) } - + func showEdit(state: AnnotationPopoverState, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction) { let data = AnnotationEditState.Data( type: state.type, @@ -89,7 +89,12 @@ extension AnnotationPopoverCoordinator: AnnotationPopoverAnnotationCoordinatorDe let state = AnnotationEditState(data: data) let handler = AnnotationEditActionHandler() let viewModel = ViewModel(initialState: state, handler: handler) - let controller = AnnotationEditViewController(viewModel: viewModel, includeColorPicker: false, includeFontPicker: false, saveAction: saveAction, deleteAction: deleteAction) + let controller = AnnotationEditViewController( + viewModel: viewModel, + properties: AnnotationEditViewController.PropertyRow.from(type: state.type, isAdditionalSettings: true), + saveAction: saveAction, + deleteAction: deleteAction + ) controller.coordinatorDelegate = self self.navigationController?.pushViewController(controller, animated: true) } diff --git a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditAction.swift b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditAction.swift index 858ccf2e8..d653c1d43 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditAction.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditAction.swift @@ -14,4 +14,5 @@ enum AnnotationEditAction { case setPageLabel(String, Bool) case setHighlight(NSAttributedString) case setFontSize(CGFloat) + case setAnnotationType(AnnotationType) } diff --git a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift index 1ed865d36..317515be7 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationEditState.swift @@ -27,11 +27,12 @@ struct AnnotationEditState: ViewModelState { static let color = Changes(rawValue: 1 << 0) static let pageLabel = Changes(rawValue: 1 << 1) + static let type = Changes(rawValue: 1 << 2) } - let type: AnnotationType let isEditable: Bool + var type: AnnotationType var color: String var lineWidth: CGFloat var pageLabel: String diff --git a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverAction.swift b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverAction.swift index e8e94946f..1cfda8c20 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverAction.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverAction.swift @@ -15,5 +15,5 @@ enum AnnotationPopoverAction { case setTags([Tag]) case setComment(NSAttributedString) case delete - case setProperties(pageLabel: String, updateSubsequentLabels: Bool, highlightText: NSAttributedString) + case setProperties(type: AnnotationType, pageLabel: String, updateSubsequentLabels: Bool, highlightText: NSAttributedString) } diff --git a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverState.swift b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverState.swift index b29e9f152..9ecd99693 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverState.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Models/AnnotationPopoverState.swift @@ -36,14 +36,15 @@ struct AnnotationPopoverState: ViewModelState { static let highlight = Changes(rawValue: 1 << 4) static let tags = Changes(rawValue: 1 << 5) static let deletion = Changes(rawValue: 1 << 6) + static let type = Changes(rawValue: 1 << 7) } let libraryId: LibraryIdentifier - let type: AnnotationType let isEditable: Bool let author: String let showsDeleteButton: Bool + var type: AnnotationType var comment: NSAttributedString var color: String var lineWidth: CGFloat diff --git a/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationEditActionHandler.swift b/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationEditActionHandler.swift index ef76ccddd..517f5bf9a 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationEditActionHandler.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationEditActionHandler.swift @@ -15,32 +15,38 @@ struct AnnotationEditActionHandler: ViewModelActionHandler { func process(action: AnnotationEditAction, in viewModel: ViewModel) { switch action { case .setColor(let hexString): - self.update(viewModel: viewModel) { state in + update(viewModel: viewModel) { state in state.color = hexString state.changes = .color } case .setLineWidth(let width): - self.update(viewModel: viewModel) { state in + update(viewModel: viewModel) { state in state.lineWidth = width } case .setPageLabel(let label, let updateSubsequentPages): - self.update(viewModel: viewModel) { state in + update(viewModel: viewModel) { state in state.pageLabel = label state.updateSubsequentLabels = updateSubsequentPages state.changes = .pageLabel } case .setHighlight(let text): - self.update(viewModel: viewModel) { state in + update(viewModel: viewModel) { state in state.highlightText = text } case .setFontSize(let size): - self.update(viewModel: viewModel) { state in + update(viewModel: viewModel) { state in state.fontSize = size } + + case .setAnnotationType(let type): + update(viewModel: viewModel) { state in + state.type = type + state.changes = .type + } } } } diff --git a/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationPopoverActionHandler.swift b/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationPopoverActionHandler.swift index 2f853d372..3508be56e 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationPopoverActionHandler.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/ViewModels/AnnotationPopoverActionHandler.swift @@ -50,12 +50,17 @@ struct AnnotationPopoverActionHandler: ViewModelActionHandler { state.changes = .deletion } - case .setProperties(pageLabel: let pageLabel, updateSubsequentLabels: let updateSubsequentLabels, highlightText: let highlightText): + case .setProperties(let type, let pageLabel, let updateSubsequentLabels, let highlightText): update(viewModel: viewModel) { state in + if state.type != type { + state.type = type + state.changes.insert(.type) + } + if state.pageLabel != pageLabel { state.pageLabel = pageLabel state.updateSubsequentLabels = updateSubsequentLabels - state.changes = .pageLabel + state.changes.insert(.pageLabel) } if state.highlightText != highlightText { diff --git a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift index 5b1d5f162..57052aaea 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationEditViewController.swift @@ -15,15 +15,54 @@ typealias AnnotationEditDeleteAction = () -> Void final class AnnotationEditViewController: UIViewController { private enum Section { - case properties, pageLabel, actions, textContent, fontSize + case properties, pageLabel, actions, textContent - func cellId(index: Int) -> String { + func cellId(index: Int, propertyRows: [PropertyRow]) -> String { switch self { - case .properties: return index == 0 ? "ColorPickerCell" : "LineWidthCell" - case .actions: return "ActionCell" - case .pageLabel: return "PageLabelCell" - case .textContent: return "TextContentCell" - case .fontSize: return "FontSizeCell" + case .properties: + guard index < propertyRows.count else { return "EmptyCell" } + switch propertyRows[index] { + case .colorPicker: + return "ColorPickerCell" + + case .lineWidth: + return "LineWidthCell" + + case .highlightUnderlineSwitch: + return "HighlightUnderlineSwitchCell" + + case .fontSize: + return "FontSizeCell" + } + + case .actions: + return "ActionCell" + + case .pageLabel: + return "PageLabelCell" + + case .textContent: + return "TextContentCell" + } + } + } + + enum PropertyRow { + case colorPicker, lineWidth, highlightUnderlineSwitch, fontSize + + static func from(type: AnnotationType, isAdditionalSettings: Bool) -> [PropertyRow] { + switch type { + case .freeText: + return isAdditionalSettings ? [] : [.fontSize, .colorPicker] + + case .ink: + return isAdditionalSettings ? [] : [.colorPicker, .lineWidth] + + case .highlight, .underline: + return isAdditionalSettings ? [.highlightUnderlineSwitch] : [.colorPicker, .highlightUnderlineSwitch] + + case .image, .note: + return isAdditionalSettings ? [] : [.colorPicker] } } } @@ -32,21 +71,24 @@ final class AnnotationEditViewController: UIViewController { private let viewModel: ViewModel private let sections: [Section] + private let propertyRows: [PropertyRow] private let saveAction: AnnotationEditSaveAction private let deleteAction: AnnotationEditDeleteAction private let disposeBag: DisposeBag weak var coordinatorDelegate: AnnotationEditCoordinatorDelegate? - init(viewModel: ViewModel, includeColorPicker: Bool, includeFontPicker: Bool, saveAction: @escaping AnnotationEditSaveAction, deleteAction: @escaping AnnotationEditDeleteAction) { + init( + viewModel: ViewModel, + properties: [PropertyRow], + saveAction: @escaping AnnotationEditSaveAction, + deleteAction: @escaping AnnotationEditDeleteAction + ) { var sections: [Section] = [.pageLabel, .actions] if viewModel.state.isEditable { - if includeColorPicker { + if !properties.isEmpty { sections.insert(.properties, at: 0) } - if includeFontPicker { - sections.insert(.fontSize, at: 0) - } if viewModel.state.type == .highlight || viewModel.state.type == .underline { sections.insert(.textContent, at: 0) } @@ -56,7 +98,8 @@ final class AnnotationEditViewController: UIViewController { self.sections = sections self.saveAction = saveAction self.deleteAction = deleteAction - self.disposeBag = DisposeBag() + propertyRows = properties + disposeBag = DisposeBag() super.init(nibName: "AnnotationEditViewController", bundle: nil) } @@ -67,68 +110,93 @@ final class AnnotationEditViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - self.title = L10n.Pdf.AnnotationPopover.title - self.view.backgroundColor = Asset.Colors.annotationPopoverBackground.color - self.setupTableView() - self.setupNavigationBar() + title = L10n.Pdf.AnnotationPopover.title + view.backgroundColor = Asset.Colors.annotationPopoverBackground.color + setupTableView() + setupNavigationBar() + + viewModel.stateObservable + .subscribe(onNext: { [weak self] state in + self?.update(to: state) + }) + .disposed(by: disposeBag) + + func setupNavigationBar() { + navigationItem.hidesBackButton = true + + let cancel = UIBarButtonItem(title: L10n.cancel, style: .plain, target: nil, action: nil) + cancel.rx.tap + .subscribe(onNext: { [weak self] in + self?.cancel() + }) + .disposed(by: disposeBag) + navigationItem.leftBarButtonItem = cancel + + guard viewModel.state.isEditable else { return } + + let save = UIBarButtonItem(title: L10n.save, style: .done, target: nil, action: nil) + save.rx.tap + .subscribe(onNext: { [weak self] in + guard let self else { return } + let state = viewModel.state + saveAction(state.data, state.updateSubsequentLabels) + self.cancel() + }) + .disposed(by: disposeBag) - self.viewModel.stateObservable - .subscribe(onNext: { [weak self] state in - self?.update(to: state) - }) - .disposed(by: self.disposeBag) + navigationItem.rightBarButtonItem = save + } + + func setupTableView() { + tableView.widthAnchor.constraint(equalToConstant: AnnotationPopoverLayout.width).isActive = true + tableView.delegate = self + tableView.dataSource = self + tableView.register(ColorPickerCell.self, forCellReuseIdentifier: Section.properties.cellId(index: 0, propertyRows: [.colorPicker])) + tableView.register(LineWidthCell.self, forCellReuseIdentifier: Section.properties.cellId(index: 0, propertyRows: [.lineWidth])) + tableView.register(FontSizeCell.self, forCellReuseIdentifier: Section.properties.cellId(index: 0, propertyRows: [.fontSize])) + tableView.register(SegmentedControlCell.self, forCellReuseIdentifier: Section.properties.cellId(index: 0, propertyRows: [.highlightUnderlineSwitch])) + tableView.register(TextContentEditCell.self, forCellReuseIdentifier: Section.textContent.cellId(index: 0, propertyRows: [])) + tableView.register(UITableViewCell.self, forCellReuseIdentifier: Section.actions.cellId(index: 0, propertyRows: [])) + tableView.register(UITableViewCell.self, forCellReuseIdentifier: Section.pageLabel.cellId(index: 0, propertyRows: [])) + tableView.setDefaultSizedHeader() + } } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(false, animated: true) - self.updatePreferredContentSize() + navigationController?.setNavigationBarHidden(false, animated: true) + updatePreferredContentSize() } // MARK: - Actions private func update(to state: AnnotationEditState) { if state.changes.contains(.color) { - self.reload(sections: [.properties, .textContent]) + reload(sections: [.properties, .textContent]) } if state.changes.contains(.pageLabel) { - self.reload(sections: [.pageLabel]) + reload(sections: [.pageLabel]) } - } - private func reload(sections: [Section]) { - for section in sections { - guard let index = self.sections.firstIndex(of: section) else { continue } - self.tableView.reloadSections(IndexSet(integer: index), with: .none) + func reload(sections: [Section]) { + for section in sections { + guard let index = self.sections.firstIndex(of: section) else { continue } + tableView.reloadSections(IndexSet(integer: index), with: .none) + } } } - private func reloadHeight() { - self.tableView.beginUpdates() - self.tableView.endUpdates() - } - - private func scrollToHighlightCursor() { - let indexPath = IndexPath(row: 0, section: 0) - - guard let cell = self.tableView.cellForRow(at: indexPath) as? TextContentEditCell, let cellCaretRect = cell.caretRect else { return } - - let rowRect = self.tableView.rectForRow(at: indexPath) - let caretRect = CGRect(x: (rowRect.minX + cellCaretRect.minX), y: (rowRect.minY + cellCaretRect.minY) + 10, width: cellCaretRect.width, height: cellCaretRect.height) - - guard caretRect.maxY > (self.tableView.contentInset.top + self.tableView.frame.height) else { return } - - self.tableView.scrollRectToVisible(caretRect, animated: false) - } - private func updatePreferredContentSize() { - let sectionCount = self.sections.count - (self.sections.contains(.textContent) ? 1 : 0) + let sectionCount = sections.count - (sections.contains(.textContent) ? 1 : 0) var height: CGFloat = (CGFloat(sectionCount) * 80) + 36 // 80 for 36 spacer and 44 cell height if sections.contains(.textContent) { - let width = AnnotationPopoverLayout.width - ((AnnotationPopoverLayout.annotationLayout.horizontalInset * 2) + - AnnotationPopoverLayout.annotationLayout.highlightContentLeadingOffset + - AnnotationPopoverLayout.annotationLayout.highlightLineWidth) + let width = AnnotationPopoverLayout.width - + ( + (AnnotationPopoverLayout.annotationLayout.horizontalInset * 2) + + AnnotationPopoverLayout.annotationLayout.highlightContentLeadingOffset + + AnnotationPopoverLayout.annotationLayout.highlightLineWidth + ) let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.minimumLineHeight = AnnotationPopoverLayout.annotationLayout.lineHeight paragraphStyle.maximumLineHeight = AnnotationPopoverLayout.annotationLayout.lineHeight @@ -138,73 +206,34 @@ final class AnnotationEditViewController: UIViewController { height += ceil(boundingRect.height) + 58 // 58 for 22 insets and 36 spacer } - if self.viewModel.state.type == .ink { + if viewModel.state.type == .ink { height += 49 // for line width slider } let size = CGSize(width: AnnotationPopoverLayout.width, height: height) - self.preferredContentSize = size - self.navigationController?.preferredContentSize = size + preferredContentSize = size + navigationController?.preferredContentSize = size } private func cancel() { - guard let navigationController = self.navigationController else { return } + guard let navigationController else { return } if navigationController.viewControllers.count == 1 { - self.navigationController?.presentingViewController?.dismiss(animated: true, completion: nil) + navigationController.presentingViewController?.dismiss(animated: true, completion: nil) } else { - self.navigationController?.popViewController(animated: true) + navigationController.popViewController(animated: true) } } - - // MARK: - Setups - - private func setupNavigationBar() { - self.navigationItem.hidesBackButton = true - - let cancel = UIBarButtonItem(title: L10n.cancel, style: .plain, target: nil, action: nil) - cancel.rx.tap.subscribe(onNext: { [weak self] in - self?.cancel() - }).disposed(by: self.disposeBag) - self.navigationItem.leftBarButtonItem = cancel - - guard self.viewModel.state.isEditable else { return } - - let save = UIBarButtonItem(title: L10n.save, style: .done, target: nil, action: nil) - save.rx.tap - .subscribe(onNext: { [weak self] in - guard let self else { return } - let state = viewModel.state - saveAction(state.data, state.updateSubsequentLabels) - self.cancel() - }) - .disposed(by: self.disposeBag) - - self.navigationItem.rightBarButtonItem = save - } - - private func setupTableView() { - self.tableView.widthAnchor.constraint(equalToConstant: AnnotationPopoverLayout.width).isActive = true - self.tableView.delegate = self - self.tableView.dataSource = self - self.tableView.register(ColorPickerCell.self, forCellReuseIdentifier: Section.properties.cellId(index: 0)) - self.tableView.register(LineWidthCell.self, forCellReuseIdentifier: Section.properties.cellId(index: 1)) - self.tableView.register(TextContentEditCell.self, forCellReuseIdentifier: Section.textContent.cellId(index: 0)) - self.tableView.register(FontSizeCell.self, forCellReuseIdentifier: Section.fontSize.cellId(index: 0)) - self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: Section.actions.cellId(index: 0)) - self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: Section.pageLabel.cellId(index: 0)) - self.tableView.setDefaultSizedHeader() - } } extension AnnotationEditViewController: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { - return self.sections.count + return sections.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch self.sections[section] { + switch sections[section] { case .properties: - return self.viewModel.state.type == .ink ? 2 : 1 + return propertyRows.count default: return 1 @@ -212,46 +241,48 @@ extension AnnotationEditViewController: UITableViewDataSource { } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let section = self.sections[indexPath.section] - let cell = tableView.dequeueReusableCell(withIdentifier: section.cellId(index: indexPath.row), for: indexPath) + let section = sections[indexPath.section] + let cell = tableView.dequeueReusableCell(withIdentifier: section.cellId(index: indexPath.row, propertyRows: propertyRows), for: indexPath) switch section { case .properties: if let cell = cell as? ColorPickerCell { - cell.setup(selectedColor: self.viewModel.state.color, annotationType: self.viewModel.state.type) + cell.setup(selectedColor: viewModel.state.color, annotationType: viewModel.state.type) cell.colorChange.subscribe { [weak viewModel] hex in viewModel?.process(action: .setColor(hex)) } .disposed(by: cell.disposeBag) } else if let cell = cell as? LineWidthCell { - cell.set(value: Float(self.viewModel.state.lineWidth)) - cell.valueObservable.subscribe(onNext: { value in self.viewModel.process(action: .setLineWidth(CGFloat(value))) }).disposed(by: cell.disposeBag) + cell.set(value: Float(viewModel.state.lineWidth)) + cell.valueObservable.subscribe(onNext: { [weak viewModel] value in viewModel?.process(action: .setLineWidth(CGFloat(value))) }).disposed(by: cell.disposeBag) + } else if let cell = cell as? FontSizeCell { + cell.set(value: viewModel.state.fontSize) + cell.valueObservable.subscribe(onNext: { [weak viewModel] value in viewModel?.process(action: .setFontSize(value)) }).disposed(by: cell.disposeBag) + } else if let cell = cell as? SegmentedControlCell { + let selected = viewModel.state.type == .highlight ? 0 : 1 + cell.setup(selected: selected, segments: [L10n.Pdf.highlight, L10n.Pdf.underline]) { [weak viewModel] selected in + viewModel?.process(action: .setAnnotationType(selected == 0 ? .highlight : .underline)) + } } case .textContent: if let cell = cell as? TextContentEditCell { - cell.setup(with: self.viewModel.state.highlightText, color: self.viewModel.state.color) + cell.setup(with: viewModel.state.highlightText, color: viewModel.state.color) cell.attributedTextAndHeightReloadNeededObservable.subscribe { [weak self] attributedText, needsHeightReload in guard let self else { return } viewModel.process(action: .setHighlight(attributedText)) if needsHeightReload { updatePreferredContentSize() - reloadHeight() - scrollToHighlightCursor() + reloadHeight(controller: self) + scrollToHighlightCursor(controller: self) } } .disposed(by: cell.disposeBag) } - case .fontSize: - if let cell = cell as? FontSizeCell { - cell.set(value: self.viewModel.state.fontSize) - cell.valueObservable.subscribe(onNext: { value in self.viewModel.process(action: .setFontSize(value)) }).disposed(by: cell.disposeBag) - } - case .pageLabel: - cell.textLabel?.text = L10n.page + " " + self.viewModel.state.pageLabel - if self.isEditing { + cell.textLabel?.text = L10n.page + " " + viewModel.state.pageLabel + if viewModel.state.isEditable { cell.accessoryType = .disclosureIndicator } else { cell.accessoryType = .none @@ -264,6 +295,20 @@ extension AnnotationEditViewController: UITableViewDataSource { } return cell + + func reloadHeight(controller: AnnotationEditViewController) { + controller.tableView.beginUpdates() + controller.tableView.endUpdates() + } + + func scrollToHighlightCursor(controller: AnnotationEditViewController) { + let indexPath = IndexPath(row: 0, section: 0) + guard let cell = controller.tableView.cellForRow(at: indexPath) as? TextContentEditCell, let cellCaretRect = cell.caretRect else { return } + let rowRect = controller.tableView.rectForRow(at: indexPath) + let caretRect = CGRect(x: (rowRect.minX + cellCaretRect.minX), y: (rowRect.minY + cellCaretRect.minY) + 10, width: cellCaretRect.width, height: cellCaretRect.height) + guard caretRect.maxY > (controller.tableView.contentInset.top + controller.tableView.frame.height) else { return } + controller.tableView.scrollRectToVisible(caretRect, animated: false) + } } } @@ -271,28 +316,35 @@ extension AnnotationEditViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) - switch self.sections[indexPath.section] { - case .properties, .textContent: break + switch sections[indexPath.section] { + case .textContent: + break + + case .properties: + guard indexPath.row < propertyRows.count else { return } + switch propertyRows[indexPath.row] { + case .colorPicker, .lineWidth, .highlightUnderlineSwitch: + break + + case .fontSize: + coordinatorDelegate?.showFontSizePicker(picked: { [weak self, weak tableView] newSize in + self?.viewModel.process(action: .setFontSize(newSize)) + tableView?.reloadRows(at: [indexPath], with: .none) + }) + } case .actions: - self.deleteAction() + deleteAction() case .pageLabel: - guard self.viewModel.state.isEditable else { return } - - self.coordinatorDelegate?.showPageLabelEditor( - label: self.viewModel.state.pageLabel, - updateSubsequentPages: self.viewModel.state.updateSubsequentLabels, + guard viewModel.state.isEditable else { return } + coordinatorDelegate?.showPageLabelEditor( + label: viewModel.state.pageLabel, + updateSubsequentPages: viewModel.state.updateSubsequentLabels, saveAction: { [weak self] newLabel, shouldUpdateSubsequentPages in self?.viewModel.process(action: .setPageLabel(newLabel, shouldUpdateSubsequentPages)) } ) - - case .fontSize: - self.coordinatorDelegate?.showFontSizePicker(picked: { [weak self, weak tableView] newSize in - self?.viewModel.process(action: .setFontSize(newSize)) - tableView?.reloadRows(at: [indexPath], with: .none) - }) } } } diff --git a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.swift b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.swift index 54e2594e8..1117ded42 100644 --- a/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.swift +++ b/Zotero/Scenes/Detail/Annotation Popover/Views/AnnotationPopoverViewController.swift @@ -125,7 +125,7 @@ final class AnnotationPopoverViewController: UIViewController { coordinatorDelegate?.showEdit( state: viewModel.state, saveAction: { [weak self] data, updateSubsequentLabels in - self?.viewModel.process(action: .setProperties(pageLabel: data.pageLabel, updateSubsequentLabels: updateSubsequentLabels, highlightText: data.highlightText)) + self?.viewModel.process(action: .setProperties(type: data.type, pageLabel: data.pageLabel, updateSubsequentLabels: updateSubsequentLabels, highlightText: data.highlightText)) }, deleteAction: { [weak self] in self?.viewModel.process(action: .delete) diff --git a/Zotero/Scenes/Detail/Annotation Popover/Views/SegmentedControlCell.swift b/Zotero/Scenes/Detail/Annotation Popover/Views/SegmentedControlCell.swift new file mode 100644 index 000000000..029792597 --- /dev/null +++ b/Zotero/Scenes/Detail/Annotation Popover/Views/SegmentedControlCell.swift @@ -0,0 +1,56 @@ +// +// SegmentedControlCell.swift +// Zotero +// +// Created by Michal Rentka on 14.01.2025. +// Copyright © 2025 Corporation for Digital Scholarship. All rights reserved. +// + +import UIKit + +class SegmentedControlCell: UITableViewCell { + private weak var segmentedControl: UISegmentedControl? + + private var selectionChanged: ((Int) -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setup() + selectionStyle = .none + + func setup() { + let segmentedControl = UISegmentedControl() + segmentedControl.addAction(UIAction(handler: { [weak self] _ in + self?.selectionChanged?(self?.segmentedControl?.selectedSegmentIndex ?? 0) + }), for: .valueChanged) + segmentedControl.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(segmentedControl) + self.segmentedControl = segmentedControl + + NSLayoutConstraint.activate([ + segmentedControl.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), + contentView.bottomAnchor.constraint(equalTo: segmentedControl.bottomAnchor, constant: 8), + segmentedControl.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), + contentView.trailingAnchor.constraint(equalTo: segmentedControl.trailingAnchor, constant: 15) + ]) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func prepareForReuse() { + super.prepareForReuse() + selectionChanged = nil + } + + func setup(selected: Int, segments: [String], selectionChanged: @escaping (Int) -> Void) { + self.selectionChanged = selectionChanged + segmentedControl?.removeAllSegments() + for (idx, segment) in segments.enumerated() { + segmentedControl?.insertSegment(withTitle: segment, at: idx, animated: false) + } + segmentedControl?.selectedSegmentIndex = selected < segments.count ? selected : 0 + } +} diff --git a/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift b/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift index 2bb59938e..ea14e4b11 100644 --- a/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift +++ b/Zotero/Scenes/Detail/PDF/AnnotationEditCoordinator.swift @@ -52,10 +52,9 @@ final class AnnotationEditCoordinator: Coordinator { let viewModel = ViewModel(initialState: state, handler: handler) let controller = AnnotationEditViewController( viewModel: viewModel, - includeColorPicker: true, - includeFontPicker: data.type == .freeText, - saveAction: self.saveAction, - deleteAction: self.deleteAction + properties: AnnotationEditViewController.PropertyRow.from(type: data.type, isAdditionalSettings: false), + saveAction: saveAction, + deleteAction: deleteAction ) controller.coordinatorDelegate = self self.navigationController?.setViewControllers([controller], animated: false) diff --git a/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift b/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift index 33666ccc5..aa0b91693 100644 --- a/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift +++ b/Zotero/Scenes/Detail/PDF/Models/PDFReaderAction.swift @@ -30,6 +30,7 @@ enum PDFReaderAction { case setLineWidth(key: String, width: CGFloat) case updateAnnotationProperties( key: String, + type: AnnotationType, color: String, lineWidth: CGFloat, fontSize: CGFloat, diff --git a/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift b/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift index 26f573b5f..b50f0beb5 100644 --- a/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift +++ b/Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift @@ -197,8 +197,9 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi case .setTags(let key, let tags): set(tags: tags, key: key, viewModel: viewModel) - case .updateAnnotationProperties(let key, let color, let lineWidth, let fontSize, let pageLabel, let updateSubsequentLabels, let highlightText, let highlightFont): + case .updateAnnotationProperties(let key, let type, let color, let lineWidth, let fontSize, let pageLabel, let updateSubsequentLabels, let highlightText, let highlightFont): set( + type: type, color: color, lineWidth: lineWidth, fontSize: fontSize, @@ -1227,17 +1228,17 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi private func set(lineWidth: CGFloat, key: String, viewModel: ViewModel) { guard let annotation = viewModel.state.annotation(for: PDFReaderState.AnnotationKey(key: key, type: .database)) else { return } - update(annotation: annotation, lineWidth: lineWidth, in: viewModel.state.document) + update(annotation: annotation, lineWidth: lineWidth, in: viewModel) } private func set(fontSize: CGFloat, key: String, viewModel: ViewModel) { guard let annotation = viewModel.state.annotation(for: PDFReaderState.AnnotationKey(key: key, type: .database)) else { return } - update(annotation: annotation, fontSize: fontSize, in: viewModel.state.document) + update(annotation: annotation, fontSize: fontSize, in: viewModel) } private func set(color: String, key: String, viewModel: ViewModel) { guard let annotation = viewModel.state.annotation(for: PDFReaderState.AnnotationKey(key: key, type: .database)) else { return } - update(annotation: annotation, color: (color, viewModel.state.interfaceStyle), in: viewModel.state.document) + update(annotation: annotation, color: (color, viewModel.state.interfaceStyle), in: viewModel) } private func set(comment: NSAttributedString, key: String, viewModel: ViewModel) { @@ -1249,7 +1250,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi state.comments[key] = comment } - update(annotation: annotation, contents: htmlComment, in: viewModel.state.document) + update(annotation: annotation, contents: htmlComment, in: viewModel) } private func set(tags: [Tag], key: String, viewModel: ViewModel) { @@ -1266,6 +1267,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi } private func set( + type: AnnotationType, color: String, lineWidth: CGFloat, fontSize: CGFloat, @@ -1276,13 +1278,16 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi key: String, viewModel: ViewModel ) { - // `lineWidth`, `fontSize` and `color` is stored in `Document`, update document, which will trigger a notification wich will update the DB + // `type`, `lineWidth`, `fontSize` and `color` is stored in `Document`, update document, which will trigger a notification wich will update the DB guard let annotation = viewModel.state.annotation(for: PDFReaderState.AnnotationKey(key: key, type: .database)) else { return } - update(annotation: annotation, color: (color, viewModel.state.interfaceStyle), lineWidth: lineWidth, fontSize: fontSize, in: viewModel.state.document) + update(annotation: annotation, type: type, color: (color, viewModel.state.interfaceStyle), lineWidth: lineWidth, fontSize: fontSize, in: viewModel) // Update remaining values directly let text = htmlAttributedStringConverter.convert(attributedString: highlightText) - let values = [KeyBaseKeyPair(key: FieldKeys.Item.Annotation.pageLabel, baseKey: nil): pageLabel, KeyBaseKeyPair(key: FieldKeys.Item.Annotation.text, baseKey: nil): text] + let values = [ + KeyBaseKeyPair(key: FieldKeys.Item.Annotation.pageLabel, baseKey: nil): pageLabel, + KeyBaseKeyPair(key: FieldKeys.Item.Annotation.text, baseKey: nil): text + ] let request = EditItemFieldsDbRequest(key: key, libraryId: viewModel.state.library.identifier, fieldValues: values, dateParser: dateParser) perform(request: request) { [weak self, weak viewModel] error in guard let self, let viewModel else { return } @@ -1300,58 +1305,111 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi private func update( annotation: PDFAnnotation, + type: AnnotationType? = nil, color: (String, UIUserInterfaceStyle)? = nil, lineWidth: CGFloat? = nil, fontSize: CGFloat? = nil, contents: String? = nil, - in document: PSPDFKit.Document + in viewModel: ViewModel ) { + let document = viewModel.state.document guard let pdfAnnotation = document.annotations(at: PageIndex(annotation.page)).first(where: { $0.key == annotation.key }) else { return } - - var changes: PdfAnnotationChanges = [] - - if let lineWidth, lineWidth.rounded(to: 3) != annotation.lineWidth { - changes.insert(.lineWidth) - } - if let fontSize, fontSize != annotation.fontSize { - changes.insert(.fontSize) - } - if let (color, _) = color, color != annotation.color { - changes.insert(.color) - } - if let contents, contents != annotation.comment { - changes.insert(.contents) + // If type changed, we need to remove the old annotation and insert a new one with proper types. + if let type, annotation.type != type { + changeType() + } else { + // Otherwise just update existing annotation + updateProperties() } - guard !changes.isEmpty else { return } + func changeType() { + let newAnnotation: PSPDFKit.Annotation + switch (type, annotation.type) { + case (.highlight, .underline): + newAnnotation = PSPDFKit.HighlightAnnotation() + + case (.underline, .highlight): + newAnnotation = PSPDFKit.UnderlineAnnotation() - document.undoController.recordCommand(named: nil, changing: [pdfAnnotation]) { - if changes.contains(.lineWidth), let inkAnnotation = pdfAnnotation as? PSPDFKit.InkAnnotation, let lineWidth { - inkAnnotation.lineWidth = lineWidth.rounded(to: 3) + default: + return } - if changes.contains(.color), let (color, interfaceStyle) = color { - let (_color, alpha, blendMode) = AnnotationColorGenerator.color(from: UIColor(hex: color), type: annotation.type, userInterfaceStyle: interfaceStyle) - pdfAnnotation.color = _color - pdfAnnotation.alpha = alpha + if let (color, interfaceStyle) = color, color != annotation.color { + let (_color, alpha, blendMode) = AnnotationColorGenerator.color(from: UIColor(hex: color), type: type, userInterfaceStyle: interfaceStyle) + newAnnotation.color = _color + newAnnotation.alpha = alpha if let blendMode { - pdfAnnotation.blendMode = blendMode + newAnnotation.blendMode = blendMode } + } else { + newAnnotation.color = pdfAnnotation.color + newAnnotation.alpha = pdfAnnotation.alpha + newAnnotation.blendMode = pdfAnnotation.blendMode } - if changes.contains(.contents), let contents { - pdfAnnotation.contents = contents - } + newAnnotation.rects = pdfAnnotation.rects + newAnnotation.boundingBox = pdfAnnotation.boundingBox + newAnnotation.pageIndex = pdfAnnotation.pageIndex + newAnnotation.contents = contents ?? pdfAnnotation.contents + newAnnotation.user = pdfAnnotation.user + newAnnotation.name = pdfAnnotation.name + + document.undoController.recordCommand(named: nil, in: { recorder in + recorder.record(removing: [pdfAnnotation]) { + document.remove(annotations: [pdfAnnotation]) + } + recorder.record(adding: [newAnnotation]) { + document.add(annotations: [newAnnotation]) + } + }) + } - if changes.contains(.fontSize), let textAnnotation = pdfAnnotation as? PSPDFKit.FreeTextAnnotation, let fontSize { - textAnnotation.fontSize = CGFloat(fontSize) + func updateProperties() { + var changes: PdfAnnotationChanges = [] + if let lineWidth, lineWidth.rounded(to: 3) != annotation.lineWidth { + changes.insert(.lineWidth) + } + if let fontSize, fontSize != annotation.fontSize { + changes.insert(.fontSize) + } + if let (color, _) = color, color != annotation.color { + changes.insert(.color) + } + if let contents, contents != annotation.comment { + changes.insert(.contents) } - NotificationCenter.default.post( - name: NSNotification.Name.PSPDFAnnotationChanged, - object: pdfAnnotation, - userInfo: [PSPDFAnnotationChangedNotificationKeyPathKey: PdfAnnotationChanges.stringValues(from: changes)] - ) + guard !changes.isEmpty else { return } + + document.undoController.recordCommand(named: nil, changing: [pdfAnnotation]) { + if changes.contains(.lineWidth), let inkAnnotation = pdfAnnotation as? PSPDFKit.InkAnnotation, let lineWidth { + inkAnnotation.lineWidth = lineWidth.rounded(to: 3) + } + + if changes.contains(.color), let (color, interfaceStyle) = color { + let (_color, alpha, blendMode) = AnnotationColorGenerator.color(from: UIColor(hex: color), type: annotation.type, userInterfaceStyle: interfaceStyle) + pdfAnnotation.color = _color + pdfAnnotation.alpha = alpha + if let blendMode { + pdfAnnotation.blendMode = blendMode + } + } + + if changes.contains(.contents), let contents { + pdfAnnotation.contents = contents + } + + if changes.contains(.fontSize), let textAnnotation = pdfAnnotation as? PSPDFKit.FreeTextAnnotation, let fontSize { + textAnnotation.fontSize = CGFloat(fontSize) + } + + NotificationCenter.default.post( + name: NSNotification.Name.PSPDFAnnotationChanged, + object: pdfAnnotation, + userInfo: [PSPDFAnnotationChangedNotificationKeyPathKey: PdfAnnotationChanges.stringValues(from: changes)] + ) + } } } diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift index ef4566ae3..0b325a959 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFAnnotationsViewController.swift @@ -119,6 +119,7 @@ final class PDFAnnotationsViewController: UIViewController { guard let viewModel else { return } viewModel.process(action: .updateAnnotationProperties( key: key.key, + type: data.type, color: data.color, lineWidth: data.lineWidth, fontSize: data.fontSize ?? 0, diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift index a2dca3f06..7e1398cc6 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFDocumentViewController.swift @@ -377,6 +377,7 @@ final class PDFDocumentViewController: UIViewController { guard let observable else { return } observable.subscribe(onNext: { [weak viewModel] state in guard let viewModel else { return } + // These are `AnnotationPopoverViewController` properties updated individually if state.changes.contains(.color) { viewModel.process(action: .setColor(key: key.key, color: state.color)) } @@ -392,13 +393,19 @@ final class PDFDocumentViewController: UIViewController { if state.changes.contains(.tags) { viewModel.process(action: .setTags(key: key.key, tags: state.tags)) } - if state.changes.contains(.pageLabel) || state.changes.contains(.highlight) { - // TODO: - fix font size + // These are `AnnotationEditViewController` properties updated all at once with Save button + if state.changes.contains(.pageLabel) || state.changes.contains(.highlight) || state.changes.contains(.type) { + var fontSize: CGFloat = 0 + if state.type == .freeText, let annotation = viewModel.state.annotation(for: key) { + // We should never actually get here, because Annotation Popup is not shown for Free Text Annotations. But in case we do get here, let's fetch current font size and pass it along. + fontSize = annotation.fontSize ?? 0 + } viewModel.process(action: .updateAnnotationProperties( key: key.key, + type: state.type, color: state.color, lineWidth: state.lineWidth, - fontSize: 0, + fontSize: fontSize, pageLabel: state.pageLabel, updateSubsequentLabels: state.updateSubsequentLabels, highlightText: state.highlightText, diff --git a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift index bbd544444..7c6857593 100644 --- a/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift +++ b/Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift @@ -448,7 +448,7 @@ class PDFReaderViewController: UIViewController { if state.changes.contains(.selectionDeletion) { // Hide popover if annotation has been deleted - if (presentedViewController as? UINavigationController)?.viewControllers.first is AnnotationPopover { + if let navigationController = presentedViewController as? UINavigationController, navigationController.viewControllers.first is AnnotationPopover, !navigationController.isBeingDismissed { dismiss(animated: true, completion: nil) } }