diff --git a/baRSS.xcodeproj/project.pbxproj b/baRSS.xcodeproj/project.pbxproj index 7e8b15f..ec57703 100644 --- a/baRSS.xcodeproj/project.pbxproj +++ b/baRSS.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195885218E1BDB00581B79 /* NSMenu+Ext.m */; }; 541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 541C67C22255470B004D2CE6 /* SettingsAppearance.m */; }; 54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; }; + 54229F552E02491A0019ACB0 /* TinySVG.m in Sources */ = {isa = PBXBuildFile; fileRef = 54229F542E02491A0019ACB0 /* TinySVG.m */; }; 544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; }; 544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; }; 54501010230E9C8600F0B165 /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 5450100F230E9C8600F0B165 /* FeedDownload.m */; }; @@ -127,6 +128,8 @@ 541C67C22255470B004D2CE6 /* SettingsAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsAppearance.m; sourceTree = ""; }; 54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = ""; }; 54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = ""; }; + 54229F532E02491A0019ACB0 /* TinySVG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TinySVG.h; sourceTree = ""; }; + 54229F542E02491A0019ACB0 /* TinySVG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TinySVG.m; sourceTree = ""; }; 544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = ""; }; 544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = ""; }; 544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = ""; }; @@ -429,6 +432,8 @@ 54209E932117325100F3B5EF /* DrawImage.m */, 54910065233A4D4000858AE2 /* URLScheme.h */, 54910066233A4D4000858AE2 /* URLScheme.m */, + 54229F532E02491A0019ACB0 /* TinySVG.h */, + 54229F542E02491A0019ACB0 /* TinySVG.m */, ); path = Helper; sourceTree = ""; @@ -628,6 +633,7 @@ 54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */, 54910067233A4D4000858AE2 /* URLScheme.m in Sources */, 54F6025D21C1D4170006D338 /* OpmlFile.m in Sources */, + 54229F552E02491A0019ACB0 /* TinySVG.m in Sources */, 5496B511214D6275003ED4ED /* UserPrefs.m in Sources */, 546A6A2F22C585580034E806 /* SettingsAboutView.m in Sources */, 54B517072270E990006C1B29 /* NSView+Ext.m in Sources */, diff --git a/baRSS/Constants.h b/baRSS/Constants.h index 9e7d451..6817801 100644 --- a/baRSS/Constants.h +++ b/baRSS/Constants.h @@ -35,6 +35,8 @@ static NSImageName const RSSImageMenuBarIconActive = @"RSSImageMenuBarIconActive static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused"; /// Menu item, unread state icon (blue dot) static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread"; +/// Feed edit, regex editor icon @c "(.*)" +static NSImageName const RSSImageRegexIcon = @"RSSImageRegexIcon"; #pragma mark - NSNotificationName constants diff --git a/baRSS/Helper/DrawImage.m b/baRSS/Helper/DrawImage.m index 53e5353..c5867dc 100644 --- a/baRSS/Helper/DrawImage.m +++ b/baRSS/Helper/DrawImage.m @@ -1,6 +1,7 @@ #import "DrawImage.h" #import "Constants.h" #import "NSColor+Ext.h" +#import "TinySVG.h" @implementation DrawSeparator @@ -125,7 +126,6 @@ static inline void AddGroupIconPath(CGContextRef c, CGFloat size, BOOL showBackg CGPathRelease(lower); } - /** Create @c CGPath for RSS icon; a circle in the lower left bottom and two radio waves going outwards. @param connection If @c NO, draw only one radio wave and a pause icon in the upper right @@ -146,22 +146,9 @@ static inline void AddRSSIconPath(CGContextRef c, CGFloat size, BOOL connection) } -#pragma mark - Icon Background Generators +#pragma mark - Icon Background -/// Create @c CGPath with rounded corners (optional). @param roundness Value between @c 0.0 and @c 1.0 -static void AddRoundedBackgroundPath(CGContextRef c, CGRect r, CGFloat roundness) { - const CGFloat corner = ShorterSide(r.size) * (roundness / 2.0); - if (corner > 0) { - CGMutablePathRef pth = CGPathCreateMutable(); - CGPathAddRoundedRect(pth, NULL, r, corner, corner); - CGContextAddPath(c, pth); - CGPathRelease(pth); - } else { - CGContextAddRect(c, r); - } -} - /// Insert and draw linear gradient with @c color saturation @c ±0.3 static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) { CGFloat h = 0, s = 1, b = 1, a = 1; @@ -190,6 +177,12 @@ static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) { #pragma mark - CGContext Drawing & Manipulation +/// Flip coordinate system +static void FlipCoordinateSystem(CGContextRef c, CGFloat height) { + CGContextTranslateCTM(c, 0, height); + CGContextScaleCTM(c, 1, -1); +} + /// Scale and translate context to the center with respect to the new scale. If @c width @c != @c length align top left. static void SetContentScale(CGContextRef c, CGSize size, CGFloat scale) { const CGFloat s = ShorterSide(size); @@ -204,7 +197,7 @@ static void DrawRoundedFrame(CGContextRef c, CGRect r, CGColorRef color, BOOL ba CGContextSetStrokeColorWithColor(c, color); CGFloat contentScale = defaultScale; if (background) { - AddRoundedBackgroundPath(c, r, corner); + svgAddRect(c, 1, r, ShorterSide(r.size) * corner/2); if (scaling != 0.0) contentScale *= scaling; } @@ -277,6 +270,31 @@ static void DrawUnreadIcon(CGRect r, NSColor *color) { CGPathRelease(path); } +/// Draw "(.*)" as vector path +static void DrawRegexIcon(CGRect r) { + const CGFloat size = ShorterSide(r.size); + CGContextRef c = NSGraphicsContext.currentContext.CGContext; + + svgAddRect(c, 1, r, .2 * size); + CGContextSetFillColorWithColor(c, NSColor.redColor.CGColor); + CGContextFillPath(c); + + // SVG files use bottom-left corner coordinate system. Quartz uses top-left. + FlipCoordinateSystem(c, r.size.height); + SetContentScale(c, r.size, 0.8); + // "(" + svgAddPath(c, size/1000, "m184 187c-140 205-134 432-1 622l-66 44c-159-221-151-499 0-708z"); + // "." + svgAddCircle(c, size/1000, 315, 675, 70, NO); + // "*" + svgAddPath(c, size/1000, "m652 277 107-35 21 63-109 36 68 92-54 39-68-93-66 91-52-41 67-88-109-37 21-63 108 37v-113h66v112z"); + // ")" + svgAddPath(c, size/1000, "m816 813c140-205 134-430 1-621l66-45c159 221 151 499 0 708z"); + + CGContextSetFillColorWithColor(c, NSColor.whiteColor.CGColor); + CGContextFillPath(c); +} + #pragma mark - NSImage Name Registration @@ -297,4 +315,5 @@ void RegisterImageViewNames(void) { Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, YES); return YES; }); Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, NO); return YES; }); Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; }); + Register(32, RSSImageRegexIcon, NSLocalizedString(@"Regex icon", nil), ^(NSRect r) { DrawRegexIcon(r); return YES; }); } diff --git a/baRSS/Helper/TinySVG.h b/baRSS/Helper/TinySVG.h new file mode 100644 index 0000000..ddc5caa --- /dev/null +++ b/baRSS/Helper/TinySVG.h @@ -0,0 +1,6 @@ +@import Cocoa; + +CGMutablePathRef tinySVG_path(CGFloat scale, const char * code); +void svgAddPath(CGContextRef context, CGFloat scale, const char * path); +void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise); +void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius); diff --git a/baRSS/Helper/TinySVG.m b/baRSS/Helper/TinySVG.m new file mode 100644 index 0000000..908c044 --- /dev/null +++ b/baRSS/Helper/TinySVG.m @@ -0,0 +1,174 @@ +#include "TinySVG.h" + + +struct SVGState { + CGFloat scale; // technically not part of parser but easier to pass along + + char op; + float x, y; + bool prevDot; + + float num[6]; + uint8 iNum; + + char buf[15]; + uint8 iBuf; +}; + + +# pragma mark - Helper + +/// if number buffer contains anything, write it to num array and start new buffer +static void finishNum(struct SVGState *state) { + if (state->iBuf > 0) { + state->buf[state->iBuf] = '\0'; + state->num[state->iNum++] = (float)atof(state->buf); + state->iBuf = 0; + state->prevDot = false; + } +} + +/// All numbers stored in num array, finalize SVG path operation and add path to @c CGContext +static void finishOp(CGMutablePathRef path, struct SVGState *state) { + char op = state->op; + if (op >= 'a' && op <= 'z') { + // convert relative to absolute coordinates + for (uint8 t = 0; t < state->iNum; t++) { + state->num[t] += (t % 2 || op == 'v') ? state->y : state->x; + } + // convert to upper-case + op = op - 'a' + 'A'; + } + + if (op == 'Z') { + CGPathCloseSubpath(path); + + } else if (op == 'V' && state->iNum == 1) { + state->y = state->num[0]; + CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale); + + } else if (op == 'H' && state->iNum == 1) { + state->x = state->num[0]; + CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale); + + } else if (op == 'M' && state->iNum == 2) { + state->x = state->num[0]; + state->y = state->num[1]; + CGPathMoveToPoint(path, NULL, state->x * state->scale, state->y * state->scale); + // Edge-case: "M 1 2 3 4 5 6" is valid SVG after move 1,2 all remaining points are lines (3,4 and 5,6) + // For this case we overwrite op here. It will be overwritten again if a new op starts. Else, assume line-op. + state->op = (state->op == 'm') ? 'l' : 'L'; + + } else if (op == 'L' && state->iNum == 2) { + state->x = state->num[0]; + state->y = state->num[1]; + CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale); + + } else if (op == 'C' && state->iNum == 6) { + state->x = state->num[4]; + state->y = state->num[5]; + CGPathAddCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, state->num[2] * state->scale, state->num[3] * state->scale, state->x * state->scale, state->y * state->scale); + } else { + NSLog(@"Unsupported SVG operation %c %d", state->op, state->iNum); + } + state->iNum = 0; +} + +/// current number not finished yet. Append another char to internal buffer +inline static void continueNum(char chr, struct SVGState *state) { + state->buf[state->iBuf++] = chr; +} + + +# pragma mark - Parser + +/// very basic svg path parser. +/// @returns @c CGMutablePathRef which must be released with @c CGPathRelease() +CGMutablePathRef tinySVG_path(CGFloat scale, const char * code) { + CGMutablePathRef path = CGPathCreateMutable(); + + struct SVGState state = { + .scale = scale, + .op = '_', + .x = 0, + .y = 0, + .prevDot = false, + + //.num = {0, 0, 0, 0, 0, 0}, + .iNum = 0, + //.buf = " ", + .iBuf = 0, + }; + + unsigned long len = strlen(code); + for (unsigned long i = 0; i < len; i++) { + char chr = code[i]; + if ((chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')) { + if (state.op != '_') { + finishNum(&state); + finishOp(path, &state); + } + state.op = chr; + } else if (chr >= '0' && chr <= '9') { + continueNum(chr, &state); + } else if (chr == '-' && state.iBuf == 0) { + continueNum(chr, &state); + } else if (chr == '.' && !state.prevDot) { + continueNum(chr, &state); + state.prevDot = true; + } else { // any number-separating character + finishNum(&state); + + // Edge-Case: SVG can reuse the previous operation without declaration + // e.g. you can draw four lines with "L1 2 3 4 5 6 7 8" + // or two curves with "c1 2 3 4 5 6 -1 -2 -3 -4 -5 -6" + // Therefore we need to complete the operation if the number of arguments is reached + if (state.iNum == 1 && strchr("HhVv", state.op) != NULL) { + finishOp(path, &state); + } else if (state.iNum == 2 && strchr("MmLl", state.op) != NULL) { + finishOp(path, &state); + } else if (state.iNum == 6 && strchr("Cc", state.op) != NULL) { + finishOp(path, &state); + } + + if (chr == '-') { + continueNum(chr, &state); + } else if (chr == '.') { + continueNum(chr, &state); + state.prevDot = true; + } + } + } + return path; +} + + +# pragma mark - External API + +/// calls @c tinySVG_path and handles @c CGPath creation and release. +void svgAddPath(CGContextRef context, CGFloat scale, const char * path) { + CGMutablePathRef tmp = tinySVG_path(scale, path); + CGContextAddPath(context, tmp); + CGPathRelease(tmp); +} + +/// calls @c CGPathAddArc with full circle +void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) { + CGMutablePathRef tmp = CGPathCreateMutable(); + CGPathAddArc(tmp, NULL, x * scale, y * scale, radius * scale, 0, M_PI * 2, clockwise); + CGContextAddPath(context, tmp); + CGPathRelease(tmp); +} + +/// Calls @c CGContextAddRect or @c CGPathAddRoundedRect (optional). +/// @param cornerRadius Use @c <=0 for no corners. Use half of @c min(w,h) for a full circle. +void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius) { + if (cornerRadius > 0) { + CGMutablePathRef tmp = CGPathCreateMutable(); + CGPathAddRoundedRect(tmp, NULL, rect, cornerRadius * scale, cornerRadius * scale); + CGContextAddPath(context, tmp); + CGPathRelease(tmp); + } else { + CGContextAddRect(context, rect); + } +}