notepad-plus-plus/scintilla/cocoa/InfoBar.mm

410 lines
14 KiB
Plaintext

/**
* Scintilla source code edit control
* InfoBar.mm - Implements special info bar with zoom info, caret position etc. to be used with
* ScintillaView.
*
* Mike Lischke <mlischke@sun.com>
*
* Copyright 2009 Sun Microsystems, Inc. All rights reserved.
* This file is dual licensed under LGPL v2.1 and the Scintilla license (http://www.scintilla.org/License.txt).
*/
#include <cmath>
#import "InfoBar.h"
//--------------------------------------------------------------------------------------------------
@implementation VerticallyCenteredTextFieldCell
// Inspired by code from Daniel Jalkut, Red Sweater Software.
- (NSRect) drawingRectForBounds: (NSRect) theRect {
// Get the parent's idea of where we should draw
NSRect newRect = [super drawingRectForBounds: theRect];
// When the text field is being edited or selected, we have to turn off the magic because it
// screws up the configuration of the field editor. We sneak around this by intercepting
// selectWithFrame and editWithFrame and sneaking a reduced, centered rect in at the last minute.
if (mIsEditingOrSelecting == NO) {
// Get our ideal size for current text
NSSize textSize = [self cellSizeForBounds: theRect];
// Center that in the proposed rect
CGFloat heightDelta = newRect.size.height - textSize.height;
if (heightDelta > 0) {
newRect.size.height -= heightDelta;
newRect.origin.y += std::ceil(heightDelta / 2);
}
}
return newRect;
}
//--------------------------------------------------------------------------------------------------
- (void) selectWithFrame: (NSRect) aRect inView: (NSView *) controlView editor: (NSText *) textObj
delegate: (id) anObject start: (NSInteger) selStart length: (NSInteger) selLength {
aRect = [self drawingRectForBounds: aRect];
mIsEditingOrSelecting = YES;
[super selectWithFrame: aRect
inView: controlView
editor: textObj
delegate: anObject
start: selStart
length: selLength];
mIsEditingOrSelecting = NO;
}
//--------------------------------------------------------------------------------------------------
- (void) editWithFrame: (NSRect) aRect inView: (NSView *) controlView editor: (NSText *) textObj
delegate: (id) anObject event: (NSEvent *) theEvent {
aRect = [self drawingRectForBounds: aRect];
mIsEditingOrSelecting = YES;
[super editWithFrame: aRect
inView: controlView
editor: textObj
delegate: anObject
event: theEvent];
mIsEditingOrSelecting = NO;
}
@end
//--------------------------------------------------------------------------------------------------
@implementation InfoBar
- (instancetype) initWithFrame: (NSRect) frame {
self = [super initWithFrame: frame];
if (self) {
NSBundle *bundle = [NSBundle bundleForClass: [InfoBar class]];
NSString *path = [bundle pathForResource: @"info_bar_bg" ofType: @"tiff" inDirectory: nil];
// macOS 10.13 introduced bug where pathForResource: fails on SMB share
if (path == nil) {
path = [bundle.bundlePath stringByAppendingPathComponent: @"Resources/info_bar_bg.tiff"];
}
mBackground = [[NSImage alloc] initWithContentsOfFile: path];
if (!mBackground.valid)
NSLog(@"Background image for info bar is invalid.");
mScaleFactor = 1.0;
mCurrentCaretX = 0;
mCurrentCaretY = 0;
[self createItems];
}
return self;
}
//--------------------------------------------------------------------------------------------------
/**
* Called by a connected component (usually the info bar) if something changed there.
*
* @param type The type of the notification.
* @param message Carries the new status message if the type is a status message change.
* @param location Carries the new location (e.g. caret) if the type is a caret change or similar type.
* @param value Carries the new zoom value if the type is a zoom change.
*/
- (void) notify: (NotificationType) type message: (NSString *) message location: (NSPoint) location
value: (float) value {
switch (type) {
case IBNZoomChanged:
[self setScaleFactor: value adjustPopup: YES];
break;
case IBNCaretChanged:
[self setCaretPosition: location];
break;
case IBNStatusChanged:
mStatusTextLabel.stringValue = message;
break;
}
}
//--------------------------------------------------------------------------------------------------
/**
* Used to set a protocol object we can use to send change notifications to.
*/
- (void) setCallback: (id <InfoBarCommunicator>) callback {
mCallback = callback;
}
//--------------------------------------------------------------------------------------------------
static NSString *DefaultScaleMenuLabels[] = {
@"20%", @"30%", @"50%", @"75%", @"100%", @"130%", @"160%", @"200%", @"250%", @"300%"
};
static float DefaultScaleMenuFactors[] = {
0.2f, 0.3f, 0.5f, 0.75f, 1.0f, 1.3f, 1.6f, 2.0f, 2.5f, 3.0f
};
static unsigned DefaultScaleMenuSelectedItemIndex = 4;
static float BarFontSize = 10.0;
- (void) createItems {
// 1) The zoom popup.
unsigned numberOfDefaultItems = sizeof(DefaultScaleMenuLabels) / sizeof(NSString *);
// Create the popup button.
mZoomPopup = [[NSPopUpButton alloc] initWithFrame: NSMakeRect(0.0, 0.0, 1.0, 1.0) pullsDown: NO];
// No border or background please.
[mZoomPopup.cell setBordered: NO];
[mZoomPopup.cell setArrowPosition: NSPopUpArrowAtBottom];
// Fill it.
for (unsigned count = 0; count < numberOfDefaultItems; count++) {
[mZoomPopup addItemWithTitle: NSLocalizedStringFromTable(DefaultScaleMenuLabels[count], @"ZoomValues", nil)];
id currentItem = [mZoomPopup itemAtIndex: count];
if (DefaultScaleMenuFactors[count] != 0.0)
[currentItem setRepresentedObject: @(DefaultScaleMenuFactors[count])];
}
[mZoomPopup selectItemAtIndex: DefaultScaleMenuSelectedItemIndex];
// Hook it up.
mZoomPopup.target = self;
mZoomPopup.action = @selector(zoomItemAction:);
// Set a suitable font.
mZoomPopup.font = [NSFont menuBarFontOfSize: BarFontSize];
// Make sure the popup is big enough to fit the cells.
[mZoomPopup sizeToFit];
// Don't let it become first responder
[mZoomPopup setRefusesFirstResponder: YES];
// put it in the scrollview.
[self addSubview: mZoomPopup];
// 2) The caret position label.
Class oldCellClass = [NSTextField cellClass];
[NSTextField setCellClass: [VerticallyCenteredTextFieldCell class]];
mCaretPositionLabel = [[NSTextField alloc] initWithFrame: NSMakeRect(0.0, 0.0, 50.0, 1.0)];
[mCaretPositionLabel setBezeled: NO];
[mCaretPositionLabel setBordered: NO];
[mCaretPositionLabel setEditable: NO];
[mCaretPositionLabel setSelectable: NO];
[mCaretPositionLabel setDrawsBackground: NO];
mCaretPositionLabel.font = [NSFont menuBarFontOfSize: BarFontSize];
NSTextFieldCell *cell = mCaretPositionLabel.cell;
cell.placeholderString = @"0:0";
cell.alignment = NSTextAlignmentCenter;
[self addSubview: mCaretPositionLabel];
// 3) The status text.
mStatusTextLabel = [[NSTextField alloc] initWithFrame: NSMakeRect(0.0, 0.0, 1.0, 1.0)];
[mStatusTextLabel setBezeled: NO];
[mStatusTextLabel setBordered: NO];
[mStatusTextLabel setEditable: NO];
[mStatusTextLabel setSelectable: NO];
[mStatusTextLabel setDrawsBackground: NO];
mStatusTextLabel.font = [NSFont menuBarFontOfSize: BarFontSize];
cell = mStatusTextLabel.cell;
cell.placeholderString = @"";
[self addSubview: mStatusTextLabel];
// Restore original cell class so that everything else doesn't get broken
[NSTextField setCellClass: oldCellClass];
}
//--------------------------------------------------------------------------------------------------
//--------------------------------------------------------------------------------------------------
/**
* Fill the background.
*/
- (void) drawRect: (NSRect) rect {
[[NSColor controlBackgroundColor] set];
[NSBezierPath fillRect: rect];
// Since the background is seamless, we don't need to take care for the proper offset.
// Simply tile the background over the invalid rectangle.
if (mBackground.size.width != 0) {
NSPoint target = {rect.origin.x, 0};
while (target.x < rect.origin.x + rect.size.width) {
[mBackground drawAtPoint: target fromRect: NSZeroRect operation: NSCompositingOperationSourceOver fraction: 1];
target.x += mBackground.size.width;
}
}
// Draw separator lines between items.
NSRect verticalLineRect;
CGFloat component = 190.0 / 255.0;
NSColor *lineColor = [NSColor colorWithDeviceRed: component green: component blue: component alpha: 1];
if (mDisplayMask & IBShowZoom) {
verticalLineRect = mZoomPopup.frame;
verticalLineRect.origin.x += verticalLineRect.size.width + 1.0;
verticalLineRect.size.width = 1.0;
if (NSIntersectsRect(rect, verticalLineRect)) {
[lineColor set];
NSRectFill(verticalLineRect);
}
}
if (mDisplayMask & IBShowCaretPosition) {
verticalLineRect = mCaretPositionLabel.frame;
verticalLineRect.origin.x += verticalLineRect.size.width + 1.0;
verticalLineRect.size.width = 1.0;
if (NSIntersectsRect(rect, verticalLineRect)) {
[lineColor set];
NSRectFill(verticalLineRect);
}
}
}
//--------------------------------------------------------------------------------------------------
- (BOOL) isOpaque {
return YES;
}
//--------------------------------------------------------------------------------------------------
/**
* Used to reposition our content depending on the size of the view.
*/
- (void) setFrame: (NSRect) newFrame {
super.frame = newFrame;
[self positionSubViews];
}
//--------------------------------------------------------------------------------------------------
- (void) positionSubViews {
NSRect currentBounds = {{0, 0}, {0, self.frame.size.height}};
if (mDisplayMask & IBShowZoom) {
[mZoomPopup setHidden: NO];
currentBounds.size.width = mZoomPopup.frame.size.width;
mZoomPopup.frame = currentBounds;
currentBounds.origin.x += currentBounds.size.width + 1; // Add 1 for the separator.
} else
[mZoomPopup setHidden: YES];
if (mDisplayMask & IBShowCaretPosition) {
[mCaretPositionLabel setHidden: NO];
currentBounds.size.width = mCaretPositionLabel.frame.size.width;
mCaretPositionLabel.frame = currentBounds;
currentBounds.origin.x += currentBounds.size.width + 1;
} else
[mCaretPositionLabel setHidden: YES];
if (mDisplayMask & IBShowStatusText) {
// The status text always takes the rest of the available space.
[mStatusTextLabel setHidden: NO];
currentBounds.size.width = self.frame.size.width - currentBounds.origin.x;
mStatusTextLabel.frame = currentBounds;
} else
[mStatusTextLabel setHidden: YES];
}
//--------------------------------------------------------------------------------------------------
/**
* Used to switch the visible parts of the info bar.
*
* @param display Bitwise ORed IBDisplay values which determine what to show on the bar.
*/
- (void) setDisplay: (IBDisplay) display {
if (mDisplayMask != display) {
mDisplayMask = display;
[self positionSubViews];
self.needsDisplay = YES;
}
}
//--------------------------------------------------------------------------------------------------
/**
* Handler for selection changes in the zoom menu.
*/
- (void) zoomItemAction: (id) sender {
NSNumber *selectedFactorObject = [[sender selectedCell] representedObject];
if (selectedFactorObject == nil) {
NSLog(@"Scale popup action: setting arbitrary zoom factors is not yet supported.");
return;
} else {
[self setScaleFactor: selectedFactorObject.floatValue adjustPopup: NO];
}
}
//--------------------------------------------------------------------------------------------------
- (void) setScaleFactor: (float) newScaleFactor adjustPopup: (BOOL) flag {
if (mScaleFactor != newScaleFactor) {
mScaleFactor = newScaleFactor;
if (flag) {
unsigned count = 0;
unsigned numberOfDefaultItems = sizeof(DefaultScaleMenuFactors) / sizeof(float);
// We only work with some preset zoom values. If the given value does not correspond
// to one then show no selection.
while (count < numberOfDefaultItems && (std::abs(newScaleFactor - DefaultScaleMenuFactors[count]) > 0.07))
count++;
if (count == numberOfDefaultItems)
[mZoomPopup selectItemAtIndex: -1];
else {
[mZoomPopup selectItemAtIndex: count];
// Set scale factor to found preset value if it comes close.
mScaleFactor = DefaultScaleMenuFactors[count];
}
} else {
// Internally set. Notify owner.
[mCallback notify: IBNZoomChanged message: nil location: NSZeroPoint value: newScaleFactor];
}
}
}
//--------------------------------------------------------------------------------------------------
/**
* Called from the notification method to update the caret position display.
*/
- (void) setCaretPosition: (NSPoint) position {
// Make the position one-based.
int newX = (int) position.x + 1;
int newY = (int) position.y + 1;
if (mCurrentCaretX != newX || mCurrentCaretY != newY) {
mCurrentCaretX = newX;
mCurrentCaretY = newY;
mCaretPositionLabel.stringValue = [NSString stringWithFormat: @"%d:%d", newX, newY];
}
}
//--------------------------------------------------------------------------------------------------
/**
* Makes the bar resize to the smallest width that can accommodate the currently enabled items.
*/
- (void) sizeToFit {
NSRect frame = self.frame;
frame.size.width = 0;
if (mDisplayMask & IBShowZoom)
frame.size.width += mZoomPopup.frame.size.width;
if (mDisplayMask & IBShowCaretPosition)
frame.size.width += mCaretPositionLabel.frame.size.width;
if (mDisplayMask & IBShowStatusText)
frame.size.width += mStatusTextLabel.frame.size.width;
self.frame = frame;
}
@end