Skip to content

Commit e69d404

Browse files
Brian Taboneclaude
andcommitted
Implement sprint: data safety, Finder integration, app polish
Six P0 issues from the MacNotePP 1.0 project board: - #11: Tab modified indicator (bullet on dirty tabs, macOS window edited dot) - #8: Save prompt on close/quit (NSAlert with Save/Don't Save/Cancel) - #16: Handle file open from Finder (application:openFiles:, CLI args, dedup) - #12: Multi-file open support (OFN_ALLOWMULTISELECT, null-delimited paths) - #17: Proper .icns app icon and bundle metadata (sips+iconutil pipeline) - #10: About dialog with version, credits, clickable repo link New files: save_prompt.h/mm, about_dialog.h/mm Modified: 12 existing files across platform/, shim/, and CMakeLists.txt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aed399e commit e69d404

16 files changed

Lines changed: 1134 additions & 395 deletions

macos/CMakeLists.txt

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,29 @@ target_link_libraries(win32shim PUBLIC
283283
scintilla_bridge
284284
)
285285

286+
# ============================================================
287+
# App icon generation (.icns from logo.png)
288+
# ============================================================
289+
set(ICONSET_DIR "${CMAKE_BINARY_DIR}/AppIcon.iconset")
290+
add_custom_command(
291+
OUTPUT "${CMAKE_BINARY_DIR}/AppIcon.icns"
292+
COMMAND ${CMAKE_COMMAND} -E make_directory "${ICONSET_DIR}"
293+
COMMAND sips -z 16 16 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_16x16.png"
294+
COMMAND sips -z 32 32 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_16x16@2x.png"
295+
COMMAND sips -z 32 32 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_32x32.png"
296+
COMMAND sips -z 64 64 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_32x32@2x.png"
297+
COMMAND sips -z 128 128 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_128x128.png"
298+
COMMAND sips -z 256 256 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_128x128@2x.png"
299+
COMMAND sips -z 256 256 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_256x256.png"
300+
COMMAND sips -z 512 512 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_256x256@2x.png"
301+
COMMAND sips -z 512 512 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_512x512.png"
302+
COMMAND sips -z 583 583 "${NPP_ROOT}/logo.png" --out "${ICONSET_DIR}/icon_512x512@2x.png"
303+
COMMAND iconutil -c icns "${ICONSET_DIR}" -o "${CMAKE_BINARY_DIR}/AppIcon.icns"
304+
DEPENDS "${NPP_ROOT}/logo.png"
305+
COMMENT "Generating AppIcon.icns from logo.png"
306+
)
307+
add_custom_target(AppIcon DEPENDS "${CMAKE_BINARY_DIR}/AppIcon.icns")
308+
286309
# ============================================================
287310
# Main macOS app target
288311
# ============================================================
@@ -310,6 +333,8 @@ add_executable(MacOSNotePP
310333
"${CMAKE_CURRENT_SOURCE_DIR}/platform/recent_files.mm"
311334
"${CMAKE_CURRENT_SOURCE_DIR}/platform/settings_manager.mm"
312335
"${CMAKE_CURRENT_SOURCE_DIR}/platform/file_monitor_mac.mm"
336+
"${CMAKE_CURRENT_SOURCE_DIR}/platform/save_prompt.mm"
337+
"${CMAKE_CURRENT_SOURCE_DIR}/platform/about_dialog.mm"
313338
)
314339
target_include_directories(MacOSNotePP PRIVATE
315340
"${CMAKE_CURRENT_SOURCE_DIR}/platform"
@@ -335,11 +360,15 @@ target_compile_options(MacOSNotePP PRIVATE
335360
-fobjc-arc
336361
-Wno-deprecated-declarations
337362
)
363+
add_dependencies(MacOSNotePP AppIcon)
338364
add_custom_command(TARGET MacOSNotePP POST_BUILD
339365
COMMAND ${CMAKE_COMMAND} -E copy_if_different
340366
"${NPP_ROOT}/logo.png"
341367
"$<TARGET_FILE_DIR:MacOSNotePP>/logo.png"
342-
COMMENT "Copying logo.png for Dock icon"
368+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
369+
"${CMAKE_BINARY_DIR}/AppIcon.icns"
370+
"$<TARGET_FILE_DIR:MacOSNotePP>/AppIcon.icns"
371+
COMMENT "Copying logo.png and AppIcon.icns"
343372
)
344373

345374
# ============================================================
@@ -375,6 +404,9 @@ add_custom_target(MacOSNotePP_package
375404
COMMAND ${CMAKE_COMMAND} -E copy_if_different
376405
"${NPP_ROOT}/logo.png"
377406
"${NPP_MACOS_APP_RESOURCES}/logo.png"
407+
COMMAND ${CMAKE_COMMAND} -E copy_if_different
408+
"${CMAKE_BINARY_DIR}/AppIcon.icns"
409+
"${NPP_MACOS_APP_RESOURCES}/AppIcon.icns"
378410
COMMAND ${CMAKE_COMMAND} -E copy_if_different
379411
"${NPP_MACOS_INFO_PLIST}"
380412
"${NPP_MACOS_APP_CONTENTS}/Info.plist"

macos/platform/MacNoteInfo.plist.in

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@
99
<key>CFBundleIdentifier</key>
1010
<string>com.softcentral.macnotepp</string>
1111
<key>CFBundleIconFile</key>
12-
<string>logo.png</string>
12+
<string>AppIcon</string>
1313
<key>CFBundleInfoDictionaryVersion</key>
1414
<string>6.0</string>
1515
<key>CFBundleName</key>
1616
<string>MacNote++</string>
17+
<key>CFBundleDisplayName</key>
18+
<string>MacNote++</string>
1719
<key>CFBundlePackageType</key>
1820
<string>APPL</string>
1921
<key>CFBundleShortVersionString</key>
@@ -22,5 +24,11 @@
2224
<string>1.0.0</string>
2325
<key>NSHighResolutionCapable</key>
2426
<true/>
27+
<key>NSHumanReadableCopyright</key>
28+
<string>Copyright © 2024-2026. Based on Notepad++ by Don Ho.</string>
29+
<key>LSMinimumSystemVersion</key>
30+
<string>13.0</string>
31+
<key>LSApplicationCategoryType</key>
32+
<string>public.app-category.developer-tools</string>
2533
</dict>
2634
</plist>

macos/platform/about_dialog.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// about_dialog.h — About dialog
2+
// Part of the Notepad++ macOS port modular refactor.
3+
4+
#pragma once
5+
6+
void showAboutDlg();

macos/platform/about_dialog.mm

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// about_dialog.mm — About dialog
2+
// Part of the Notepad++ macOS port modular refactor.
3+
4+
#import <Cocoa/Cocoa.h>
5+
#include "about_dialog.h"
6+
7+
@interface AboutDialogController : NSObject
8+
- (void)openRepository:(id)sender;
9+
@end
10+
11+
@implementation AboutDialogController
12+
- (void)openRepository:(id)sender
13+
{
14+
[[NSWorkspace sharedWorkspace] openURL:
15+
[NSURL URLWithString:@"https://github.com/hybridmachine/MacOS-NotePP"]];
16+
}
17+
@end
18+
19+
void showAboutDlg()
20+
{
21+
@autoreleasepool {
22+
NSPanel* panel = [[NSPanel alloc]
23+
initWithContentRect:NSMakeRect(0, 0, 400, 360)
24+
styleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskClosable
25+
backing:NSBackingStoreBuffered
26+
defer:NO];
27+
[panel setTitle:@"About MacNote++"];
28+
[panel center];
29+
30+
// Keep the controller alive during modal
31+
AboutDialogController* controller = [[AboutDialogController alloc] init];
32+
33+
NSView* contentView = [panel contentView];
34+
CGFloat y = 310;
35+
36+
// App icon
37+
NSImage* logo = [NSApp applicationIconImage];
38+
if (!logo)
39+
{
40+
NSString* dir = [[[NSBundle mainBundle] executablePath] stringByDeletingLastPathComponent];
41+
logo = [[NSImage alloc] initWithContentsOfFile:[dir stringByAppendingPathComponent:@"logo.png"]];
42+
}
43+
if (logo)
44+
{
45+
NSImageView* iconView = [[NSImageView alloc] initWithFrame:NSMakeRect(168, y - 64, 64, 64)];
46+
[iconView setImage:logo];
47+
[iconView setImageScaling:NSImageScaleProportionallyUpOrDown];
48+
[contentView addSubview:iconView];
49+
}
50+
y -= 80;
51+
52+
// App name
53+
NSTextField* nameLabel = [NSTextField labelWithString:@"MacNote++"];
54+
[nameLabel setFont:[NSFont boldSystemFontOfSize:18]];
55+
[nameLabel setAlignment:NSTextAlignmentCenter];
56+
[nameLabel setFrame:NSMakeRect(20, y, 360, 24)];
57+
[contentView addSubview:nameLabel];
58+
y -= 22;
59+
60+
// Version — read from bundle at runtime
61+
NSString* version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
62+
if (!version) version = @"1.0.0";
63+
NSString* build = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
64+
NSString* versionStr = build && ![build isEqualToString:version]
65+
? [NSString stringWithFormat:@"Version %@ (%@)", version, build]
66+
: [NSString stringWithFormat:@"Version %@", version];
67+
NSTextField* versionLabel = [NSTextField labelWithString:versionStr];
68+
[versionLabel setFont:[NSFont systemFontOfSize:13]];
69+
[versionLabel setTextColor:[NSColor secondaryLabelColor]];
70+
[versionLabel setAlignment:NSTextAlignmentCenter];
71+
[versionLabel setFrame:NSMakeRect(20, y, 360, 18)];
72+
[contentView addSubview:versionLabel];
73+
y -= 28;
74+
75+
// Attribution
76+
NSTextField* creditLabel = [NSTextField labelWithString:@"Based on Notepad++ by Don Ho"];
77+
[creditLabel setFont:[NSFont systemFontOfSize:13]];
78+
[creditLabel setAlignment:NSTextAlignmentCenter];
79+
[creditLabel setFrame:NSMakeRect(20, y, 360, 18)];
80+
[contentView addSubview:creditLabel];
81+
y -= 30;
82+
83+
// Separator
84+
NSBox* separator = [[NSBox alloc] initWithFrame:NSMakeRect(40, y, 320, 1)];
85+
[separator setBoxType:NSBoxSeparator];
86+
[contentView addSubview:separator];
87+
y -= 24;
88+
89+
// Component versions
90+
NSTextField* compLabel = [NSTextField labelWithString:
91+
@"Scintilla 5.5.3 \u2022 Lexilla 5.4.0 \u2022 Boost.Regex 1.90.0"];
92+
[compLabel setFont:[NSFont systemFontOfSize:11]];
93+
[compLabel setTextColor:[NSColor tertiaryLabelColor]];
94+
[compLabel setAlignment:NSTextAlignmentCenter];
95+
[compLabel setFrame:NSMakeRect(20, y, 360, 16)];
96+
[contentView addSubview:compLabel];
97+
y -= 22;
98+
99+
// Build date
100+
NSString* buildDate = [NSString stringWithFormat:@"Built: %s", __DATE__];
101+
NSTextField* buildLabel = [NSTextField labelWithString:buildDate];
102+
[buildLabel setFont:[NSFont systemFontOfSize:11]];
103+
[buildLabel setTextColor:[NSColor tertiaryLabelColor]];
104+
[buildLabel setAlignment:NSTextAlignmentCenter];
105+
[buildLabel setFrame:NSMakeRect(20, y, 360, 16)];
106+
[contentView addSubview:buildLabel];
107+
y -= 28;
108+
109+
// Repository link (clickable button styled as link)
110+
NSButton* linkButton = [[NSButton alloc] initWithFrame:NSMakeRect(60, y, 280, 20)];
111+
[linkButton setTitle:@"github.com/hybridmachine/MacOS-NotePP"];
112+
[linkButton setBezelStyle:NSBezelStyleInline];
113+
[linkButton setBordered:NO];
114+
[linkButton setTarget:controller];
115+
[linkButton setAction:@selector(openRepository:)];
116+
NSMutableAttributedString* linkAttr = [[NSMutableAttributedString alloc]
117+
initWithString:@"github.com/hybridmachine/MacOS-NotePP"];
118+
[linkAttr addAttribute:NSForegroundColorAttributeName
119+
value:[NSColor linkColor]
120+
range:NSMakeRange(0, linkAttr.length)];
121+
[linkAttr addAttribute:NSUnderlineStyleAttributeName
122+
value:@(NSUnderlineStyleSingle)
123+
range:NSMakeRange(0, linkAttr.length)];
124+
[linkAttr addAttribute:NSFontAttributeName
125+
value:[NSFont systemFontOfSize:11]
126+
range:NSMakeRange(0, linkAttr.length)];
127+
[linkButton setAttributedTitle:linkAttr];
128+
[linkButton setAlignment:NSTextAlignmentCenter];
129+
[contentView addSubview:linkButton];
130+
y -= 24;
131+
132+
// License
133+
NSTextField* licenseLabel = [NSTextField labelWithString:@"GNU General Public License v3"];
134+
[licenseLabel setFont:[NSFont systemFontOfSize:11]];
135+
[licenseLabel setTextColor:[NSColor secondaryLabelColor]];
136+
[licenseLabel setAlignment:NSTextAlignmentCenter];
137+
[licenseLabel setFrame:NSMakeRect(20, y, 360, 16)];
138+
[contentView addSubview:licenseLabel];
139+
140+
// OK button
141+
NSButton* okButton = [[NSButton alloc] initWithFrame:NSMakeRect(160, 12, 80, 32)];
142+
[okButton setTitle:@"OK"];
143+
[okButton setBezelStyle:NSBezelStyleRounded];
144+
[okButton setTarget:NSApp];
145+
[okButton setAction:@selector(stopModal)];
146+
[okButton setKeyEquivalent:@"\r"];
147+
[contentView addSubview:okButton];
148+
149+
// Also handle Escape to close
150+
[panel setDefaultButtonCell:[okButton cell]];
151+
152+
[NSApp runModalForWindow:panel];
153+
[panel close];
154+
155+
// prevent ARC from releasing controller during modal
156+
(void)controller;
157+
}
158+
}

macos/platform/app_delegate.mm

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include "string_utils.h"
99
#include "document_manager.h"
1010
#include "file_operations.h"
11+
#include "save_prompt.h"
1112
#include "menu_builder.h"
1213
#include "wndproc.h"
1314
#include "scintilla_config.h"
@@ -65,12 +66,28 @@ - (NSView*)hitTest:(NSPoint)point
6566

6667
static void setDockIconFromLogo()
6768
{
69+
// In a proper app bundle, macOS loads the icon from CFBundleIconFile automatically.
70+
// Check if the bundle icon is already loaded and usable.
71+
NSImage* bundleIcon = [NSApp applicationIconImage];
72+
if (bundleIcon && bundleIcon.size.width > 32)
73+
return;
74+
75+
// Fallback for development builds: load logo.png from alongside the executable
6876
NSString* executablePath = [[NSBundle mainBundle] executablePath];
6977
if (!executablePath)
7078
return;
7179

72-
NSString* logoPath = [[executablePath stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"logo.png"];
73-
NSImage* dockIcon = [[NSImage alloc] initWithContentsOfFile:logoPath];
80+
NSString* dir = [executablePath stringByDeletingLastPathComponent];
81+
82+
// Try .icns first
83+
NSString* icnsPath = [dir stringByAppendingPathComponent:@"AppIcon.icns"];
84+
NSImage* dockIcon = [[NSImage alloc] initWithContentsOfFile:icnsPath];
85+
if (!dockIcon)
86+
{
87+
// Fall back to logo.png
88+
NSString* logoPath = [dir stringByAppendingPathComponent:@"logo.png"];
89+
dockIcon = [[NSImage alloc] initWithContentsOfFile:logoPath];
90+
}
7491
if (dockIcon)
7592
[NSApp setApplicationIconImage:dockIcon];
7693
}
@@ -230,6 +247,26 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification
230247

231248
if (scn->nmhdr.code == 2028) // SCN_FOCUSIN
232249
ctx().activeView = 0;
250+
else if (scn->nmhdr.code == SCN_SAVEPOINTLEFT)
251+
{
252+
int tabIdx = ctx().activeTab;
253+
if (tabIdx >= 0 && tabIdx < static_cast<int>(ctx().documents.size()))
254+
{
255+
ctx().documents[tabIdx].modified = true;
256+
updateTabModifiedIndicator(0, tabIdx);
257+
updateWindowDocumentEdited();
258+
}
259+
}
260+
else if (scn->nmhdr.code == SCN_SAVEPOINTREACHED)
261+
{
262+
int tabIdx = ctx().activeTab;
263+
if (tabIdx >= 0 && tabIdx < static_cast<int>(ctx().documents.size()))
264+
{
265+
ctx().documents[tabIdx].modified = false;
266+
updateTabModifiedIndicator(0, tabIdx);
267+
updateWindowDocumentEdited();
268+
}
269+
}
233270
else if (scn->nmhdr.code == 2010) // SCN_MARGINCLICK
234271
{
235272
if (scn->margin == 1 && ctx().scintillaView)
@@ -353,10 +390,41 @@ - (void)applicationDidFinishLaunching:(NSNotification*)notification
353390

354391
restoreSession();
355392

393+
// Handle CLI arguments (for direct executable launch: ./MacOSNotePP file.txt)
394+
NSArray<NSString*>* args = [[NSProcessInfo processInfo] arguments];
395+
for (NSUInteger i = 1; i < args.count; ++i)
396+
{
397+
NSString* arg = args[i];
398+
if ([arg hasPrefix:@"-"]) continue;
399+
BOOL isDir = NO;
400+
if ([[NSFileManager defaultManager] fileExistsAtPath:arg isDirectory:&isDir] && !isDir)
401+
openFileAtPath(arg);
402+
}
403+
356404
NSLog(@"=== Notepad++ macOS Port — Phase 7 ===");
357405
NSLog(@"Settings, split view, edit commands, encoding, session, drag-and-drop!");
358406
}
359407

408+
- (void)application:(NSApplication*)sender openFiles:(NSArray<NSString*>*)filenames
409+
{
410+
BOOL anyOpened = NO;
411+
for (NSString* path in filenames)
412+
{
413+
BOOL isDir = NO;
414+
if ([[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isDir] && !isDir)
415+
{
416+
if (openFileAtPath(path))
417+
anyOpened = YES;
418+
}
419+
}
420+
421+
[sender activateIgnoringOtherApps:YES];
422+
if (ctx().mainWindow)
423+
[ctx().mainWindow makeKeyAndOrderFront:nil];
424+
425+
[sender replyToOpenOrPrint:anyOpened ? NSApplicationDelegateReplySuccess : NSApplicationDelegateReplyFailure];
426+
}
427+
360428
- (void)appearanceChanged:(NSNotification*)notification
361429
{
362430
dispatch_async(dispatch_get_main_queue(), ^{
@@ -374,6 +442,18 @@ - (void)performContextAction:(NSMenuItem*)sender
374442
SendMessageW(ctx().mainHwnd, WM_COMMAND, MAKEWPARAM(static_cast<WORD>(sender.tag), 0), 0);
375443
}
376444

445+
- (BOOL)windowShouldClose:(NSWindow*)sender
446+
{
447+
return promptAndHandleQuit() ? YES : NO;
448+
}
449+
450+
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender
451+
{
452+
if (promptAndHandleQuit())
453+
return NSTerminateNow;
454+
return NSTerminateCancel;
455+
}
456+
377457
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
378458
{
379459
return YES;

macos/platform/document_manager.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ int addNewTab(const std::wstring& title, const std::string& content,
2020
const std::wstring& filePath = L"", int langIndex = 2);
2121
void closeTabFromView(int viewIndex, int tabIndex);
2222
void closeTab(int tabIndex);
23+
24+
void updateTabModifiedIndicator(int viewIndex, int tabIndex);
25+
void updateWindowDocumentEdited();

0 commit comments

Comments
 (0)