Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Gas Mask.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@
AA00000C000000000000AAAA /* URLValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000B000000000000AAAA /* URLValidatorTests.swift */; };
AA00000E000000000000AAAA /* URLSheetPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */; };
AA000010000000000000AAAA /* URLSheetViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000F000000000000AAAA /* URLSheetViewTests.swift */; };
AA000012000000000000AAAA /* HostsDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000011000000000000AAAA /* HostsDataStore.swift */; };
AA000014000000000000AAAA /* HostsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000013000000000000AAAA /* HostsRowView.swift */; };
AA000016000000000000AAAA /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000015000000000000AAAA /* SidebarView.swift */; };
AA000018000000000000AAAA /* SidebarInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000017000000000000AAAA /* SidebarInstaller.swift */; };
AA00001A000000000000AAAA /* HostsDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000019000000000000AAAA /* HostsDataStoreTests.swift */; };
BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000001000000000000BBBB /* RemoteIntervalMapper.swift */; };
BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000003000000000000BBBB /* ShortcutRecorderView.swift */; };
CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000CC01 /* GlobalShortcuts.swift */; };
Expand Down Expand Up @@ -329,6 +334,12 @@
AA00000B000000000000AAAA /* URLValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLValidatorTests.swift; sourceTree = "<group>"; };
AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetPresenterTests.swift; sourceTree = "<group>"; };
AA00000F000000000000AAAA /* URLSheetViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetViewTests.swift; sourceTree = "<group>"; };
AA000011000000000000AAAA /* HostsDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsDataStore.swift; path = "Source/Swift/HostsDataStore.swift"; sourceTree = "<group>"; };
AA000013000000000000AAAA /* HostsRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsRowView.swift; path = "Source/Swift/HostsRowView.swift"; sourceTree = "<group>"; };
AA000015000000000000AAAA /* SidebarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SidebarView.swift; path = "Source/Swift/SidebarView.swift"; sourceTree = "<group>"; };
AA000017000000000000AAAA /* SidebarInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SidebarInstaller.swift; path = "Source/Swift/SidebarInstaller.swift"; sourceTree = "<group>"; };
AA000019000000000000AAAA /* HostsDataStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsDataStoreTests.swift; sourceTree = "<group>"; };
35A183A71A0ACF37002D6289 /* menuIcon@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = "menuIcon@2x.tiff"; path = "Resources/Images/menuIcon@2x.tiff"; sourceTree = "<group>"; };
BB000001000000000000BBBB /* RemoteIntervalMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RemoteIntervalMapper.swift; path = "Source/Swift/RemoteIntervalMapper.swift"; sourceTree = "<group>"; };
BB000003000000000000BBBB /* ShortcutRecorderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShortcutRecorderView.swift; path = "Source/Swift/ShortcutRecorderView.swift"; sourceTree = "<group>"; };
CC000001000000000000CC01 /* GlobalShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GlobalShortcuts.swift; path = "Source/Swift/GlobalShortcuts.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -767,6 +778,10 @@
AA000005000000000000AAAA /* NetworkStatusObserver.swift */,
AA000007000000000000AAAA /* URLSheetView.swift */,
AA000009000000000000AAAA /* URLSheetPresenter.swift */,
AA000011000000000000AAAA /* HostsDataStore.swift */,
AA000013000000000000AAAA /* HostsRowView.swift */,
AA000015000000000000AAAA /* SidebarView.swift */,
AA000017000000000000AAAA /* SidebarInstaller.swift */,
BB000020000000000000BBBB /* Preferences */,
);
name = Swift;
Expand Down Expand Up @@ -850,6 +865,7 @@
AA00000B000000000000AAAA /* URLValidatorTests.swift */,
AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */,
AA00000F000000000000AAAA /* URLSheetViewTests.swift */,
AA000019000000000000AAAA /* HostsDataStoreTests.swift */,
BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */,
BB00000F000000000000BBBB /* SparkleObserverTests.swift */,
BB000011000000000000BBBB /* PreferencesPresenterTests.swift */,
Expand Down Expand Up @@ -1090,6 +1106,10 @@
AA000006000000000000AAAA /* NetworkStatusObserver.swift in Sources */,
AA000008000000000000AAAA /* URLSheetView.swift in Sources */,
AA00000A000000000000AAAA /* URLSheetPresenter.swift in Sources */,
AA000012000000000000AAAA /* HostsDataStore.swift in Sources */,
AA000014000000000000AAAA /* HostsRowView.swift in Sources */,
AA000016000000000000AAAA /* SidebarView.swift in Sources */,
AA000018000000000000AAAA /* SidebarInstaller.swift in Sources */,
BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */,
BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */,
CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */,
Expand Down Expand Up @@ -1138,6 +1158,7 @@
AA00000C000000000000AAAA /* URLValidatorTests.swift in Sources */,
AA00000E000000000000AAAA /* URLSheetPresenterTests.swift in Sources */,
AA000010000000000000AAAA /* URLSheetViewTests.swift in Sources */,
AA00001A000000000000AAAA /* HostsDataStoreTests.swift in Sources */,
BB00000E000000000000BBBB /* RemoteIntervalMapperTests.swift in Sources */,
BB000010000000000000BBBB /* SparkleObserverTests.swift in Sources */,
BB000012000000000000BBBB /* PreferencesPresenterTests.swift in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions Source/EditorController.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#import "EditorController.h"
#import "Preferences.h"
#import "ExtendedNSSplitView.h"
#import "Gas_Mask-Swift.h"

#define SplitViewMinWidth 140
#define SplitViewMaxWidth 300
Expand All @@ -41,6 +42,8 @@ - (void)awakeFromNib
if (position > SplitViewMaxWidth) {
[splitView setPosition:SplitViewDefaultWidth ofDividerAtIndex:dividerIndex];
}

[SidebarInstaller installIn:splitView];
}

#pragma mark - Split View Delegate
Expand Down
1 change: 1 addition & 0 deletions Source/HostsMainController.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@

- (Hosts*)activeHostsFile;
- (Hosts*)selectedHosts;
- (void)selectHosts:(Hosts*)hosts;
- (NSArray*)allHostsFilesGrouped;
- (NSArray*)allHostsFiles;
- (int)filesCount;
Expand Down
12 changes: 12 additions & 0 deletions Source/HostsMainController.m
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,18 @@ - (Hosts*)selectedHosts
return [[self selectedObjects] lastObject];
}

- (void)selectHosts:(Hosts *)hosts
{
NSIndexPath *path = [self hostsIndexPath:hosts];
if (path) {
// Defer to next run loop iteration so the tree controller
// finishes any in-flight insert/remove before we change selection.
dispatch_async(dispatch_get_main_queue(), ^{
[self setSelectionIndexPath:path];
});
}
}

- (NSArray*)allHostsFilesGrouped
{
int nrControllers = [controllers count];
Expand Down
1 change: 1 addition & 0 deletions Source/ListController.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@
+ (ListController*)defaultInstance;

- (Hosts*)selectedHosts;
- (void)deactivate;

@end
5 changes: 5 additions & 0 deletions Source/ListController.m
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ - (void)awakeFromNib
[self selectActiveHostsFile];
}

- (void)deactivate
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}

- (void)updateItem:(NSNotification *)notification
{
int index = [self indexOfHosts:[notification object]];
Expand Down
9 changes: 9 additions & 0 deletions Source/Swift/GasMask-Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
#import <Cocoa/Cocoa.h>
#import "Network.h"
#import "Node.h"
#import "Hosts.h"
#import "HostsGroup.h"
#import "RemoteHosts.h"
#import "CombinedHosts.h"
#import "HostsMainController.h"
#import "RemoteHostsController.h"
#import "Preferences.h"
#import "Error.h"
#import "LocalHostsController.h"
#import "CombinedHostsController.h"
#import "ListController.h"
#import "Preferences+Remote.h"
#import "LoginItem.h"
144 changes: 144 additions & 0 deletions Source/Swift/HostsDataStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Foundation
import Combine

// MARK: - Notification Names
// These are defined as #define macros in Gas_Mask_Prefix.pch, which Swift cannot import.
// Mirror them here as NSNotification.Name constants.

extension NSNotification.Name {
static let hostsFileCreated = NSNotification.Name("HostsFileCreatedNotification")
static let hostsFileRemoved = NSNotification.Name("HostsFileRemovedNotification")
static let hostsFileRenamed = NSNotification.Name("HostsFileRenamedNotification")
static let hostsFileSaved = NSNotification.Name("HostsFileSavedNotification")
static let hostsNodeNeedsUpdate = NSNotification.Name("HostsNodeNeedsUpdateNotification")
static let hostsFileShouldBeRenamed = NSNotification.Name("HostsFileShouldBeRenamedNotification")
static let hostsFileShouldBeSelected = NSNotification.Name("HostsFileShouldBeSelectedNotification")
static let synchronizingStatusChanged = NSNotification.Name("SynchronizingStatusChangedNotification")
static let allHostsFilesLoadedFromDisk = NSNotification.Name("AllHostsFilesLoadedFromDiskNotification")
}

// MARK: - HostsDataStore

final class HostsDataStore: ObservableObject {

static let shared = HostsDataStore()

// MARK: Published Properties

@Published var hostsGroups: [HostsGroup] = []
@Published var selectedHosts: Hosts? {
didSet {
guard selectedHosts !== oldValue, !isSyncingSelection else { return }
if let hosts = selectedHosts {
HostsMainController.defaultInstance()?.select(hosts)
}
}
}
@Published var filesCount: Int = 0
@Published var canRemoveFiles: Bool = false
@Published var renamingHosts: Hosts?

// MARK: Private

private var notificationObservers: [NSObjectProtocol] = []
private var isSyncingSelection = false

// MARK: Init

private init() {
refreshGroups()
refreshFilesCount()
observeNotifications()
}

// MARK: Refresh

func refreshGroups() {
guard let controller = HostsMainController.defaultInstance(),
let content = controller.content as? [HostsGroup] else {
hostsGroups = []
return
}
hostsGroups = content
}

private func refreshFilesCount() {
guard let controller = HostsMainController.defaultInstance() else { return }
filesCount = Int(controller.filesCount())
canRemoveFiles = controller.canRemoveFiles()
}

// MARK: Selection Sync

/// Called when the ObjC layer selects a hosts file (via notification).
private func syncSelectionFromModel(_ hosts: Hosts?) {
isSyncingSelection = true
selectedHosts = hosts
isSyncingSelection = false
}

// MARK: Notification Observers

private func observeNotifications() {
let nc = NotificationCenter.default

// Data change notifications — refresh groups and counts
let refreshNames: [NSNotification.Name] = [
.hostsFileCreated,
.hostsFileRemoved,
.hostsFileRenamed
]

for name in refreshNames {
let observer = nc.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
self?.refreshGroups()
self?.refreshFilesCount()
}
notificationObservers.append(observer)
}

// All files loaded — refresh data then select the active file
let loadedObserver = nc.addObserver(
forName: .allHostsFilesLoadedFromDisk, object: nil, queue: .main
) { [weak self] _ in
self?.refreshGroups()
self?.refreshFilesCount()
let active = HostsMainController.defaultInstance()?.activeHostsFile()
self?.syncSelectionFromModel(active)
}
notificationObservers.append(loadedObserver)

// Single-row refresh notifications — reassign hostsGroups to force SwiftUI diffing
let rowRefreshNames: [NSNotification.Name] = [
.hostsFileSaved,
.hostsNodeNeedsUpdate,
.synchronizingStatusChanged
]

for name in rowRefreshNames {
let observer = nc.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
guard let self else { return }
self.hostsGroups = self.hostsGroups
}
notificationObservers.append(observer)
}

// UI action notifications
let renameObserver = nc.addObserver(
forName: .hostsFileShouldBeRenamed, object: nil, queue: .main
) { [weak self] notification in
self?.renamingHosts = notification.object as? Hosts
}
notificationObservers.append(renameObserver)

let selectObserver = nc.addObserver(
forName: .hostsFileShouldBeSelected, object: nil, queue: .main
) { [weak self] notification in
if let hosts = notification.object as? Hosts {
self?.syncSelectionFromModel(hosts)
}
}
notificationObservers.append(selectObserver)
}

}
Loading