Skip to content

Commit d200be5

Browse files
committed
Display the categories and sub services in a submenu-style
Second step of #103, what remains is to apply this model to all services that have a lot of "big" components
1 parent 4fde009 commit d200be5

File tree

4 files changed

+209
-28
lines changed

4 files changed

+209
-28
lines changed

Diff for: stts/BottomBar.swift

+44-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ class BottomBar: NSView {
1818
let doneButton = NSButton()
1919
let aboutButton = NSButton()
2020
let quitButton = NSButton()
21+
let backButton = NSButton()
2122
let statusField = NSTextField()
2223
let separator = ServiceTableRowView()
2324

@@ -30,6 +31,7 @@ class BottomBar: NSView {
3031
var reloadServicesCallback: () -> Void = {}
3132
var openSettingsCallback: () -> Void = {}
3233
var closeSettingsCallback: () -> Void = {}
34+
var backCallback: () -> Void = {}
3335

3436
override init(frame frameRect: NSRect) {
3537
super.init(frame: frameRect)
@@ -42,7 +44,12 @@ class BottomBar: NSView {
4244
}
4345

4446
private func commonInit() {
45-
[separator, settingsButton, reloadButton, statusField, doneButton, aboutButton, quitButton].forEach {
47+
[
48+
separator,
49+
settingsButton, reloadButton, statusField, // Main view buttons
50+
doneButton, aboutButton, quitButton, // Editor view buttons
51+
backButton // Category view buttons
52+
].forEach {
4653
$0.translatesAutoresizingMaskIntoConstraints = false
4754
addSubview($0)
4855
}
@@ -95,7 +102,11 @@ class BottomBar: NSView {
95102

96103
quitButton.widthAnchor.constraint(equalToConstant: 46),
97104
quitButton.centerYAnchor.constraint(equalTo: centerYAnchor),
98-
quitButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3)
105+
quitButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3),
106+
107+
backButton.widthAnchor.constraint(equalToConstant: 46),
108+
backButton.centerYAnchor.constraint(equalTo: centerYAnchor),
109+
backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 3)
99110
])
100111

101112
settingsButton.isBordered = false
@@ -150,6 +161,13 @@ class BottomBar: NSView {
150161
quitButton.isHidden = true
151162
quitButton.target = NSApp
152163
quitButton.action = #selector(NSApplication.terminate(_:))
164+
165+
backButton.title = "Back"
166+
backButton.bezelStyle = .regularSquare
167+
backButton.controlSize = .regular
168+
backButton.isHidden = true
169+
backButton.target = self
170+
backButton.action = #selector(BottomBar.back)
153171
}
154172

155173
func updateStatusText() {
@@ -162,6 +180,16 @@ class BottomBar: NSView {
162180
}
163181
}
164182

183+
func openedCategory(_ category: ServiceCategory?, backCallback: @escaping () -> Void) {
184+
doneButton.isHidden = false
185+
aboutButton.isHidden = category != nil
186+
quitButton.isHidden = category != nil
187+
188+
backButton.isHidden = category == nil
189+
190+
self.backCallback = backCallback
191+
}
192+
165193
@objc func reloadServices() {
166194
reloadServicesCallback()
167195
}
@@ -179,6 +207,8 @@ class BottomBar: NSView {
179207
}
180208

181209
@objc func closeSettings() {
210+
backCallback()
211+
182212
settingsButton.isHidden = false
183213
statusField.isHidden = false
184214
reloadButton.isHidden = false
@@ -187,6 +217,8 @@ class BottomBar: NSView {
187217
aboutButton.isHidden = true
188218
quitButton.isHidden = true
189219

220+
backButton.isHidden = true
221+
190222
closeSettingsCallback()
191223
}
192224

@@ -221,4 +253,14 @@ class BottomBar: NSView {
221253
NSApp.orderFrontStandardAboutPanel(options: [NSApplication.AboutPanelOptionKey(rawValue: "Credits"): credits])
222254
NSApp.activate(ignoringOtherApps: true)
223255
}
256+
257+
@objc func back() {
258+
backButton.isHidden = true
259+
260+
doneButton.isHidden = false
261+
aboutButton.isHidden = false
262+
quitButton.isHidden = false
263+
264+
backCallback()
265+
}
224266
}

Diff for: stts/EditorTableView/EditorTableCell.swift

+45-7
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,22 @@ class EditorTableCell: NSTableCellView {
1717
enum ToggleButton {
1818
static let size = NSSize(width: 36, height: 20)
1919
}
20+
21+
enum ArrowImage {
22+
static let size = NSSize(width: 20, height: 20)
23+
}
24+
}
25+
26+
enum CellType {
27+
case service
28+
case category
2029
}
2130

2231
static let defaultHeight: CGFloat = 30
2332

2433
let toggleButton = NSButton()
34+
let arrowImageView = NSImageView()
35+
2536
var selected: Bool = false {
2637
didSet {
2738
setNeedsDisplay(frame)
@@ -30,6 +41,19 @@ class EditorTableCell: NSTableCellView {
3041

3142
var toggleCallback: () -> Void = {}
3243

44+
var type: CellType = .service {
45+
didSet {
46+
switch type {
47+
case .service:
48+
toggleButton.isHidden = false
49+
arrowImageView.isHidden = true
50+
case .category:
51+
toggleButton.isHidden = true
52+
arrowImageView.isHidden = false
53+
}
54+
}
55+
}
56+
3357
static func estimatedHeight(for service: Service, maxWidth: CGFloat) -> CGFloat {
3458
return
3559
service.name.height(forWidth: maxWidth, font: Design.Name.font) +
@@ -60,11 +84,6 @@ class EditorTableCell: NSTableCellView {
6084
textField.font = Design.Name.font
6185
textField.textColor = NSColor.textColor
6286
textField.backgroundColor = NSColor.clear
63-
textField.translatesAutoresizingMaskIntoConstraints = false
64-
addSubview(textField)
65-
66-
toggleButton.translatesAutoresizingMaskIntoConstraints = false
67-
addSubview(toggleButton)
6887

6988
toggleButton.title = ""
7089
toggleButton.isBordered = false
@@ -76,15 +95,34 @@ class EditorTableCell: NSTableCellView {
7695
toggleButton.layer?.borderWidth = 1
7796
toggleButton.layer?.cornerRadius = 4
7897

98+
arrowImageView.image = NSImage(named: "NSGoRightTemplate")
99+
100+
[textField, toggleButton, arrowImageView].forEach {
101+
$0.translatesAutoresizingMaskIntoConstraints = false
102+
addSubview($0)
103+
}
104+
79105
NSLayoutConstraint.activate([
80106
textField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: Design.padding.left),
81107
textField.centerYAnchor.constraint(equalTo: centerYAnchor),
82108

83-
toggleButton.leadingAnchor.constraint(equalTo: textField.trailingAnchor, constant: Design.innerSpacing),
109+
toggleButton.leadingAnchor.constraint(
110+
greaterThanOrEqualTo: textField.trailingAnchor,
111+
constant: Design.innerSpacing
112+
),
84113
toggleButton.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Design.padding.right),
85114
toggleButton.centerYAnchor.constraint(equalTo: centerYAnchor),
86115
toggleButton.widthAnchor.constraint(equalToConstant: Design.ToggleButton.size.width),
87-
toggleButton.heightAnchor.constraint(equalToConstant: Design.ToggleButton.size.height)
116+
toggleButton.heightAnchor.constraint(equalToConstant: Design.ToggleButton.size.height),
117+
118+
arrowImageView.leadingAnchor.constraint(
119+
greaterThanOrEqualTo: textField.trailingAnchor,
120+
constant: Design.innerSpacing
121+
),
122+
arrowImageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -Design.padding.right),
123+
arrowImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
124+
arrowImageView.widthAnchor.constraint(equalToConstant: Design.ArrowImage.size.width),
125+
arrowImageView.heightAnchor.constraint(equalToConstant: Design.ArrowImage.size.height)
88126
])
89127
}
90128

Diff for: stts/EditorTableView/EditorTableViewController.swift

+113-17
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import Cocoa
88
class EditorTableViewController: NSObject, SwitchableTableViewController {
99
let contentView: NSStackView
1010
let scrollView: CustomScrollView
11+
let bottomBar: BottomBar
1112
let tableView = NSTableView()
1213

1314
let allServices: [BaseService] = BaseService.all().sorted()
15+
let allServicesWithoutSubServices: [BaseService] = BaseService.allWithoutSubServices().sorted()
1416
var filteredServices: [BaseService]
1517
var selectedServices: [BaseService] = Preferences.shared.selectedServices
1618

@@ -20,6 +22,64 @@ class EditorTableViewController: NSObject, SwitchableTableViewController {
2022

2123
var hidden: Bool = true
2224

25+
var savedScrollPosition: CGPoint = .zero
26+
27+
var selectedCategory: ServiceCategory? {
28+
didSet {
29+
// Save the scroll position between screens
30+
let scrollToPosition: CGPoint?
31+
32+
if selectedCategory != nil && oldValue == nil {
33+
savedScrollPosition = CGPoint(x: 0, y: tableView.visibleRect.minY)
34+
scrollToPosition = .zero
35+
} else if selectedCategory == nil && oldValue != nil {
36+
scrollToPosition = savedScrollPosition
37+
} else {
38+
scrollToPosition = nil
39+
}
40+
41+
// Adjust UI
42+
bottomBar.openedCategory(selectedCategory, backCallback: { [weak self] in
43+
self?.selectedCategory = nil
44+
})
45+
46+
guard let category = selectedCategory else {
47+
// Show the unfiltered services
48+
filteredServices = allServicesWithoutSubServices
49+
tableView.reloadData()
50+
51+
if let scrollPosition = scrollToPosition {
52+
tableView.scroll(scrollPosition)
53+
}
54+
55+
return
56+
}
57+
58+
// Find the sub services
59+
var subServices = allServices.filter {
60+
// Can't check superclass matches without mirror
61+
Mirror(reflecting: $0).superclassMirror?.subjectType == category.subServiceSuperclass
62+
63+
// Exclude the category so that we can add it at the top
64+
&& $0 != category as? BaseService
65+
}.sorted()
66+
67+
// Add the category as the top item
68+
(category as? BaseService).flatMap { subServices.insert($0, at: 0) }
69+
70+
filteredServices = subServices
71+
tableView.reloadData()
72+
73+
if let scrollPosition = scrollToPosition {
74+
tableView.scroll(scrollPosition)
75+
}
76+
}
77+
}
78+
79+
var isSearching: Bool {
80+
settingsView.searchField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) != ""
81+
}
82+
2383
private var maxNameWidth: CGFloat? {
2484
didSet {
2585
if oldValue != maxNameWidth {
@@ -28,10 +88,11 @@ class EditorTableViewController: NSObject, SwitchableTableViewController {
2888
}
2989
}
3090

31-
init(contentView: NSStackView, scrollView: CustomScrollView) {
91+
init(contentView: NSStackView, scrollView: CustomScrollView, bottomBar: BottomBar) {
3292
self.contentView = contentView
3393
self.scrollView = scrollView
34-
self.filteredServices = allServices
94+
self.filteredServices = allServicesWithoutSubServices
95+
self.bottomBar = bottomBar
3596

3697
super.init()
3798
setup()
@@ -56,11 +117,12 @@ class EditorTableViewController: NSObject, SwitchableTableViewController {
56117
settingsView.searchCallback = { [weak self] searchString in
57118
guard
58119
let strongSelf = self,
59-
let allServices = strongSelf.allServices as? [Service]
120+
let allServices = strongSelf.allServices as? [Service],
121+
let allServicesWithoutSubServices = strongSelf.allServicesWithoutSubServices as? [Service]
60122
else { return }
61123

62124
if searchString.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
63-
strongSelf.filteredServices = allServices
125+
strongSelf.filteredServices = allServicesWithoutSubServices
64126
} else {
65127
// Can't filter array with NSPredicate without making Service inherit KVO from NSObject, therefore we create
66128
// an array of service names that we can run the predicate on
@@ -71,6 +133,10 @@ class EditorTableViewController: NSObject, SwitchableTableViewController {
71133
strongSelf.filteredServices = allServices.filter { filteredServiceNames.contains($0.name) }
72134
}
73135

136+
if strongSelf.selectedCategory != nil {
137+
strongSelf.selectedCategory = nil
138+
}
139+
74140
strongSelf.tableView.reloadData()
75141
}
76142

@@ -122,6 +188,10 @@ class EditorTableViewController: NSObject, SwitchableTableViewController {
122188
func willHide() {
123189
settingsView.isHidden = true
124190
}
191+
192+
@objc func deselectCategory() {
193+
selectedCategory = nil
194+
}
125195
}
126196

127197
extension EditorTableViewController: NSTableViewDataSource {
@@ -132,6 +202,19 @@ extension EditorTableViewController: NSTableViewDataSource {
132202
func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
133203
return nil
134204
}
205+
206+
func tableViewSelectionDidChange(_ notification: Notification) {
207+
guard tableView.selectedRow != -1 else { return }
208+
209+
// We're only interested in selections of categories
210+
guard
211+
selectedCategory == nil,
212+
let category = filteredServices[tableView.selectedRow] as? ServiceCategory
213+
else { return }
214+
215+
// Change the selected category
216+
selectedCategory = category
217+
}
135218
}
136219

137220
extension EditorTableViewController: NSTableViewDelegate {
@@ -151,22 +234,35 @@ extension EditorTableViewController: NSTableViewDelegate {
151234
guard let view = cell as? EditorTableCell else { return nil }
152235
guard let service = filteredServices[row] as? Service else { return nil }
153236

154-
view.textField?.stringValue = service.name
155-
view.selected = selectedServices.contains(service)
156-
view.toggleCallback = { [weak self] in
157-
guard let strongSelf = self else { return }
158-
159-
strongSelf.selectionChanged = true
237+
if isSearching || selectedCategory != nil {
238+
view.type = .service
239+
} else {
240+
view.type = (service is ServiceCategory) ? .category : .service
241+
}
160242

161-
if view.selected {
162-
self?.selectedServices.append(service)
163-
} else {
164-
if let index = self?.selectedServices.firstIndex(of: service) {
165-
self?.selectedServices.remove(at: index)
243+
switch view.type {
244+
case .service:
245+
view.textField?.stringValue = service.name
246+
view.selected = selectedServices.contains(service)
247+
view.toggleCallback = { [weak self] in
248+
guard let strongSelf = self else { return }
249+
250+
strongSelf.selectionChanged = true
251+
252+
if view.selected {
253+
self?.selectedServices.append(service)
254+
} else {
255+
if let index = self?.selectedServices.firstIndex(of: service) {
256+
self?.selectedServices.remove(at: index)
257+
}
166258
}
167-
}
168259

169-
Preferences.shared.selectedServices = strongSelf.selectedServices
260+
Preferences.shared.selectedServices = strongSelf.selectedServices
261+
}
262+
case .category:
263+
view.textField?.stringValue = (service as? ServiceCategory)?.categoryName ?? service.name
264+
view.selected = false
265+
view.toggleCallback = {}
170266
}
171267

172268
maxNameWidth = EditorTableCell.maxNameWidth(for: tableView)

0 commit comments

Comments
 (0)