Screenshots in the Cocoa frontend
This commit is contained in:
parent
3f954f1d0c
commit
fc10a90dec
111
Cocoa/Document.m
111
Cocoa/Document.m
@ -2201,4 +2201,115 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency)
|
|||||||
{
|
{
|
||||||
return &gb;
|
return &gb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSImage *)takeScreenshot
|
||||||
|
{
|
||||||
|
NSImage *ret = nil;
|
||||||
|
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]) {
|
||||||
|
ret = [_view renderToImage];
|
||||||
|
}
|
||||||
|
if (!ret) {
|
||||||
|
ret = [Document imageFromData:[NSData dataWithBytesNoCopy:_view.currentBuffer
|
||||||
|
length:GB_get_screen_width(&gb) * GB_get_screen_height(&gb) * 4
|
||||||
|
freeWhenDone:false]
|
||||||
|
width:GB_get_screen_width(&gb)
|
||||||
|
height:GB_get_screen_height(&gb)
|
||||||
|
scale:1.0];
|
||||||
|
}
|
||||||
|
[ret lockFocus];
|
||||||
|
NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:NSMakeRect(0, 0,
|
||||||
|
ret.size.width, ret.size.height)];
|
||||||
|
[ret unlockFocus];
|
||||||
|
ret = [[NSImage alloc] initWithSize:ret.size];
|
||||||
|
[ret addRepresentation:bitmapRep];
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)screenshotFilename
|
||||||
|
{
|
||||||
|
NSDate *date = [NSDate date];
|
||||||
|
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||||
|
dateFormatter.dateStyle = NSDateFormatterLongStyle;
|
||||||
|
dateFormatter.timeStyle = NSDateFormatterMediumStyle;
|
||||||
|
return [[NSString stringWithFormat:@"%@ – %@.png",
|
||||||
|
self.fileURL.lastPathComponent.stringByDeletingPathExtension,
|
||||||
|
[dateFormatter stringFromDate:date]] stringByReplacingOccurrencesOfString:@":" withString:@"."]; // Gotta love Mac OS Classic
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction)saveScreenshot:(id)sender
|
||||||
|
{
|
||||||
|
NSString *folder = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBScreenshotFolder"];
|
||||||
|
BOOL isDirectory = false;
|
||||||
|
if (folder) {
|
||||||
|
[[NSFileManager defaultManager] fileExistsAtPath:folder isDirectory:&isDirectory];
|
||||||
|
}
|
||||||
|
if (!folder) {
|
||||||
|
bool shouldResume = running;
|
||||||
|
[self stop];
|
||||||
|
NSOpenPanel *openPanel = [NSOpenPanel openPanel];
|
||||||
|
openPanel.canChooseFiles = false;
|
||||||
|
openPanel.canChooseDirectories = true;
|
||||||
|
openPanel.message = @"Choose a folder for screenshots";
|
||||||
|
[openPanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
|
||||||
|
if (result == NSModalResponseOK) {
|
||||||
|
[[NSUserDefaults standardUserDefaults] setObject:openPanel.URL.path
|
||||||
|
forKey:@"GBScreenshotFolder"];
|
||||||
|
[self saveScreenshot:sender];
|
||||||
|
}
|
||||||
|
if (shouldResume) {
|
||||||
|
[self start];
|
||||||
|
}
|
||||||
|
|
||||||
|
}];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSImage *image = [self takeScreenshot];
|
||||||
|
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
|
||||||
|
dateFormatter.dateStyle = NSDateFormatterLongStyle;
|
||||||
|
dateFormatter.timeStyle = NSDateFormatterMediumStyle;
|
||||||
|
NSString *filename = [self screenshotFilename];
|
||||||
|
filename = [folder stringByAppendingPathComponent:filename];
|
||||||
|
unsigned i = 2;
|
||||||
|
while ([[NSFileManager defaultManager] fileExistsAtPath:filename]) {
|
||||||
|
filename = [[filename stringByDeletingPathExtension] stringByAppendingFormat:@" %d.png", i++];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
|
||||||
|
NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
||||||
|
[data writeToFile:filename atomically:NO];
|
||||||
|
[self.osdView displayText:@"Screenshot saved"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction)saveScreenshotAs:(id)sender
|
||||||
|
{
|
||||||
|
bool shouldResume = running;
|
||||||
|
[self stop];
|
||||||
|
NSImage *image = [self takeScreenshot];
|
||||||
|
NSSavePanel *savePanel = [NSSavePanel savePanel];
|
||||||
|
[savePanel setNameFieldStringValue:[self screenshotFilename]];
|
||||||
|
[savePanel beginSheetModalForWindow:self.mainWindow completionHandler:^(NSInteger result) {
|
||||||
|
if (result == NSModalResponseOK) {
|
||||||
|
[savePanel orderOut:self];
|
||||||
|
NSBitmapImageRep *imageRep = (NSBitmapImageRep *)image.representations.firstObject;
|
||||||
|
NSData *data = [imageRep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
|
||||||
|
[data writeToURL:savePanel.URL atomically:NO];
|
||||||
|
[[NSUserDefaults standardUserDefaults] setObject:savePanel.URL.path.stringByDeletingLastPathComponent
|
||||||
|
forKey:@"GBScreenshotFolder"];
|
||||||
|
}
|
||||||
|
if (shouldResume) {
|
||||||
|
[self start];
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
[self.osdView displayText:@"Screenshot saved"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction)copyScreenshot:(id)sender
|
||||||
|
{
|
||||||
|
NSImage *image = [self takeScreenshot];
|
||||||
|
[[NSPasteboard generalPasteboard] clearContents];
|
||||||
|
[[NSPasteboard generalPasteboard] writeObjects:@[image]];
|
||||||
|
[self.osdView displayText:@"Screenshot copied"];
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -28,4 +28,5 @@
|
|||||||
@property (nonatomic, weak) IBOutlet NSButton *autoUpdatesCheckbox;
|
@property (nonatomic, weak) IBOutlet NSButton *autoUpdatesCheckbox;
|
||||||
@property (weak) IBOutlet NSSlider *volumeSlider;
|
@property (weak) IBOutlet NSSlider *volumeSlider;
|
||||||
@property (weak) IBOutlet NSButton *OSDCheckbox;
|
@property (weak) IBOutlet NSButton *OSDCheckbox;
|
||||||
|
@property (weak) IBOutlet NSButton *screenshotFilterCheckbox;
|
||||||
@end
|
@end
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
#import "NSString+StringForKey.h"
|
#import "NSString+StringForKey.h"
|
||||||
#import "GBButtons.h"
|
#import "GBButtons.h"
|
||||||
#import "BigSurToolbar.h"
|
#import "BigSurToolbar.h"
|
||||||
|
#import "GBViewMetal.h"
|
||||||
#import <Carbon/Carbon.h>
|
#import <Carbon/Carbon.h>
|
||||||
|
|
||||||
@implementation GBPreferencesWindow
|
@implementation GBPreferencesWindow
|
||||||
@ -32,6 +33,7 @@
|
|||||||
NSSlider *_volumeSlider;
|
NSSlider *_volumeSlider;
|
||||||
NSButton *_autoUpdatesCheckbox;
|
NSButton *_autoUpdatesCheckbox;
|
||||||
NSButton *_OSDCheckbox;
|
NSButton *_OSDCheckbox;
|
||||||
|
NSButton *_screenshotFilterCheckbox;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSArray *)filterList
|
+ (NSArray *)filterList
|
||||||
@ -766,4 +768,27 @@
|
|||||||
forKey:@"GBOSDEnabled"];
|
forKey:@"GBOSDEnabled"];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (IBAction)changeFilterScreenshots:(id)sender
|
||||||
|
{
|
||||||
|
[[NSUserDefaults standardUserDefaults] setBool:[(NSButton *)sender state] == NSOnState
|
||||||
|
forKey:@"GBFilterScreenshots"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSButton *)screenshotFilterCheckbox
|
||||||
|
{
|
||||||
|
return _screenshotFilterCheckbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setScreenshotFilterCheckbox:(NSButton *)screenshotFilterCheckbox
|
||||||
|
{
|
||||||
|
_screenshotFilterCheckbox = screenshotFilterCheckbox;
|
||||||
|
if (![GBViewMetal isSupported]) {
|
||||||
|
[_screenshotFilterCheckbox setEnabled:false];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[_screenshotFilterCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBFilterScreenshots"]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -27,4 +27,5 @@ typedef enum {
|
|||||||
- (uint32_t *)previousBuffer;
|
- (uint32_t *)previousBuffer;
|
||||||
- (void)screenSizeChanged;
|
- (void)screenSizeChanged;
|
||||||
- (void)setRumble: (double)amp;
|
- (void)setRumble: (double)amp;
|
||||||
|
- (NSImage *)renderToImage;
|
||||||
@end
|
@end
|
||||||
|
@ -677,4 +677,10 @@ static const uint8_t workboy_vk_to_key[] = {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSImage *)renderToImage;
|
||||||
|
{
|
||||||
|
/* Not going to support this on OpenGL, OpenGL is too much of a terrible API for me
|
||||||
|
to bother figuring out how the hell something so trivial can be done. */
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
@end
|
@end
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
#import <CoreImage/CoreImage.h>
|
||||||
#import "GBViewMetal.h"
|
#import "GBViewMetal.h"
|
||||||
#pragma clang diagnostic ignored "-Wpartial-availability"
|
#pragma clang diagnostic ignored "-Wpartial-availability"
|
||||||
|
|
||||||
@ -51,8 +52,9 @@ static const vector_float2 rect[] =
|
|||||||
MTKView *view = [[MTKView alloc] initWithFrame:self.frame device:(device = MTLCreateSystemDefaultDevice())];
|
MTKView *view = [[MTKView alloc] initWithFrame:self.frame device:(device = MTLCreateSystemDefaultDevice())];
|
||||||
view.delegate = self;
|
view.delegate = self;
|
||||||
self.internalView = view;
|
self.internalView = view;
|
||||||
view.paused = YES;
|
view.paused = true;
|
||||||
view.enableSetNeedsDisplay = YES;
|
view.enableSetNeedsDisplay = true;
|
||||||
|
view.framebufferOnly = false;
|
||||||
|
|
||||||
vertices = [device newBufferWithBytes:rect
|
vertices = [device newBufferWithBytes:rect
|
||||||
length:sizeof(rect)
|
length:sizeof(rect)
|
||||||
@ -212,4 +214,19 @@ static const vector_float2 rect[] =
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (NSImage *)renderToImage
|
||||||
|
{
|
||||||
|
CIImage *ciImage = [CIImage imageWithMTLTexture:[[(MTKView *)self.internalView currentDrawable] texture]
|
||||||
|
options:@{
|
||||||
|
kCIImageColorSpace: (__bridge_transfer id)CGColorSpaceCreateDeviceRGB()
|
||||||
|
}];
|
||||||
|
ciImage = [ciImage imageByApplyingTransform:CGAffineTransformTranslate(CGAffineTransformMakeScale(1, -1),
|
||||||
|
0, ciImage.extent.size.height)];
|
||||||
|
CIContext *context = [CIContext context];
|
||||||
|
CGImageRef cgImage = [context createCGImage:ciImage fromRect:ciImage.extent];
|
||||||
|
NSImage *ret = [[NSImage alloc] initWithCGImage:cgImage size:self.internalView.bounds.size];
|
||||||
|
CGImageRelease(cgImage);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -316,6 +316,23 @@
|
|||||||
</menu>
|
</menu>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem isSeparatorItem="YES" id="5GS-tt-E0a"/>
|
<menuItem isSeparatorItem="YES" id="5GS-tt-E0a"/>
|
||||||
|
<menuItem title="Save Screenshot" keyEquivalent="s" id="0J3-yf-iXs">
|
||||||
|
<connections>
|
||||||
|
<action selector="saveScreenshot:" target="-1" id="gJd-ml-J8p"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Save Screenshot As…" alternate="YES" keyEquivalent="s" id="98X-Fp-Uny">
|
||||||
|
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="saveScreenshotAs:" target="-1" id="Cxc-Gx-ql1"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem title="Copy Screenshot" keyEquivalent="S" id="vbX-pB-QC8">
|
||||||
|
<connections>
|
||||||
|
<action selector="copyScreenshot:" target="-1" id="XJC-EB-HNl"/>
|
||||||
|
</connections>
|
||||||
|
</menuItem>
|
||||||
|
<menuItem isSeparatorItem="YES" id="zk7-gf-LXN"/>
|
||||||
<menuItem title="Game Boy" tag="1" id="g7C-LA-VAr">
|
<menuItem title="Game Boy" tag="1" id="g7C-LA-VAr">
|
||||||
<modifierMask key="keyEquivalentModifierMask"/>
|
<modifierMask key="keyEquivalentModifierMask"/>
|
||||||
<connections>
|
<connections>
|
||||||
|
@ -90,6 +90,7 @@
|
|||||||
<outlet property="rewindPopupButton" destination="7fg-Ww-JjR" id="Ka2-TP-B1x"/>
|
<outlet property="rewindPopupButton" destination="7fg-Ww-JjR" id="Ka2-TP-B1x"/>
|
||||||
<outlet property="rtcPopupButton" destination="tFf-H1-XUL" id="zxb-4h-aqg"/>
|
<outlet property="rtcPopupButton" destination="tFf-H1-XUL" id="zxb-4h-aqg"/>
|
||||||
<outlet property="rumbleModePopupButton" destination="Ogs-xG-b4b" id="vuw-VN-MTp"/>
|
<outlet property="rumbleModePopupButton" destination="Ogs-xG-b4b" id="vuw-VN-MTp"/>
|
||||||
|
<outlet property="screenshotFilterCheckbox" destination="spQ-Md-OFi" id="f9y-Ek-XQV"/>
|
||||||
<outlet property="sgbPopupButton" destination="dza-T7-RkX" id="B0o-Nb-pIH"/>
|
<outlet property="sgbPopupButton" destination="dza-T7-RkX" id="B0o-Nb-pIH"/>
|
||||||
<outlet property="skipButton" destination="d2I-jU-sLb" id="udX-8K-0sK"/>
|
<outlet property="skipButton" destination="d2I-jU-sLb" id="udX-8K-0sK"/>
|
||||||
<outlet property="temperatureSlider" destination="NuA-mL-AJZ" id="w11-n7-Bmj"/>
|
<outlet property="temperatureSlider" destination="NuA-mL-AJZ" id="w11-n7-Bmj"/>
|
||||||
@ -98,11 +99,11 @@
|
|||||||
<point key="canvasLocation" x="183" y="354"/>
|
<point key="canvasLocation" x="183" y="354"/>
|
||||||
</window>
|
</window>
|
||||||
<customView id="sRK-wO-K6R">
|
<customView id="sRK-wO-K6R">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="399"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="421"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T91-rh-rRp">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T91-rh-rRp">
|
||||||
<rect key="frame" x="18" y="362" width="284" height="17"/>
|
<rect key="frame" x="18" y="384" width="284" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Graphics filter:" id="pXg-WY-8Q5">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Graphics filter:" id="pXg-WY-8Q5">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -111,7 +112,7 @@
|
|||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="6pP-kK-EEC">
|
||||||
<rect key="frame" x="30" y="329" width="262" height="26"/>
|
<rect key="frame" x="30" y="351" width="262" height="26"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Nearest neighbor (Pixelated)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="neN-eo-LA7" id="I1w-05-lGl">
|
<popUpButtonCell key="cell" type="push" title="Nearest neighbor (Pixelated)" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="neN-eo-LA7" id="I1w-05-lGl">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
@ -147,6 +148,17 @@
|
|||||||
<action selector="graphicFilterChanged:" target="QvC-M9-y7g" id="n87-t4-fbV"/>
|
<action selector="graphicFilterChanged:" target="QvC-M9-y7g" id="n87-t4-fbV"/>
|
||||||
</connections>
|
</connections>
|
||||||
</popUpButton>
|
</popUpButton>
|
||||||
|
<button fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="spQ-Md-OFi">
|
||||||
|
<rect key="frame" x="32" y="330" width="259" height="18"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="check" title="Apply filter to screenshots" bezelStyle="regularSquare" imagePosition="left" inset="2" id="JbP-bE-w8A">
|
||||||
|
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="changeFilterScreenshots:" target="QvC-M9-y7g" id="t82-FI-eSe"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wc3-2K-6CD">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Wc3-2K-6CD">
|
||||||
<rect key="frame" x="18" y="307" width="284" height="17"/>
|
<rect key="frame" x="18" y="307" width="284" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
|
||||||
@ -307,7 +319,7 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
</subviews>
|
</subviews>
|
||||||
<point key="canvasLocation" x="-176" y="679.5"/>
|
<point key="canvasLocation" x="-176" y="690.5"/>
|
||||||
</customView>
|
</customView>
|
||||||
<customView id="ymk-46-SX7">
|
<customView id="ymk-46-SX7">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="320" height="375"/>
|
<rect key="frame" x="0.0" y="0.0" width="320" height="375"/>
|
||||||
|
Loading…
Reference in New Issue
Block a user