- I made both surfaces friendlier to screen readers.
CMS changes landed in [layout.php](/Users/tyemeclifford/Documents/GH/blog/themes/neon/layout.php), [style.css](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/style.css), [app.js](/Users/tyemeclifford/Documents/GH/blog/themes/neon/assets/app.js), and [lone-embed.php](/Users/tyemeclifford/Documents/GH/blog/lone-embed.php): skip link, named search/forms, current page state, live notices/empty states, labelled media/embed controls, visible focus states, theme control pressed states, and a focus-trapped/lightbox dialog with proper ARIA. Mac app changes landed in [main.m](/Users/tyemeclifford/Documents/GH/blog/macclient/AppKitClient/main.m), and I rebuilt [TCMS](/Users/tyemeclifford/Documents/GH/blog/macclient/TCMS.app/Contents/MacOS/TCMS): AppKit controls/tables/fields/previews now get accessibility labels/help/value text, workspace sections are named, media/comment tables expose useful context, and status messages are announced via macOS accessibility notifications.
This commit is contained in:
+150
-22
@@ -60,6 +60,25 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
: [NSString stringWithFormat:@"%.1f %@", amount, units[unitIndex]];
|
||||
}
|
||||
|
||||
static void TCMSSetAccessibility(NSView *view, NSString *label, NSString *help) {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
if (label.length > 0) {
|
||||
[view setAccessibilityLabel:label];
|
||||
}
|
||||
if (help.length > 0) {
|
||||
[view setAccessibilityHelp:help];
|
||||
}
|
||||
}
|
||||
|
||||
static void TCMSSetAccessibilityValue(NSView *view, NSString *value) {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
[view setAccessibilityValue:value ?: @""];
|
||||
}
|
||||
|
||||
@interface TCMSMarkdownTextView : NSTextView
|
||||
@end
|
||||
|
||||
@@ -383,6 +402,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.window.minSize = NSMakeSize(1040, 680);
|
||||
self.window.delegate = self;
|
||||
self.window.releasedWhenClosed = NO;
|
||||
TCMSSetAccessibility(self.window.contentView, @"TCMS workspace", @"Manage posts, pages, media, comments, and settings.");
|
||||
[self.window center];
|
||||
|
||||
NSView *content = self.window.contentView;
|
||||
@@ -390,10 +410,12 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
sidebar.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin;
|
||||
sidebar.wantsLayer = YES;
|
||||
sidebar.layer.backgroundColor = [NSColor colorWithCalibratedWhite:0.08 alpha:1].CGColor;
|
||||
TCMSSetAccessibility(sidebar, @"Navigation and status", @"Choose a TCMS section and hear current status messages.");
|
||||
[content addSubview:sidebar];
|
||||
|
||||
NSTextField *brand = [self label:@"TCMS" frame:NSMakeRect(18, 680, 154, 34) font:[NSFont boldSystemFontOfSize:28]];
|
||||
brand.textColor = NSColor.whiteColor;
|
||||
TCMSSetAccessibility(brand, @"TCMS", @"Ty Clifford's Content Management System.");
|
||||
[sidebar addSubview:brand];
|
||||
|
||||
NSArray *buttons = @[
|
||||
@@ -410,6 +432,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
button.frame = NSMakeRect(18, y, 154, 36);
|
||||
button.bezelStyle = NSBezelStyleTexturedRounded;
|
||||
button.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(button, [NSString stringWithFormat:@"Show %@", entry[0]], [NSString stringWithFormat:@"Switch to the %@ section.", entry[0]]);
|
||||
[sidebar addSubview:button];
|
||||
y -= 46;
|
||||
}
|
||||
@@ -418,10 +441,13 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.statusLabel.textColor = [NSColor colorWithCalibratedWhite:0.78 alpha:1];
|
||||
self.statusLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
self.statusLabel.autoresizingMask = NSViewMaxYMargin;
|
||||
TCMSSetAccessibility(self.statusLabel, @"Status", @"Latest TCMS status message.");
|
||||
TCMSSetAccessibilityValue(self.statusLabel, @"Ready");
|
||||
[sidebar addSubview:self.statusLabel];
|
||||
|
||||
self.workspace = [[NSView alloc] initWithFrame:NSMakeRect(190, 0, 990, 740)];
|
||||
self.workspace.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
TCMSSetAccessibility(self.workspace, @"Posts section", @"Main TCMS editing workspace.");
|
||||
[content addSubview:self.workspace];
|
||||
[self.window makeKeyAndOrderFront:nil];
|
||||
}
|
||||
@@ -504,6 +530,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
label.bezeled = NO;
|
||||
label.drawsBackground = NO;
|
||||
label.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(label, text, nil);
|
||||
return label;
|
||||
}
|
||||
|
||||
@@ -512,6 +539,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
field.placeholderString = placeholder;
|
||||
field.delegate = self;
|
||||
field.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(field, placeholder, nil);
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -546,6 +574,24 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.mediaPreviewURLString = nil;
|
||||
}
|
||||
|
||||
- (void)announceAccessibilityMessage:(NSString *)message {
|
||||
if (message.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *info = @{
|
||||
NSAccessibilityAnnouncementKey: message,
|
||||
NSAccessibilityPriorityKey: @(NSAccessibilityPriorityMedium)
|
||||
};
|
||||
NSAccessibilityPostNotificationWithUserInfo(self.window ?: NSApp, NSAccessibilityAnnouncementRequestedNotification, info);
|
||||
}
|
||||
|
||||
- (void)configureWorkspaceAccessibilityWithTitle:(NSString *)title heading:(NSView *)heading {
|
||||
NSString *label = [NSString stringWithFormat:@"%@ section", title.length > 0 ? title : @"TCMS"];
|
||||
TCMSSetAccessibility(self.workspace, label, @"Main TCMS workspace.");
|
||||
NSAccessibilityPostNotification(heading ?: self.workspace, NSAccessibilityLayoutChangedNotification);
|
||||
}
|
||||
|
||||
- (void)showContentSection:(NSString *)section {
|
||||
self.section = section;
|
||||
[self clearWorkspace];
|
||||
@@ -554,36 +600,51 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
|
||||
NSTextField *heading = [self label:[section capitalizedString] frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]];
|
||||
[self.workspace addSubview:heading];
|
||||
[self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)];
|
||||
[self addButton:@"New" frame:NSMakeRect(296, 690, 58, 30) action:@selector(newContent)];
|
||||
[self addButton:@"Draft" frame:NSMakeRect(362, 690, 70, 30) action:@selector(saveDraft)];
|
||||
[self addButton:@"Update" frame:NSMakeRect(440, 690, 76, 30) action:@selector(updateContent)];
|
||||
[self addButton:@"Publish" frame:NSMakeRect(524, 690, 78, 30) action:@selector(publishContent)];
|
||||
[self addButton:@"Upload Media" frame:NSMakeRect(610, 690, 118, 30) action:@selector(uploadEditorMedia)];
|
||||
[self addButton:@"Delete" frame:NSMakeRect(736, 690, 70, 30) action:@selector(deleteContent)];
|
||||
[self configureWorkspaceAccessibilityWithTitle:[section capitalizedString] heading:heading];
|
||||
NSButton *refreshButton = [self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)];
|
||||
NSButton *newButton = [self addButton:@"New" frame:NSMakeRect(296, 690, 58, 30) action:@selector(newContent)];
|
||||
NSButton *draftButton = [self addButton:@"Draft" frame:NSMakeRect(362, 690, 70, 30) action:@selector(saveDraft)];
|
||||
NSButton *updateButton = [self addButton:@"Update" frame:NSMakeRect(440, 690, 76, 30) action:@selector(updateContent)];
|
||||
NSButton *publishButton = [self addButton:@"Publish" frame:NSMakeRect(524, 690, 78, 30) action:@selector(publishContent)];
|
||||
NSButton *uploadMediaButton = [self addButton:@"Upload Media" frame:NSMakeRect(610, 690, 118, 30) action:@selector(uploadEditorMedia)];
|
||||
NSButton *deleteButton = [self addButton:@"Delete" frame:NSMakeRect(736, 690, 70, 30) action:@selector(deleteContent)];
|
||||
TCMSSetAccessibility(refreshButton, [NSString stringWithFormat:@"Refresh %@ list", section], nil);
|
||||
TCMSSetAccessibility(newButton, [NSString stringWithFormat:@"Create new %@", type], nil);
|
||||
TCMSSetAccessibility(draftButton, @"Save current content as draft", nil);
|
||||
TCMSSetAccessibility(updateButton, @"Update current content", nil);
|
||||
TCMSSetAccessibility(publishButton, @"Publish current content", nil);
|
||||
TCMSSetAccessibility(uploadMediaButton, @"Upload media and insert it in the editor", nil);
|
||||
TCMSSetAccessibility(deleteButton, @"Delete current content", @"Deletes the selected post or page after confirmation.");
|
||||
|
||||
NSScrollView *listScroll = [self buildTableWithFrame:NSMakeRect(20, 56, 280, 614) columns:@[@"title", @"status"]];
|
||||
listScroll.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin;
|
||||
TCMSSetAccessibility(listScroll, [NSString stringWithFormat:@"%@ list", [section capitalizedString]], @"Select a row to load it into the editor.");
|
||||
TCMSSetAccessibility(self.tableView, [NSString stringWithFormat:@"%@ table", [section capitalizedString]], @"Select a row to load it into the editor.");
|
||||
self.previousPageButton = [NSButton buttonWithTitle:@"Previous" target:self action:@selector(previousContentPage)];
|
||||
self.previousPageButton.frame = NSMakeRect(20, 20, 78, 28);
|
||||
self.previousPageButton.bezelStyle = NSBezelStyleRounded;
|
||||
self.previousPageButton.autoresizingMask = NSViewMaxYMargin;
|
||||
TCMSSetAccessibility(self.previousPageButton, @"Previous page", [NSString stringWithFormat:@"Show the previous page of %@.", section]);
|
||||
[self.workspace addSubview:self.previousPageButton];
|
||||
|
||||
self.pageInfoLabel = [self label:@"1 / 1" frame:NSMakeRect(104, 24, 66, 20) font:[NSFont systemFontOfSize:12]];
|
||||
self.pageInfoLabel.alignment = NSTextAlignmentCenter;
|
||||
self.pageInfoLabel.autoresizingMask = NSViewMaxYMargin;
|
||||
TCMSSetAccessibility(self.pageInfoLabel, @"Page", @"Current list page.");
|
||||
TCMSSetAccessibilityValue(self.pageInfoLabel, @"Page 1 of 1");
|
||||
[self.workspace addSubview:self.pageInfoLabel];
|
||||
|
||||
self.nextPageButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(nextContentPage)];
|
||||
self.nextPageButton.frame = NSMakeRect(176, 20, 54, 28);
|
||||
self.nextPageButton.bezelStyle = NSBezelStyleRounded;
|
||||
self.nextPageButton.autoresizingMask = NSViewMaxYMargin;
|
||||
TCMSSetAccessibility(self.nextPageButton, @"Next page", [NSString stringWithFormat:@"Show the next page of %@.", section]);
|
||||
[self.workspace addSubview:self.nextPageButton];
|
||||
|
||||
self.perPagePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(236, 20, 64, 28)];
|
||||
self.perPagePopup.autoresizingMask = NSViewMaxYMargin;
|
||||
self.perPagePopup.toolTip = @"Items per page";
|
||||
TCMSSetAccessibility(self.perPagePopup, @"Items per page", [NSString stringWithFormat:@"Choose how many %@ to show per page.", section]);
|
||||
self.perPagePopup.target = self;
|
||||
self.perPagePopup.action = @selector(contentPerPageChanged:);
|
||||
[self.perPagePopup addItemsWithTitles:@[@"10", @"25", @"50", @"100"]];
|
||||
@@ -602,9 +663,11 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.titleField.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
||||
self.statusPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(left + width - 220, 646, 100, 28)];
|
||||
self.statusPopup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.statusPopup, @"Status", @"Choose draft or published.");
|
||||
[self.statusPopup addItemsWithTitles:@[@"draft", @"published"]];
|
||||
self.typePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(left + width - 110, 646, 90, 28)];
|
||||
self.typePopup.autoresizingMask = NSViewMinXMargin | NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.typePopup, @"Content type", @"Choose post or page.");
|
||||
[self.typePopup addItemsWithTitles:@[@"post", @"page"]];
|
||||
[self.workspace addSubview:self.titleField];
|
||||
[self.workspace addSubview:self.statusPopup];
|
||||
@@ -625,12 +688,15 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.allowCommentsButton = [NSButton checkboxWithTitle:@"Allow comments" target:self action:@selector(markDirty)];
|
||||
self.allowCommentsButton.frame = NSMakeRect(left, 460, 180, 24);
|
||||
self.allowCommentsButton.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.allowCommentsButton, @"Allow comments", @"Allow readers to leave comments on this content.");
|
||||
self.spellCheckButton = [NSButton checkboxWithTitle:@"Spell check" target:self action:@selector(editorOptionChanged:)];
|
||||
self.spellCheckButton.frame = NSMakeRect(left + 190, 460, 120, 24);
|
||||
self.spellCheckButton.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.spellCheckButton, @"Spell check", @"Toggle continuous spell checking in the Markdown editor.");
|
||||
self.autoCorrectButton = [NSButton checkboxWithTitle:@"Auto-correct" target:self action:@selector(editorOptionChanged:)];
|
||||
self.autoCorrectButton.frame = NSMakeRect(left + 320, 460, 130, 24);
|
||||
self.autoCorrectButton.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.autoCorrectButton, @"Auto-correct", @"Toggle automatic corrections in the Markdown editor.");
|
||||
for (NSView *view in @[self.slugField, self.categoryField, self.tagsField, self.summaryField, self.authorField, self.coverField, self.allowCommentsButton, self.spellCheckButton, self.autoCorrectButton]) {
|
||||
[self.workspace addSubview:view];
|
||||
}
|
||||
@@ -641,6 +707,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.editorScroll.hasHorizontalScroller = NO;
|
||||
self.editorScroll.autohidesScrollers = YES;
|
||||
self.editorScroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
TCMSSetAccessibility(self.editorScroll, @"Markdown editor", @"Edit the Markdown body for the current post or page.");
|
||||
self.markdownView = [[TCMSMarkdownTextView alloc] initWithFrame:self.editorScroll.contentView.bounds];
|
||||
self.markdownView.font = [NSFont fontWithName:@"Menlo" size:13] ?: [NSFont monospacedSystemFontOfSize:13 weight:NSFontWeightRegular];
|
||||
self.markdownView.allowsUndo = YES;
|
||||
@@ -651,6 +718,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.markdownView.textContainer.heightTracksTextView = NO;
|
||||
self.markdownView.textContainerInset = NSMakeSize(8, 8);
|
||||
self.markdownView.delegate = self;
|
||||
TCMSSetAccessibility(self.markdownView, @"Markdown editor", @"Edit the Markdown body for the current post or page.");
|
||||
self.editorScroll.documentView = self.markdownView;
|
||||
[self.workspace addSubview:self.editorScroll];
|
||||
[self resizeEditorTextView];
|
||||
@@ -669,18 +737,27 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.section = @"media";
|
||||
[self clearWorkspace];
|
||||
self.contentListPage = 1;
|
||||
[self.workspace addSubview:[self label:@"Media" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]]];
|
||||
[self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)];
|
||||
[self addButton:@"Upload" frame:NSMakeRect(296, 690, 78, 30) action:@selector(uploadMedia)];
|
||||
NSTextField *heading = [self label:@"Media" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]];
|
||||
[self.workspace addSubview:heading];
|
||||
[self configureWorkspaceAccessibilityWithTitle:@"Media" heading:heading];
|
||||
NSButton *refreshButton = [self addButton:@"Refresh" frame:NSMakeRect(210, 690, 78, 30) action:@selector(refreshCurrentSection)];
|
||||
NSButton *uploadButton = [self addButton:@"Upload" frame:NSMakeRect(296, 690, 78, 30) action:@selector(uploadMedia)];
|
||||
self.openMediaButton = [self addButton:@"Open" frame:NSMakeRect(382, 690, 66, 30) action:@selector(openSelectedMedia:)];
|
||||
self.openMediaButton.enabled = NO;
|
||||
self.mediaURLCopyButton = [self addButton:@"Copy URL" frame:NSMakeRect(456, 690, 88, 30) action:@selector(copySelectedMediaURL:)];
|
||||
self.mediaURLCopyButton.enabled = NO;
|
||||
self.mediaEmbedCopyButton = [self addButton:@"Copy Embed" frame:NSMakeRect(552, 690, 104, 30) action:@selector(copySelectedMediaEmbed:)];
|
||||
self.mediaEmbedCopyButton.enabled = NO;
|
||||
TCMSSetAccessibility(refreshButton, @"Refresh media list", nil);
|
||||
TCMSSetAccessibility(uploadButton, @"Upload media", @"Upload a file to the TCMS media library.");
|
||||
TCMSSetAccessibility(self.openMediaButton, @"Open selected media", @"Open the selected media item in the default app.");
|
||||
TCMSSetAccessibility(self.mediaURLCopyButton, @"Copy selected media URL", nil);
|
||||
TCMSSetAccessibility(self.mediaEmbedCopyButton, @"Copy Markdown embed for selected media", nil);
|
||||
|
||||
NSScrollView *mediaScroll = [self buildTableWithFrame:NSMakeRect(20, 56, 420, 614) columns:@[@"name", @"mime", @"size"]];
|
||||
mediaScroll.autoresizingMask = NSViewHeightSizable | NSViewMaxXMargin;
|
||||
TCMSSetAccessibility(mediaScroll, @"Media list", @"Select a row to preview media.");
|
||||
TCMSSetAccessibility(self.tableView, @"Media table", @"Select a row to preview media.");
|
||||
self.tableView.target = self;
|
||||
self.tableView.doubleAction = @selector(openSelectedMedia:);
|
||||
[self configureMediaContextMenu];
|
||||
@@ -689,22 +766,27 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.previousPageButton.frame = NSMakeRect(20, 20, 78, 28);
|
||||
self.previousPageButton.bezelStyle = NSBezelStyleRounded;
|
||||
self.previousPageButton.autoresizingMask = NSViewMaxYMargin;
|
||||
TCMSSetAccessibility(self.previousPageButton, @"Previous media page", nil);
|
||||
[self.workspace addSubview:self.previousPageButton];
|
||||
|
||||
self.pageInfoLabel = [self label:@"1 / 1" frame:NSMakeRect(104, 24, 66, 20) font:[NSFont systemFontOfSize:12]];
|
||||
self.pageInfoLabel.alignment = NSTextAlignmentCenter;
|
||||
self.pageInfoLabel.autoresizingMask = NSViewMaxYMargin;
|
||||
TCMSSetAccessibility(self.pageInfoLabel, @"Page", @"Current media list page.");
|
||||
TCMSSetAccessibilityValue(self.pageInfoLabel, @"Page 1 of 1");
|
||||
[self.workspace addSubview:self.pageInfoLabel];
|
||||
|
||||
self.nextPageButton = [NSButton buttonWithTitle:@"Next" target:self action:@selector(nextContentPage)];
|
||||
self.nextPageButton.frame = NSMakeRect(176, 20, 54, 28);
|
||||
self.nextPageButton.bezelStyle = NSBezelStyleRounded;
|
||||
self.nextPageButton.autoresizingMask = NSViewMaxYMargin;
|
||||
TCMSSetAccessibility(self.nextPageButton, @"Next media page", nil);
|
||||
[self.workspace addSubview:self.nextPageButton];
|
||||
|
||||
self.perPagePopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(236, 20, 64, 28)];
|
||||
self.perPagePopup.autoresizingMask = NSViewMaxYMargin;
|
||||
self.perPagePopup.toolTip = @"Items per page";
|
||||
TCMSSetAccessibility(self.perPagePopup, @"Media items per page", nil);
|
||||
self.perPagePopup.target = self;
|
||||
self.perPagePopup.action = @selector(contentPerPageChanged:);
|
||||
[self.perPagePopup addItemsWithTitles:@[@"10", @"25", @"50", @"100"]];
|
||||
@@ -723,16 +805,20 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.mediaPreviewContainer.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.mediaPreviewContainer.wantsLayer = YES;
|
||||
self.mediaPreviewContainer.layer.backgroundColor = [NSColor colorWithCalibratedWhite:0.11 alpha:1].CGColor;
|
||||
TCMSSetAccessibility(self.mediaPreviewContainer, @"Media preview", @"Preview for the selected media item.");
|
||||
[self.workspace addSubview:self.mediaPreviewContainer];
|
||||
|
||||
self.mediaPreviewLabel = [self label:@"No media selected" frame:NSMakeRect(16, 612, previewWidth - 32, 24) font:[NSFont boldSystemFontOfSize:15]];
|
||||
self.mediaPreviewLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
||||
self.mediaPreviewLabel.textColor = [NSColor colorWithCalibratedWhite:0.92 alpha:1];
|
||||
TCMSSetAccessibility(self.mediaPreviewLabel, @"Selected media", nil);
|
||||
TCMSSetAccessibilityValue(self.mediaPreviewLabel, @"No media selected");
|
||||
[self.mediaPreviewContainer addSubview:self.mediaPreviewLabel];
|
||||
|
||||
self.mediaDetailLabel = [self label:@"" frame:NSMakeRect(16, 584, previewWidth - 32, 20) font:[NSFont systemFontOfSize:12]];
|
||||
self.mediaDetailLabel.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
||||
self.mediaDetailLabel.textColor = [NSColor colorWithCalibratedWhite:0.72 alpha:1];
|
||||
TCMSSetAccessibility(self.mediaDetailLabel, @"Media details", nil);
|
||||
[self.mediaPreviewContainer addSubview:self.mediaDetailLabel];
|
||||
|
||||
NSRect previewFrame = NSMakeRect(16, 16, previewWidth - 32, 552);
|
||||
@@ -740,12 +826,14 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.mediaImageView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.mediaImageView.imageScaling = NSImageScaleProportionallyUpOrDown;
|
||||
self.mediaImageView.hidden = YES;
|
||||
TCMSSetAccessibility(self.mediaImageView, @"Image preview", @"Preview image for the selected media item.");
|
||||
[self.mediaPreviewContainer addSubview:self.mediaImageView];
|
||||
|
||||
self.mediaPlayerView = [[AVPlayerView alloc] initWithFrame:previewFrame];
|
||||
self.mediaPlayerView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
self.mediaPlayerView.controlsStyle = AVPlayerViewControlsStyleDefault;
|
||||
self.mediaPlayerView.hidden = YES;
|
||||
TCMSSetAccessibility(self.mediaPlayerView, @"Media player", @"Play the selected audio or video item.");
|
||||
[self.mediaPreviewContainer addSubview:self.mediaPlayerView];
|
||||
|
||||
[self resetMediaPreview];
|
||||
@@ -756,18 +844,28 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
- (void)showCommentsSection {
|
||||
self.section = @"comments";
|
||||
[self clearWorkspace];
|
||||
[self.workspace addSubview:[self label:@"Comments" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]]];
|
||||
NSTextField *heading = [self label:@"Comments" frame:NSMakeRect(20, 690, 180, 28) font:[NSFont boldSystemFontOfSize:24]];
|
||||
[self.workspace addSubview:heading];
|
||||
[self configureWorkspaceAccessibilityWithTitle:@"Comments" heading:heading];
|
||||
self.commentStatusPopup = [[NSPopUpButton alloc] initWithFrame:NSMakeRect(210, 690, 120, 30)];
|
||||
self.commentStatusPopup.autoresizingMask = NSViewMinYMargin;
|
||||
[self.commentStatusPopup addItemsWithTitles:@[@"pending", @"approved", @"spam", @"all"]];
|
||||
TCMSSetAccessibility(self.commentStatusPopup, @"Comment status filter", @"Choose which comments to list.");
|
||||
[self.workspace addSubview:self.commentStatusPopup];
|
||||
[self addButton:@"Refresh" frame:NSMakeRect(340, 690, 90, 30) action:@selector(refreshCurrentSection)];
|
||||
[self addButton:@"Approve" frame:NSMakeRect(440, 690, 90, 30) action:@selector(approveComment)];
|
||||
[self addButton:@"Pending" frame:NSMakeRect(540, 690, 90, 30) action:@selector(pendingComment)];
|
||||
[self addButton:@"Spam" frame:NSMakeRect(640, 690, 80, 30) action:@selector(spamComment)];
|
||||
[self addButton:@"Delete" frame:NSMakeRect(730, 690, 80, 30) action:@selector(deleteComment)];
|
||||
NSButton *refreshButton = [self addButton:@"Refresh" frame:NSMakeRect(340, 690, 90, 30) action:@selector(refreshCurrentSection)];
|
||||
NSButton *approveButton = [self addButton:@"Approve" frame:NSMakeRect(440, 690, 90, 30) action:@selector(approveComment)];
|
||||
NSButton *pendingButton = [self addButton:@"Pending" frame:NSMakeRect(540, 690, 90, 30) action:@selector(pendingComment)];
|
||||
NSButton *spamButton = [self addButton:@"Spam" frame:NSMakeRect(640, 690, 80, 30) action:@selector(spamComment)];
|
||||
NSButton *deleteButton = [self addButton:@"Delete" frame:NSMakeRect(730, 690, 80, 30) action:@selector(deleteComment)];
|
||||
TCMSSetAccessibility(refreshButton, @"Refresh comments", nil);
|
||||
TCMSSetAccessibility(approveButton, @"Approve selected comment", nil);
|
||||
TCMSSetAccessibility(pendingButton, @"Mark selected comment pending", nil);
|
||||
TCMSSetAccessibility(spamButton, @"Mark selected comment as spam", nil);
|
||||
TCMSSetAccessibility(deleteButton, @"Delete selected comment", @"Deletes the selected comment from the server.");
|
||||
NSScrollView *commentsScroll = [self buildTableWithFrame:NSMakeRect(20, 20, self.workspace.bounds.size.width - 40, 650) columns:@[@"author", @"status", @"slug", @"body"]];
|
||||
commentsScroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
TCMSSetAccessibility(commentsScroll, @"Comments list", @"Select a comment row before moderating it.");
|
||||
TCMSSetAccessibility(self.tableView, @"Comments table", @"Select a comment row before moderating it.");
|
||||
[self refreshComments];
|
||||
[self refreshWindowLayout];
|
||||
}
|
||||
@@ -775,7 +873,9 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
- (void)showSettingsSection {
|
||||
self.section = @"settings";
|
||||
[self clearWorkspace];
|
||||
[self.workspace addSubview:[self label:@"Settings" frame:NSMakeRect(20, 690, 220, 28) font:[NSFont boldSystemFontOfSize:24]]];
|
||||
NSTextField *heading = [self label:@"Settings" frame:NSMakeRect(20, 690, 220, 28) font:[NSFont boldSystemFontOfSize:24]];
|
||||
[self.workspace addSubview:heading];
|
||||
[self configureWorkspaceAccessibilityWithTitle:@"Settings" heading:heading];
|
||||
[self.workspace addSubview:[self label:@"TCMS URL" frame:NSMakeRect(20, 640, 140, 24) font:[NSFont systemFontOfSize:13]]];
|
||||
[self.workspace addSubview:[self label:@"Username" frame:NSMakeRect(20, 596, 140, 24) font:[NSFont systemFontOfSize:13]]];
|
||||
[self.workspace addSubview:[self label:@"Password" frame:NSMakeRect(20, 552, 140, 24) font:[NSFont systemFontOfSize:13]]];
|
||||
@@ -783,22 +883,29 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
|
||||
self.siteField = [self field:@"https://example.com/blog" frame:NSMakeRect(170, 638, 520, 28)];
|
||||
self.siteField.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.siteField, @"TCMS URL", @"Base URL for the TCMS site.");
|
||||
self.usernameField = [self field:@"admin" frame:NSMakeRect(170, 594, 260, 28)];
|
||||
self.usernameField.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.usernameField, @"Username", @"Username for the TCMS API.");
|
||||
self.passwordField = [[NSSecureTextField alloc] initWithFrame:NSMakeRect(170, 550, 260, 28)];
|
||||
self.passwordField.delegate = self;
|
||||
self.passwordField.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.passwordField, @"Password", @"Password for the TCMS API.");
|
||||
self.autosaveField = [self field:@"30" frame:NSMakeRect(170, 506, 120, 28)];
|
||||
TCMSSetAccessibility(self.autosaveField, @"Autosave seconds", @"Minimum autosave interval is 10 seconds.");
|
||||
self.autosaveField.autoresizingMask = NSViewMinYMargin;
|
||||
self.remoteAutosaveButton = [NSButton checkboxWithTitle:@"Also save remotely as draft" target:nil action:nil];
|
||||
self.remoteAutosaveButton.frame = NSMakeRect(170, 464, 260, 24);
|
||||
self.remoteAutosaveButton.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(self.remoteAutosaveButton, @"Also save remotely as draft", @"When enabled, autosave sends a draft copy to the server.");
|
||||
for (NSView *view in @[self.siteField, self.usernameField, self.passwordField, self.autosaveField, self.remoteAutosaveButton]) {
|
||||
[self.workspace addSubview:view];
|
||||
}
|
||||
|
||||
[self addButton:@"Save Settings" frame:NSMakeRect(170, 414, 120, 32) action:@selector(saveSettings)];
|
||||
[self addButton:@"Test Auth" frame:NSMakeRect(300, 414, 100, 32) action:@selector(testConnection)];
|
||||
NSButton *saveButton = [self addButton:@"Save Settings" frame:NSMakeRect(170, 414, 120, 32) action:@selector(saveSettings)];
|
||||
NSButton *testButton = [self addButton:@"Test Auth" frame:NSMakeRect(300, 414, 100, 32) action:@selector(testConnection)];
|
||||
TCMSSetAccessibility(saveButton, @"Save settings", nil);
|
||||
TCMSSetAccessibility(testButton, @"Test authentication", @"Save settings, then test the TCMS API credentials.");
|
||||
[self populateSettingsFields];
|
||||
[self wireSettingsKeyLoop];
|
||||
[self refreshWindowLayout];
|
||||
@@ -809,6 +916,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
button.frame = frame;
|
||||
button.bezelStyle = NSBezelStyleRounded;
|
||||
button.autoresizingMask = NSViewMinYMargin;
|
||||
TCMSSetAccessibility(button, title, nil);
|
||||
[self.workspace addSubview:button];
|
||||
return button;
|
||||
}
|
||||
@@ -821,6 +929,8 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.tableView.delegate = self;
|
||||
self.tableView.dataSource = self;
|
||||
self.tableView.columnAutoresizingStyle = NSTableViewUniformColumnAutoresizingStyle;
|
||||
TCMSSetAccessibility(scroll, @"Table", @"Use arrow keys to move through rows.");
|
||||
TCMSSetAccessibility(self.tableView, @"Table", @"Use arrow keys to move through rows.");
|
||||
for (NSString *identifier in columns) {
|
||||
NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:identifier];
|
||||
column.title = [identifier capitalizedString];
|
||||
@@ -854,13 +964,16 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
|
||||
[self.tableView deselectAll:nil];
|
||||
[self.tableView reloadData];
|
||||
self.pageInfoLabel.stringValue = total == 0 ? @"0 / 0" : [NSString stringWithFormat:@"%ld / %ld", (long)self.contentListPage, (long)totalPages];
|
||||
NSString *pageText = total == 0 ? @"0 / 0" : [NSString stringWithFormat:@"%ld / %ld", (long)self.contentListPage, (long)totalPages];
|
||||
self.pageInfoLabel.stringValue = pageText;
|
||||
TCMSSetAccessibilityValue(self.pageInfoLabel, total == 0 ? @"No pages" : [NSString stringWithFormat:@"Page %ld of %ld", (long)self.contentListPage, (long)totalPages]);
|
||||
self.previousPageButton.enabled = total > 0 && self.contentListPage > 1;
|
||||
self.nextPageButton.enabled = total > 0 && self.contentListPage < totalPages;
|
||||
self.perPagePopup.enabled = total > 0;
|
||||
if ([self.section isEqualToString:@"media"]) {
|
||||
[self resetMediaPreview];
|
||||
}
|
||||
TCMSSetAccessibilityValue(self.tableView, [self currentListStatusMessage]);
|
||||
[self refreshWindowLayout];
|
||||
}
|
||||
|
||||
@@ -928,6 +1041,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
}
|
||||
NSDictionary *item = self.tableItems[row];
|
||||
field.stringValue = [tableColumn.identifier isEqualToString:@"size"] ? TCMSFormatBytes(item[@"size"]) : TCMSString(item[tableColumn.identifier]);
|
||||
TCMSSetAccessibility(field, [NSString stringWithFormat:@"%@: %@", tableColumn.title ?: TCMSString(tableColumn.identifier), field.stringValue], nil);
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -1570,6 +1684,9 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.mediaEmbedCopyButton.enabled = NO;
|
||||
self.mediaPreviewLabel.stringValue = @"No media selected";
|
||||
self.mediaDetailLabel.stringValue = @"";
|
||||
TCMSSetAccessibilityValue(self.mediaPreviewContainer, @"No media selected");
|
||||
TCMSSetAccessibilityValue(self.mediaPreviewLabel, @"No media selected");
|
||||
TCMSSetAccessibilityValue(self.mediaDetailLabel, @"");
|
||||
}
|
||||
|
||||
- (void)previewMediaItem:(NSDictionary *)item {
|
||||
@@ -1586,8 +1703,12 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
self.openMediaButton.enabled = YES;
|
||||
self.mediaURLCopyButton.enabled = YES;
|
||||
self.mediaEmbedCopyButton.enabled = YES;
|
||||
self.mediaPreviewLabel.stringValue = TCMSString(item[@"name"]).length ? TCMSString(item[@"name"]) : url.lastPathComponent;
|
||||
NSString *mediaName = TCMSString(item[@"name"]).length ? TCMSString(item[@"name"]) : url.lastPathComponent;
|
||||
self.mediaPreviewLabel.stringValue = mediaName;
|
||||
self.mediaDetailLabel.stringValue = [self mediaDetailForItem:item];
|
||||
TCMSSetAccessibilityValue(self.mediaPreviewContainer, [NSString stringWithFormat:@"%@. %@", mediaName, self.mediaDetailLabel.stringValue]);
|
||||
TCMSSetAccessibilityValue(self.mediaPreviewLabel, mediaName);
|
||||
TCMSSetAccessibilityValue(self.mediaDetailLabel, self.mediaDetailLabel.stringValue);
|
||||
self.mediaImageView.image = nil;
|
||||
self.mediaImageView.hidden = YES;
|
||||
[self.mediaPlayerView.player pause];
|
||||
@@ -1596,6 +1717,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
|
||||
if ([mime hasPrefix:@"image/"]) {
|
||||
self.mediaImageView.hidden = NO;
|
||||
TCMSSetAccessibility(self.mediaImageView, [NSString stringWithFormat:@"Image preview for %@", mediaName], nil);
|
||||
[self setStatus:@"Loading image preview."];
|
||||
[[[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
NSImage *image = data.length > 0 ? [[NSImage alloc] initWithData:data] : nil;
|
||||
@@ -1619,6 +1741,7 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
AVPlayer *player = [AVPlayer playerWithURL:url];
|
||||
self.mediaPlayerView.player = player;
|
||||
self.mediaPlayerView.hidden = NO;
|
||||
TCMSSetAccessibility(self.mediaPlayerView, [mime hasPrefix:@"audio/"] ? [NSString stringWithFormat:@"Audio player for %@", mediaName] : [NSString stringWithFormat:@"Video player for %@", mediaName], nil);
|
||||
[self setStatus:[mime hasPrefix:@"audio/"] ? @"Audio ready to play." : @"Video ready to play."];
|
||||
return;
|
||||
}
|
||||
@@ -1652,7 +1775,10 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
}
|
||||
self.tableItems = [NSMutableArray arrayWithArray:json[@"items"] ?: @[]];
|
||||
[self.tableView reloadData];
|
||||
[self setStatus:@"Comments refreshed."];
|
||||
NSString *status = self.commentStatusPopup.titleOfSelectedItem ?: @"pending";
|
||||
NSString *message = [NSString stringWithFormat:@"Showing %ld %@ comments.", (long)self.tableItems.count, status];
|
||||
TCMSSetAccessibilityValue(self.tableView, message);
|
||||
[self setStatus:message];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -1748,6 +1874,8 @@ static NSString *TCMSFormatBytes(id value) {
|
||||
|
||||
- (void)setStatus:(NSString *)message {
|
||||
self.statusLabel.stringValue = message ?: @"";
|
||||
TCMSSetAccessibilityValue(self.statusLabel, message ?: @"");
|
||||
[self announceAccessibilityMessage:message ?: @""];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user