// // KMFontWell.swift // PDF Reader Pro // // Created by tangchao on 2023/11/7. // import Cocoa class KMFontWellCell: NSButtonCell { var textColor: NSColor? var hasTextColor: Bool = false private let FONTNAME_KEY = "fontName" private let FONTSIZE_KEY = "fontSize" private let TEXTCOLOR_KEY = "textColor" private let HASTEXTCOLOR_KEY = "hasTextColor" deinit { KMPrint("KMFontWellCell deinit.") } override init(textCell string: String) { super.init(textCell: string) self.commonInit() } required init(coder: NSCoder) { super.init(coder: coder) self.textColor = coder.decodeObject(forKey: TEXTCOLOR_KEY) as? NSColor self.hasTextColor = coder.decodeBool(forKey: HASTEXTCOLOR_KEY) self.commonInit() } func commonInit() { if self.textColor == nil { self.textColor = .black } self.bezelStyle = .shadowlessSquare self.setButtonType(.pushOnPushOff) self.state = .off } override func encode(with coder: NSCoder) { super.encode(with: coder) coder.encodeConditionalObject(self.textColor, forKey: TEXTCOLOR_KEY) coder.encode(self.hasTextColor, forKey: HASTEXTCOLOR_KEY) } override func copy(with zone: NSZone? = nil) -> Any { let cell = super.copy(with: zone) as? KMFontWellCell cell?.textColor = self.textColor?.copy(with: zone) as? NSColor cell?.hasTextColor = self.hasTextColor return cell as Any } override func drawBezel(withFrame frame: NSRect, in controlView: NSView) { KMDrawTextFieldBezel(frame, controlView) if (self.state == .on) { NSGraphicsContext.saveGraphicsState() NSColor.selectedControlColor.setFill() frame.fill(using: .plusDarker) NSGraphicsContext.restoreGraphicsState() } if (self.isHighlighted) { NSGraphicsContext.saveGraphicsState() NSColor(calibratedWhite: 0, alpha: 0.1).setFill() frame.frame(withWidth: 1, using: .plusDarker) NSGraphicsContext.restoreGraphicsState() } if (self.showsFirstResponder) { NSGraphicsContext.saveGraphicsState() NSFocusRingPlacement.only.set() _ = CGRect.fill(frame) NSGraphicsContext.restoreGraphicsState() } } override var attributedTitle: NSAttributedString { set { super.attributedTitle = newValue } get { if (self.hasTextColor) { var attrString = super.attributedTitle.mutableCopy() as? NSMutableAttributedString attrString?.addAttribute(.foregroundColor, value: self.textColor ?? NSColor.black, range: NSMakeRange(0, attrString?.length ?? 0)) if let data = self.textColor?.usingColorSpaceName(.calibratedRGB)?.brightnessComponent, data > 0.8 { let shade = NSShadow() shade.shadowColor = .black shade.shadowBlurRadius = 1 attrString?.addAttribute(.shadow, value: shade, range: NSMakeRange(0, attrString?.length ?? 0)) } return attrString! } else { return super.attributedTitle } } } } private let SKFontWellWillBecomeActiveNotification = "SKFontWellWillBecomeActiveNotification" private let ACTION_KEY = "action" private let TARGET_KEY = "target" class KMFontWell: NSButton { var isActive: Bool { get { return self.state == .on } } var fontName: String? { get { return self.font?.fontName } set { if let _fontName = newValue { let newFont = NSFont(name: _fontName, size: self.font?.pointSize ?? 12) self.font = newFont } } } var fontSize: CGFloat { get { return self.font?.pointSize ?? 12 } set { if let _fontName = self.fontName { let newFont = NSFont(name: _fontName, size: newValue) self.font = newFont } } } var textColor: NSColor? { get { return (self.cell as? KMFontWellCell)?.textColor } set { let didChange = self.textColor?.isEqual(to: newValue) ?? false (self.cell as? KMFontWellCell)?.textColor = newValue if didChange { self.needsDisplay = true } } } var hasTextColor: Bool { get { if let data = self.cell as? KMFontWellCell { return data.hasTextColor } return false } set { if self.hasTextColor != newValue { (self.cell as? KMFontWellCell)?.hasTextColor = newValue self.needsDisplay = true } } } private var _pri_target: AnyObject? private var _pri_action: Selector? /* @interface SKFontWell : NSButton { NSMutableDictionary *bindingInfo; } - (void)changeFontFromFontManager:(id)sender; - (void)changeAttributesFromFontManager:(id)sender; */ override class var cellClass: AnyClass? { set { super.cellClass = newValue } get { return KMFontWellCell.self } } deinit { // SKENSURE_MAIN_THREAD( // [self unbind:FONTNAME_KEY]; // [self unbind:FONTSIZE_KEY]; // ); NotificationCenter.default.removeObserver(self) // SKDESTROY(bindingInfo); } override init(frame frameRect: NSRect) { super.init(frame: frameRect) self.commonInit() } required init?(coder: NSCoder) { super.init(coder: coder) let oldCell = self.cell as? NSButtonCell if let data = oldCell?.isKind(of: Self.cellClass!), !data { let newCell = KMFontWellCell(textCell: "") newCell.alignment = oldCell?.alignment ?? .center newCell.isEditable = oldCell?.isEditable ?? true newCell.target = oldCell?.target newCell.action = oldCell?.action self.cell = newCell } action = NSSelectorFromString(coder.decodeObject(forKey: ACTION_KEY) as? String ?? "") target = coder.decodeObject(forKey: TARGET_KEY) as AnyObject? self.commonInit() } override func encode(with coder: NSCoder) { super.encode(with: coder) if let data = self.action { coder.encode(NSStringFromSelector(data), forKey: ACTION_KEY) } coder.encodeConditionalObject(target, forKey: TARGET_KEY) } func commonInit() { if (self.font == nil) { self.font = .systemFont(ofSize: 0.0) } self.isEnabled = true self._fontChanged() super.action = #selector(_changeActive) super.target = self // bindingInfo = [[NSMutableDictionary alloc] init]; // [self registerForDraggedTypes:[NSArray arrayWithObjects:SKNSFontPanelDescriptorsPboardType, SKNSFontPanelFamiliesPboardType, NSPasteboardTypeColor, nil]]; } override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) // Drawing code here. } override func viewWillMove(toWindow newWindow: NSWindow?) { self._deactivate() super.viewWillMove(toWindow: newWindow) } @objc func fontPickerWillBecomeActive(_ notification: NSNotification) { let sender = notification.object if self.isEqual(to: sender) == false && self.isActive { self._deactivate() } } @objc func fontPanelWillClose(_ notification: NSNotification) { self._deactivate() } override var font: NSFont? { get { return super.font } set { let didChange = (self.font?.isEqual(to: newValue) == false) super.font = newValue if didChange { self._fontChanged() } } } override func activeFontWell() -> KMFontWell? { if self.isActive { return self } return super.activeFontWell() } func changeFontFromFontManager(_ sender: AnyObject?) { if (self.isActive) { if let _fontManager = sender as? NSFontManager { if let _font = self.font { self.font = _fontManager.convert(_font) } } // [self notifyFontBinding]; self.sendAction(self._pri_action, to: self._pri_target) } } override var target: AnyObject? { get { return self._pri_target } set { self._pri_target = newValue } } override var action: Selector? { get { return self._pri_action } set { self._pri_action = newValue } } /* #define SKNSFontPanelDescriptorsPboardType @"NSFontPanelDescriptorsPboardType" #define SKNSFontPanelFamiliesPboardType @"NSFontPanelFamiliesPboardType" #define SKNSFontCollectionFontDescriptors @"NSFontCollectionFontDescriptors" #define FONT_KEY @"font" static char SKFontWellFontNameObservationContext; static char SKFontWellFontSizeObservationContext; @interface SKFontWell (SKPrivate) - (void)changeActive:(id)sender; - (void)fontChanged; @end @implementation SKFontWell + (void)initialize { // SKINITIALIZE; [self exposeBinding:FONTNAME_KEY]; [self exposeBinding:FONTSIZE_KEY]; [self exposeBinding:TEXTCOLOR_KEY]; } + (NSSet *)keyPathsForValuesAffectingFontName { return [NSSet setWithObjects:FONT_KEY, nil]; } + (NSSet *)keyPathsForValuesAffectingFontSize { return [NSSet setWithObjects:FONT_KEY, nil]; } - (Class)valueClassForBinding:(NSString *)binding { if ([binding isEqualToString:FONTNAME_KEY]) return [NSString class]; else if ([binding isEqualToString:FONTNAME_KEY]) return [NSNumber class]; else if ([binding isEqualToString:TEXTCOLOR_KEY]) return [NSColor class]; else return [super valueClassForBinding:binding]; } - (void)notifyFontBinding { NSDictionary *info = [self infoForBinding:FONTNAME_KEY]; [[info objectForKey:NSObservedObjectKey] setValue:[self fontName] forKeyPath:[info objectForKey:NSObservedKeyPathKey]]; info = [self infoForBinding:FONTSIZE_KEY]; [[info objectForKey:NSObservedObjectKey] setValue:[NSNumber numberWithDouble:[self fontSize]] forKeyPath:[info objectForKey:NSObservedKeyPathKey]]; } - (void)notifyTextColorBinding { NSDictionary *info = [self infoForBinding:TEXTCOLOR_KEY]; if (info) { id value = [self textColor]; NSString *transformerName = [[info objectForKey:NSOptionsKey] objectForKey:NSValueTransformerNameBindingOption]; if (transformerName && [transformerName isEqual:[NSNull null]] == NO) { NSValueTransformer *valueTransformer = [NSValueTransformer valueTransformerForName:transformerName]; value = [valueTransformer reverseTransformedValue:value]; } [[info objectForKey:NSObservedObjectKey] setValue:value forKeyPath:[info objectForKey:NSObservedKeyPathKey]]; } } - (void)changeAttributesFromFontManager:(id)sender { if ([self isActive] && [self hasTextColor]) { [self setTextColor:[[sender convertAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[self textColor], NSForegroundColorAttributeName, nil]] valueForKey:NSForegroundColorAttributeName]]; [self notifyTextColorBinding]; [self sendAction:[self action] to:[self target]]; } } #pragma mark Accessors - (SEL)action { return action; } - (void)setAction:(SEL)newAction { action = newAction; } - (id)target { return target; } - (void)setTarget:(id)newTarget { target = newTarget; } #pragma mark Binding support - (void)bind:(NSString *)bindingName toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options { if ([bindingName isEqualToString:FONTNAME_KEY] || [bindingName isEqualToString:FONTSIZE_KEY]) { if ([bindingInfo objectForKey:bindingName]) [self unbind:bindingName]; // NSDictionary *bindingsData = [NSDictionary dictionaryWithObjectsAndKeys:observableController, NSObservedObjectKey, [[keyPath copy] autorelease], NSObservedKeyPathKey, [[options copy] autorelease], NSOptionsKey, nil]; // [bindingInfo setObject:bindingsData forKey:bindingName]; void *context = NULL; if ([bindingName isEqualToString:FONTNAME_KEY]) context = &SKFontWellFontNameObservationContext; else if ([bindingName isEqualToString:FONTSIZE_KEY]) context = &SKFontWellFontSizeObservationContext; [observableController addObserver:self forKeyPath:keyPath options:0 context:context]; [self observeValueForKeyPath:keyPath ofObject:observableController change:nil context:context]; } else { [super bind:bindingName toObject:observableController withKeyPath:keyPath options:options]; } [self setNeedsDisplay:YES]; } - (void)unbind:(NSString *)bindingName { if ([bindingName isEqualToString:FONTNAME_KEY] || [bindingName isEqualToString:FONTSIZE_KEY]) { NSDictionary *info = [self infoForBinding:bindingName]; [[info objectForKey:NSObservedObjectKey] removeObserver:self forKeyPath:[info objectForKey:NSObservedKeyPathKey]]; [bindingInfo removeObjectForKey:bindingName]; } else { [super unbind:bindingName]; } [self setNeedsDisplay:YES]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSString *key = nil; if (context == &SKFontWellFontNameObservationContext) key = FONTNAME_KEY; else if (context == &SKFontWellFontSizeObservationContext) key = FONTSIZE_KEY; if (key) { NSDictionary *info = [self infoForBinding:key]; id value = [[info objectForKey:NSObservedObjectKey] valueForKeyPath:[info objectForKey:NSObservedKeyPathKey]]; if (NSIsControllerMarker(value) == NO) [self setValue:value forKey:key]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (NSDictionary *)infoForBinding:(NSString *)bindingName { return [bindingInfo objectForKey:bindingName] ?: [super infoForBinding:bindingName]; } #pragma mark NSDraggingDestination protocol - (NSDragOperation)draggingEntered:(id )sender { if ([self isEnabled] && [sender draggingSource] != self && [[sender draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:SKNSFontPanelDescriptorsPboardType, SKNSFontPanelFamiliesPboardType, ([self hasTextColor] ? NSPasteboardTypeColor : nil), nil]]) { [[self cell] setHighlighted:YES]; [self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]]; [self setNeedsDisplay:YES]; return NSDragOperationGeneric; } else return NSDragOperationNone; } - (void)draggingExited:(id )sender { if ([self isEnabled] && [sender draggingSource] != self && [[sender draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:SKNSFontPanelDescriptorsPboardType, SKNSFontPanelFamiliesPboardType, ([self hasTextColor] ? NSPasteboardTypeColor : nil), nil]]) { [[self cell] setHighlighted:NO]; [self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]]; [self setNeedsDisplay:YES]; } } - (BOOL)prepareForDragOperation:(id )sender { return [self isEnabled] && [sender draggingSource] != self && [[sender draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:SKNSFontPanelDescriptorsPboardType, SKNSFontPanelFamiliesPboardType, ([self hasTextColor] ? NSPasteboardTypeColor : nil), nil]]; } - (BOOL)performDragOperation:(id )sender{ NSPasteboard *pboard = [sender draggingPasteboard]; NSString *type = [pboard availableTypeFromArray:[NSArray arrayWithObjects:SKNSFontPanelDescriptorsPboardType, SKNSFontPanelFamiliesPboardType, ([self hasTextColor] ? NSPasteboardTypeColor : nil), nil]]; NSFont *droppedFont = nil; NSColor *droppedColor = nil; @try { if ([type isEqualToString:SKNSFontPanelDescriptorsPboardType]) { NSData *data = [pboard dataForType:type]; NSDictionary *dict = [data isKindOfClass:[NSData class]] ? [NSKeyedUnarchiver unarchiveObjectWithData:data] : nil; if ([dict isKindOfClass:[NSDictionary class]]) { NSArray *fontDescriptors = [dict objectForKey:SKNSFontCollectionFontDescriptors]; NSFontDescriptor *fontDescriptor = ([fontDescriptors isKindOfClass:[NSArray class]] && [fontDescriptors count]) ? [fontDescriptors objectAtIndex:0] : nil; if ([fontDescriptor isKindOfClass:[NSFontDescriptor class]]) { NSNumber *size = [[fontDescriptor fontAttributes] objectForKey:NSFontSizeAttribute] ?: [dict objectForKey:NSFontSizeAttribute]; CGFloat fontSize = [size respondsToSelector:@selector(doubleValue)] ? [size doubleValue] : [self fontSize]; droppedFont = [NSFont fontWithDescriptor:fontDescriptor size:fontSize]; } } } else if ([type isEqualToString:SKNSFontPanelFamiliesPboardType]) { NSArray *families = [pboard propertyListForType:type]; NSString *family = ([families isKindOfClass:[NSArray class]] && [families count]) ? [families objectAtIndex:0] : nil; if ([family isKindOfClass:[NSString class]]) droppedFont = [[NSFontManager sharedFontManager] convertFont:[self font] toFamily:family]; } else if ([type isEqualToString:NSPasteboardTypeColor]) { droppedColor = [NSColor colorFromPasteboard:pboard]; } } @catch (id exception) { NSLog(@"Ignoring exception %@ when dropping on SKFontWell failed", exception); } if (droppedFont) { [self setFont:droppedFont]; [self notifyFontBinding]; [self sendAction:[self action] to:[self target]]; } if (droppedColor) { [self setTextColor:droppedColor]; [self notifyTextColorBinding]; [self sendAction:[self action] to:[self target]]; } [[self cell] setHighlighted:NO]; [self setKeyboardFocusRingNeedsDisplayInRect:[self bounds]]; [self setNeedsDisplay:YES]; return droppedFont != nil || droppedColor != nil; } @end */ } // MARK: - Private Methods extension KMFontWell { private func _fontChanged() { if self.isActive { NSFontManager.shared.setSelectedFont(self.font ?? .systemFont(ofSize: 0), isMultiple: false) } self.title = String(format: "%@ %.0f", self.font?.displayName ?? "", self.fontSize) self.needsDisplay = true } @objc private func _changeActive(_ sender: AnyObject?) { if (self.isEnabled) { if (self.isActive) { self._activate() } else { self._deactivate() } } } private func _activate() { let nc = NotificationCenter.default let fm = NSFontManager.shared nc.post(name: NSNotification.Name(rawValue: SKFontWellWillBecomeActiveNotification), object: self) fm.setSelectedFont(self.font ?? .systemFont(ofSize: 0), isMultiple: false) fm.orderFrontFontPanel(self) nc.addObserver(self, selector: #selector(fontPickerWillBecomeActive), name: NSNotification.Name(SKFontWellWillBecomeActiveNotification), object: nil) nc.addObserver(self, selector: #selector(fontPanelWillClose), name: NSWindow.willCloseNotification, object: fm.fontPanel(true)) self.state = .on self.setKeyboardFocusRingNeedsDisplay(self.bounds) self.needsDisplay = true } private func _deactivate() { NotificationCenter.default.removeObserver(self) self.state = .off self.setKeyboardFocusRingNeedsDisplay(self.bounds) self.needsDisplay = true } }