Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

120 lines
6.3 KiB
Swift

import Foundation
import UIKit
import Display
import ContextUI
import TelegramPresentationData
import RichTextEditorCore
import RichTextEditorUIKit
import TelegramCore
/// Presents the editor's table row/column structural menu as a Telegram `ContextController`, anchored to
/// the tapped handle described by `request`. Shared by both editor hosts (the chat composer and the
/// attachment screen); they differ only in HOW the built controller is presented, injected via `present`.
/// This is the ONE place that maps the editor's framework-agnostic `TableStructuralMenuRequest` to ContextUI.
@available(iOS 13.0, *)
public func presentTableStructuralMenu(
_ request: TableStructuralMenuRequest,
presentationData: PresentationData,
present: (ViewController) -> Void
) {
guard let anchorView = request.view else { return }
// A transient zero-interaction anchor at the handle's rect (in the canvas's coordinate space); the
// reference source converts it to window space. Removed when the controller is dismissed.
let anchor = UIView(frame: request.sourceRect)
anchor.isUserInteractionEnabled = false
anchorView.addSubview(anchor)
// The top "attributes" group (alignment + header) applies to the selected cells directly, as opposed
// to the Add/Delete structural actions below. Built up front so the separator logic stays coherent
// regardless of which of the two (if either) is present.
var attributeItems: [ContextMenuItem] = []
if let alignment = request.alignment {
attributeItems.append(.custom(TableStructuralMenuAlignmentItem(
initialHorizontal: alignment.horizontal.map(tableHAlign(fromCore:)),
initialVertical: alignment.vertical.map(tableVAlign(fromCore:)),
action: { h, v in
alignment.apply(h.map(coreHAlign(from:)), v.map(coreVAlign(from:)))
}), false))
}
var items: [ContextMenuItem] = attributeItems
if !attributeItems.isEmpty && (!request.actions.isEmpty || request.header != nil) {
items.append(.separator)
}
if let header = request.header {
items.append(.action(ContextMenuActionItem(
text: header.isHeader == true ? presentationData.strings.RichText_Menu_Table_HighlightOff : presentationData.strings.RichText_Menu_Table_HighlightOn,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: header.isHeader == true ? "Chat/Context Menu/CellHighlightRemove" : "Chat/Context Menu/CellHighlightAdd"), color: theme.contextMenu.primaryColor)
},
action: { _, f in f(.default); header.apply() }
)))
}
items.append(contentsOf: request.actions.map { action in
let (title, icon) = tableStructuralMenuTitleAndIcon(action.kind, strings: presentationData.strings)
return .action(ContextMenuActionItem(
text: title,
textColor: tableStructuralMenuIsDestructive(action.kind) ? .destructive : .primary,
icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: icon), color: tableStructuralMenuIsDestructive(action.kind) ? theme.contextMenu.destructiveColor : theme.contextMenu.primaryColor)
},
action: { _, f in f(.default); action.perform() }
))
})
let controller = makeContextController(
presentationData: presentationData,
source: .reference(RichTextStructuralMenuReferenceSource(sourceView: anchor)),
items: .single(ContextController.Items(content: .list(items))),
gesture: nil
)
controller.dismissed = { [weak anchor] in anchor?.removeFromSuperview() }
present(controller)
}
private func tableStructuralMenuTitleAndIcon(_ kind: TableStructuralMenuRequest.Kind, strings: PresentationStrings) -> (title: String, icon: String) {
switch kind {
case .addColumnLeft: return (strings.RichText_Menu_Table_AddColumnLeft, "Chat/Context Menu/CellAddLeft")
case .addColumnRight: return (strings.RichText_Menu_Table_AddColumnRight, "Chat/Context Menu/CellAddRight")
case .deleteColumn: return (strings.RichText_Menu_Table_DeleteColumn, "Chat/Context Menu/CellDelete")
case .addRowAbove: return (strings.RichText_Menu_Table_AddRowAbove, "Chat/Context Menu/CellAddTop")
case .addRowBelow: return (strings.RichText_Menu_Table_AddRowBelow, "Chat/Context Menu/CellAddBottom")
case .deleteRow: return (strings.RichText_Menu_Table_DeleteRow, "Chat/Context Menu/CellDelete")
case .mergeCells: return (strings.RichText_Menu_Table_MergeCells, "Chat/Context Menu/CellMergeH")
case .splitCell: return (strings.RichText_Menu_Table_SplitCell, "Chat/Context Menu/CellSplitH")
}
}
private func tableStructuralMenuIsDestructive(_ kind: TableStructuralMenuRequest.Kind) -> Bool {
switch kind {
case .deleteColumn, .deleteRow: return true
default: return false
}
}
private func tableHAlign(fromCore a: TextAlignment) -> TableHorizontalAlignment {
switch a { case .left, .natural, .justified: return .left; case .center: return .center; case .right: return .right }
}
private func coreHAlign(from a: TableHorizontalAlignment) -> TextAlignment {
switch a { case .left: return .left; case .center: return .center; case .right: return .right }
}
private func tableVAlign(fromCore a: VerticalAlignment) -> TableVerticalAlignment {
switch a { case .top: return .top; case .middle: return .middle; case .bottom: return .bottom }
}
private func coreVAlign(from a: TableVerticalAlignment) -> VerticalAlignment {
switch a { case .top: return .top; case .middle: return .middle; case .bottom: return .bottom }
}
/// Anchors a `ContextController` to a sub-rect view (the transient handle anchor). Mirrors the attachment
/// screen's `RichTextActionContextReferenceSource`, generalized to any anchor view.
@available(iOS 13.0, *)
private final class RichTextStructuralMenuReferenceSource: ContextReferenceContentSource {
private let sourceView: UIView
init(sourceView: UIView) { self.sourceView = sourceView }
func transitionInfo() -> ContextControllerReferenceViewInfo? {
return ContextControllerReferenceViewInfo(referenceView: self.sourceView,
contentAreaInScreenSpace: UIScreen.main.bounds,
insets: UIEdgeInsets(top: -4.0, left: 0.0, bottom: -4.0, right: 0.0), actionsPosition: .bottom)
}
}