Как равномерно масштабировать расширенный текст в NSTextView?
Контекст:
У меня есть обычное приложение Mac OS X от Cocoa на базе документов, которое использует NSTextView
для ввода расширенного текста. Пользователь может редактировать семейство шрифтов, размер и цвет текста в NSTextView
.
Базовый SDK: 10,7
Цель развертывания: 10.6
Вопрос:
Я хотел бы реализовать масштабирование всего пользовательского интерфейса программно (включая NSTextView
), пока пользователь редактирует текст. Масштабирование кадра NSTextView
не представляет проблемы. Но я не знаю, как масштабировать редактируемый текст внутри представления, который может содержать несколько разных точечных размеров в разных подразделах всего пробега текста.
Как применить единый масштабный коэффициент к богатому тексту, отображаемому в NSTextView
?
Это должно хорошо сочетаться с "богатым текстом", так что семейство шрифтов пользователя, цвет и особенно размер точек (которые могут быть разными в разных точках текста) сохраняются, но масштабируются равномерно/относительно.
Возможно ли это с учетом моих базовых SDK и целей развертывания? Возможно ли это с новой базой SDK или целью развертывания?
Ответы
Ответ 1
Если целью является масштабирование представления (и фактически не изменение атрибутов в строке), я бы предложил использовать метод scaleUnitSquareToSize: вместе с ScalingScrollView (доступный с кодом примера TextEdit) для правильного поведения полосы прокрутки.
Основной фигурой из ScalingScrollView является:
- (void)setScaleFactor:(CGFloat)newScaleFactor adjustPopup:(BOOL)flag
{
CGFloat oldScaleFactor = scaleFactor;
if (scaleFactor != newScaleFactor)
{
NSSize curDocFrameSize, newDocBoundsSize;
NSView *clipView = [[self documentView] superview];
scaleFactor = newScaleFactor;
// Get the frame. The frame must stay the same.
curDocFrameSize = [clipView frame].size;
// The new bounds will be frame divided by scale factor
newDocBoundsSize.width = curDocFrameSize.width / scaleFactor;
newDocBoundsSize.height = curDocFrameSize.height / scaleFactor;
}
scaleFactor = newScaleFactor;
[scale_delegate scaleChanged:oldScaleFactor newScale:newScaleFactor];
}
scale_delegate
- ваш делегат, который может настроить ваш объект NSTextView
:
- (void) scaleChanged:(CGFloat)oldScale newScale:(CGFloat)newScale
{
NSInteger percent = lroundf(newScale * 100);
CGFloat scaler = newScale / oldScale;
[textView scaleUnitSquareToSize:NSMakeSize(scaler, scaler)];
NSLayoutManager* lm = [textView layoutManager];
NSTextContainer* tc = [textView textContainer];
[lm ensureLayoutForTextContainer:tc];
}
Метод scaleUnitSquareToSize:
масштабируется относительно текущего состояния, поэтому вы отслеживаете свой масштабный коэффициент, а затем преобразовываете запрос абсолютного масштаба (200%) в запрос относительной шкалы.
Ответ 2
Работает как для iOS, так и для Mac OS
@implementation NSAttributedString (Scale)
- (NSAttributedString *)attributedStringWithScale:(double)scale
{
if(scale == 1.0)
{
return self;
}
NSMutableAttributedString *copy = [self mutableCopy];
[copy beginEditing];
NSRange fullRange = NSMakeRange(0, copy.length);
[self enumerateAttribute:NSFontAttributeName inRange:fullRange options:0 usingBlock:^(UIFont *oldFont, NSRange range, BOOL *stop) {
double currentFontSize = oldFont.pointSize;
double newFontSize = currentFontSize * scale;
// don't trust -[UIFont fontWithSize:]
UIFont *scaledFont = [UIFont fontWithName:oldFont.fontName size:newFontSize];
[copy removeAttribute:NSFontAttributeName range:range];
[copy addAttribute:NSFontAttributeName value:scaledFont range:range];
}];
[self enumerateAttribute:NSParagraphStyleAttributeName inRange:fullRange options:0 usingBlock:^(NSParagraphStyle *oldParagraphStyle, NSRange range, BOOL *stop) {
NSMutableParagraphStyle *newParagraphStyle = [oldParagraphStyle mutableCopy];
newParagraphStyle.lineSpacing *= scale;
newParagraphStyle.paragraphSpacing *= scale;
newParagraphStyle.firstLineHeadIndent *= scale;
newParagraphStyle.headIndent *= scale;
newParagraphStyle.tailIndent *= scale;
newParagraphStyle.minimumLineHeight *= scale;
newParagraphStyle.maximumLineHeight *= scale;
newParagraphStyle.paragraphSpacing *= scale;
newParagraphStyle.paragraphSpacingBefore *= scale;
[copy removeAttribute:NSParagraphStyleAttributeName range:range];
[copy addAttribute:NSParagraphStyleAttributeName value:newParagraphStyle range:range];
}];
[copy endEditing];
return copy;
}
@end
Ответ 3
Здесь.
Я нашел одно решение, которое работает и не очень сложно реализовать. Я не уверен, что это лучшее/идеальное решение. Я все еще заинтересован в поиске других решений. Но здесь один из способов:
Вручную масштабируйте размер шрифта и размер строки по нескольким свойствам исходного текста NSAttributedString
перед отображением, а затем снимите отображаемый текст перед сохранением в качестве источника.
Проблема с этим решением заключается в том, что при масштабировании система Font Panel покажет фактический масштабный размер отображаемой точки выделенного текста (а не "реальный" размер исходной точки) при редактировании. Это нежелательно.
Здесь моя реализация этого:
- (void)scaleAttributedString:(NSMutableAttributedString *)str by:(CGFloat)scale {
if (1.0 == scale) return;
NSRange r = NSMakeRange(0, [str length]);
[str enumerateAttribute:NSFontAttributeName inRange:r options:0 usingBlock:^(NSFont *oldFont, NSRange range, BOOL *stop) {
NSFont *newFont = [NSFont fontWithName:[oldFont familyName] size:[oldFont pointSize] * scale];
NSParagraphStyle *oldParaStyle = [str attribute:NSParagraphStyleAttributeName atIndex:range.location effectiveRange:NULL];
NSMutableParagraphStyle *newParaStyle = [[oldParaStyle mutableCopy] autorelease];
CGFloat oldLineHeight = [oldParaStyle lineHeightMultiple];
CGFloat newLineHeight = scale * oldLineHeight;
[newParaStyle setLineHeightMultiple:newLineHeight];
id newAttrs = @{
NSParagraphStyleAttributeName: newParaStyle,
NSFontAttributeName: newFont,
};
[str addAttributes:newAttrs range:range];
}];
}
Это требует масштабирования исходного текста перед отображением:
// scale text
CGFloat scale = getCurrentScaleFactor();
[self scaleAttributedString:str by:scale];
А затем обратное масштабирование отображаемого текста перед сохранением в качестве источника:
// un-scale text
CGFloat scale = 1.0 / getCurrentScaleFactor();
[self scaleAttributedString:str by:scale];
Ответ 4
Я хочу поблагодарить Марка Мунца за его ответ, так как он спас меня от блуждания в темном лесу, полном безумия увеличения NSScrollView и NSLayoutManagers.
Для тех, кто все еще ищет, это мой подход. Этот код находится внутри NSDocument. Весь текст вставляется в контейнер с фиксированной шириной и по центру, а при масштабировании здесь сохраняется перенос слов и т.д. Он создает приятный вид "просмотра страницы", не прибегая к сложному управлению макетом.
Для работы этого примера необходимо, чтобы в вашем классе были установлены константы CGFloat _documentSize
и NSTextView textView
.
- (void) initZoom {
// Call this when the view has loaded and is ready
// I am storing a separate _scaleFactor and _magnification for my own purposes, mainly to have the initial scale to be higher than 1.0
_scaleFactor = 1.0;
_magnification = 1.1;
[self setScaleFactor:_magnification adjustPopup:false];
[self updateLayout];
// NOTE: You might need to call updateLayout after the content is set and we know the window size etc.
}
- (void) zoom: (bool) zoomIn {
if (!_scaleFactor) _scaleFactor = _magnification;
// Arbitrary maximum levels of zoom
if (zoomIn) {
if (_magnification < 1.6) _magnification += 0.1;
} else {
if (_magnification > 0.8) _magnification -= 0.1;
}
[self setScaleFactor:_magnification adjustPopup:false];
[self updateLayout];
}
- (void)setScaleFactor:(CGFloat)newScaleFactor adjustPopup:(BOOL)flag
{
CGFloat oldScaleFactor = _scaleFactor;
if (_scaleFactor != newScaleFactor)
{
NSSize curDocFrameSize, newDocBoundsSize;
NSView *clipView = [[self textView] superview];
_scaleFactor = newScaleFactor;
// Get the frame. The frame must stay the same.
curDocFrameSize = [clipView frame].size;
// The new bounds will be frame divided by scale factor
//newDocBoundsSize.width = curDocFrameSize.width / _scaleFactor;
newDocBoundsSize.width = curDocFrameSize.width;
newDocBoundsSize.height = curDocFrameSize.height / _scaleFactor;
NSRect newFrame = NSMakeRect(0, 0, newDocBoundsSize.width, newDocBoundsSize.height);
clipView.frame = newFrame;
}
_scaleFactor = newScaleFactor;
[self scaleChanged:oldScaleFactor newScale:newScaleFactor];
}
- (void) scaleChanged:(CGFloat)oldScale newScale:(CGFloat)newScale
{
CGFloat scaler = newScale / oldScale;
[self.textView scaleUnitSquareToSize:NSMakeSize(scaler, scaler)];
NSLayoutManager* lm = [self.textView layoutManager];
NSTextContainer* tc = [self.textView textContainer];
[lm ensureLayoutForTextContainer:tc];
}
- (void) updateLayout {
CGFloat width = (self.textView.frame.size.width / 2 - _documentWidth * _magnification / 2) / _magnification; self.textView.textContainerInset = NSMakeSize(width, TEXT_INSET_TOP);
self.textView.textContainer.size = NSMakeSize(_documentWidth, self.textView.textContainer.size.height);
}