mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 11:25:57 +03:00
Implement Quick Action bar
This commit is contained in:
parent
b219ce991b
commit
3019629591
@ -7,6 +7,7 @@ Change Log
|
||||
|
||||
### New Features
|
||||
|
||||
- Add Quick Action bar which can launch by the Command-K keys.
|
||||
- Insert scanned text in a photo taken by iPhone or iPad.
|
||||
|
||||
|
||||
|
@ -285,6 +285,12 @@
|
||||
2A5DCE861D1888D800D5D74C /* SyntaxMappingConflictsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5DCE851D1888D800D5D74C /* SyntaxMappingConflictsView.swift */; };
|
||||
2A5DCE871D1888D800D5D74C /* SyntaxMappingConflictsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5DCE851D1888D800D5D74C /* SyntaxMappingConflictsView.swift */; };
|
||||
2A5DCE8A1D18FFDB00D5D74C /* EncodingListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5DCE881D18FFDB00D5D74C /* EncodingListView.swift */; };
|
||||
2A5E41052B0AEFBB00D5EA20 /* QuickActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5E41042B0AEFBB00D5EA20 /* QuickActionView.swift */; };
|
||||
2A5E41062B0AEFBB00D5EA20 /* QuickActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5E41042B0AEFBB00D5EA20 /* QuickActionView.swift */; };
|
||||
2A5E41082B0AF62100D5EA20 /* QuickActionWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5E41072B0AF62100D5EA20 /* QuickActionWindowController.swift */; };
|
||||
2A5E41092B0AF62100D5EA20 /* QuickActionWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5E41072B0AF62100D5EA20 /* QuickActionWindowController.swift */; };
|
||||
2A5E410B2B0B559300D5EA20 /* ActionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5E410A2B0B559300D5EA20 /* ActionCommand.swift */; };
|
||||
2A5E410C2B0B559400D5EA20 /* ActionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A5E410A2B0B559300D5EA20 /* ActionCommand.swift */; };
|
||||
2A5E6FC12A72342700E33EA7 /* UnicodeNormalization.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2A5E6FC02A72342700E33EA7 /* UnicodeNormalization.xcstrings */; };
|
||||
2A5E6FC22A72342700E33EA7 /* UnicodeNormalization.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2A5E6FC02A72342700E33EA7 /* UnicodeNormalization.xcstrings */; };
|
||||
2A5E6FC42A723CEA00E33EA7 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2A5E6FC32A723CE900E33EA7 /* InfoPlist.xcstrings */; };
|
||||
@ -1037,6 +1043,9 @@
|
||||
2A5DCE851D1888D800D5D74C /* SyntaxMappingConflictsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyntaxMappingConflictsView.swift; sourceTree = "<group>"; };
|
||||
2A5DCE881D18FFDB00D5D74C /* EncodingListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncodingListView.swift; sourceTree = "<group>"; };
|
||||
2A5DD44122793B9B0057AAD1 /* fr */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = fr; path = fr.lproj/ReportTemplate.md; sourceTree = "<group>"; };
|
||||
2A5E41042B0AEFBB00D5EA20 /* QuickActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionView.swift; sourceTree = "<group>"; };
|
||||
2A5E41072B0AF62100D5EA20 /* QuickActionWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickActionWindowController.swift; sourceTree = "<group>"; };
|
||||
2A5E410A2B0B559300D5EA20 /* ActionCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionCommand.swift; sourceTree = "<group>"; };
|
||||
2A5E6FC02A72342700E33EA7 /* UnicodeNormalization.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = UnicodeNormalization.xcstrings; path = mul.lproj/UnicodeNormalization.xcstrings; sourceTree = "<group>"; };
|
||||
2A5E6FC32A723CE900E33EA7 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = InfoPlist.xcstrings; path = mul.lproj/InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||
2A5E6FC62A723F3C00E33EA7 /* ServicesMenu.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = ServicesMenu.xcstrings; path = mul.lproj/ServicesMenu.xcstrings; sourceTree = "<group>"; };
|
||||
@ -2043,6 +2052,16 @@
|
||||
name = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A5E410D2B0CE4DB00D5EA20 /* Quick Action Bar */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
2A5E41072B0AF62100D5EA20 /* QuickActionWindowController.swift */,
|
||||
2A5E41042B0AEFBB00D5EA20 /* QuickActionView.swift */,
|
||||
2A5E410A2B0B559300D5EA20 /* ActionCommand.swift */,
|
||||
);
|
||||
name = "Quick Action Bar";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A6602EB1D05E04E003E8D87 /* Accessory Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2374,6 +2393,7 @@
|
||||
children = (
|
||||
2A5D13091D1ED10400D38E6A /* ConsolePanelController.swift */,
|
||||
2A4257AF1D22FD490086DAAD /* ColorCodePanelController.swift */,
|
||||
2A5E410D2B0CE4DB00D5EA20 /* Quick Action Bar */,
|
||||
);
|
||||
name = Panels;
|
||||
sourceTree = "<group>";
|
||||
@ -2787,9 +2807,9 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2A39F15726F74C2500B52876 /* AccessibleStepper.swift in Sources */,
|
||||
2A5E410B2B0B559300D5EA20 /* ActionCommand.swift in Sources */,
|
||||
2AF29EC42882EE7700DF31D2 /* AdvancedCharacterCounter.swift in Sources */,
|
||||
2A04E9C327FEFA86008C82D8 /* AdvancedCharacterCounterView.swift in Sources */,
|
||||
2A73B9332A8F6620002F3A16 /* RegexTextField.swift in Sources */,
|
||||
2ACDC09B1D172CDE009B72D6 /* AntialiasingTextField.swift in Sources */,
|
||||
2AC2462F1D1BC70C00E46CFA /* AppDelegate.swift in Sources */,
|
||||
2A2792931D1DACC400F3FC5D /* AppearancePaneController.swift in Sources */,
|
||||
@ -2999,8 +3019,11 @@
|
||||
2AA175FA2AC5634500F6462C /* PopoverHolderView.swift in Sources */,
|
||||
2A1856131D48AFEA008FA79E /* PrintPanelAccessoryController.swift in Sources */,
|
||||
2AFAFD4B1D41487600F1458F /* PrintTextView.swift in Sources */,
|
||||
2A5E41052B0AEFBB00D5EA20 /* QuickActionView.swift in Sources */,
|
||||
2A5E41082B0AF62100D5EA20 /* QuickActionWindowController.swift in Sources */,
|
||||
2A57B98F294ED75900771696 /* RangedIntegerFormatStyle.swift in Sources */,
|
||||
2A4AF76820759BE500C47606 /* RegexFindPanelTextView.swift in Sources */,
|
||||
2A73B9332A8F6620002F3A16 /* RegexTextField.swift in Sources */,
|
||||
2A1814B921CF8BD500602214 /* RegularExpressionFormatter.swift in Sources */,
|
||||
2A53F56727585A0E00ED16DF /* RegularExpressionReferenceView.swift in Sources */,
|
||||
2A0778622072040500876277 /* RegularExpressionSyntaxType.swift in Sources */,
|
||||
@ -3148,6 +3171,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
2A39F15826F74C2500B52876 /* AccessibleStepper.swift in Sources */,
|
||||
2A5E410C2B0B559400D5EA20 /* ActionCommand.swift in Sources */,
|
||||
2AF29EC52882EE7700DF31D2 /* AdvancedCharacterCounter.swift in Sources */,
|
||||
2A04E9C427FEFA87008C82D8 /* AdvancedCharacterCounterView.swift in Sources */,
|
||||
2ACDC09A1D172CDE009B72D6 /* AntialiasingTextField.swift in Sources */,
|
||||
@ -3257,7 +3281,6 @@
|
||||
2AEAA8232096380C001A175C /* HighlightExtractors.swift in Sources */,
|
||||
2A8458942A073C5F0056B1EA /* HighlightDefinition.swift in Sources */,
|
||||
2AAD61F81D2BA3F5008FE772 /* HighlightParser.swift in Sources */,
|
||||
2A73B9342A8F6620002F3A16 /* RegexTextField.swift in Sources */,
|
||||
2AE95A1B2A86270000E85CF5 /* HoleContentView.swift in Sources */,
|
||||
2A5D13131D1EE8FF00D38E6A /* HUDView.swift in Sources */,
|
||||
2AE144B72B00A963005E8CF1 /* Identifiable.swift in Sources */,
|
||||
@ -3361,7 +3384,10 @@
|
||||
2A1856121D48AFEA008FA79E /* PrintPanelAccessoryController.swift in Sources */,
|
||||
2AFAFD4A1D41487600F1458F /* PrintTextView.swift in Sources */,
|
||||
2A57B990294ED75900771696 /* RangedIntegerFormatStyle.swift in Sources */,
|
||||
2A5E41062B0AEFBB00D5EA20 /* QuickActionView.swift in Sources */,
|
||||
2A5E41092B0AF62100D5EA20 /* QuickActionWindowController.swift in Sources */,
|
||||
2A4AF76720759BE500C47606 /* RegexFindPanelTextView.swift in Sources */,
|
||||
2A73B9342A8F6620002F3A16 /* RegexTextField.swift in Sources */,
|
||||
2A1814B821CF8BD500602214 /* RegularExpressionFormatter.swift in Sources */,
|
||||
2A53F56827585A0E00ED16DF /* RegularExpressionReferenceView.swift in Sources */,
|
||||
2A0778612072040500876277 /* RegularExpressionSyntaxType.swift in Sources */,
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22154"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22155"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
@ -609,6 +609,12 @@ Gw
|
||||
<action selector="toggleStatusBar:" target="Ady-hI-5gd" id="pjH-8E-CJp"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="WNT-nz-Qhz"/>
|
||||
<menuItem title="Show Quick Actions…" keyEquivalent="k" id="7Su-OV-TgR">
|
||||
<connections>
|
||||
<action selector="showQuickActions:" target="Ady-hI-5gd" id="6AA-OS-m64"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="366"/>
|
||||
<menuItem title="Split Editor" id="793">
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
|
112
CotEditor/Sources/ActionCommand.swift
Normal file
112
CotEditor/Sources/ActionCommand.swift
Normal file
@ -0,0 +1,112 @@
|
||||
//
|
||||
// ActionCommand.swift
|
||||
//
|
||||
// CotEditor
|
||||
// https://coteditor.com
|
||||
//
|
||||
// Created by 1024jp on 2023-11-20.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// © 2023 1024jp
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
struct ActionCommand: Identifiable {
|
||||
|
||||
enum Kind {
|
||||
|
||||
case command
|
||||
case outline
|
||||
case script
|
||||
}
|
||||
|
||||
let id = UUID()
|
||||
|
||||
var kind: Kind
|
||||
var title: String
|
||||
var paths: [String] = []
|
||||
var shortcut: Shortcut?
|
||||
|
||||
var action: Selector
|
||||
var tag: Int = 0
|
||||
|
||||
|
||||
/// Perform the original menu action.
|
||||
@discardableResult
|
||||
func perform() -> Bool {
|
||||
|
||||
let sender = NSMenuItem()
|
||||
sender.title = self.title
|
||||
sender.action = self.action
|
||||
sender.tag = self.tag
|
||||
|
||||
return NSApp.sendAction(self.action, to: nil, from: sender)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension NSMenuItem {
|
||||
|
||||
/// The flat collection of `ActionCommand` representation including descendant items.
|
||||
var actionCommands: [ActionCommand] {
|
||||
|
||||
if let submenu = self.submenu {
|
||||
return submenu.items
|
||||
.flatMap { $0.actionCommands }
|
||||
.map {
|
||||
var command = $0
|
||||
command.paths.insert(self.title, at: 0)
|
||||
return command
|
||||
}
|
||||
|
||||
} else if let action = self.action, !self.isHidden, !ActionCommand.unsupportedActions.contains(action) {
|
||||
self.validate()
|
||||
return [ActionCommand(kind: (action == #selector(ScriptManager.launchScript)) ? .script : .command,
|
||||
title: self.title, paths: [], shortcut: self.shortcut, action: action, tag: self.tag)]
|
||||
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Validate the menu item so that the menu item properties, such as title, are updated to fit to the latest states.
|
||||
private func validate() {
|
||||
|
||||
guard
|
||||
let validator = self.target
|
||||
?? self.action.flatMap({ NSApp.target(forAction: $0, to: self.target, from: self) }) as AnyObject?
|
||||
else { return }
|
||||
|
||||
switch validator {
|
||||
case let validator as any NSMenuItemValidation:
|
||||
validator.validateMenuItem(self)
|
||||
case let validator as any NSUserInterfaceValidations:
|
||||
validator.validateUserInterfaceItem(self)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension ActionCommand {
|
||||
|
||||
static let unsupportedActions: [Selector] = [
|
||||
#selector(AppDelegate.showQuickActions),
|
||||
]
|
||||
}
|
@ -332,6 +332,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
|
||||
|
||||
/// Show the Quick Action bar.
|
||||
@IBAction func showQuickActions(_ sender: Any?) {
|
||||
|
||||
QuickActionWindowController.shared.showWindow(sender)
|
||||
}
|
||||
|
||||
|
||||
/// Show Snippet pane in the Settings window.
|
||||
@IBAction func showSnippetEditor(_ sender: Any?) {
|
||||
|
||||
|
301
CotEditor/Sources/QuickActionView.swift
Normal file
301
CotEditor/Sources/QuickActionView.swift
Normal file
@ -0,0 +1,301 @@
|
||||
//
|
||||
// QuickActionView.swift
|
||||
//
|
||||
// CotEditor
|
||||
// https://coteditor.com
|
||||
//
|
||||
// Created by 1024jp on 2023-11-20.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// © 2023 1024jp
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
|
||||
struct QuickActionView: View {
|
||||
|
||||
struct Candidate: Identifiable {
|
||||
|
||||
var command: ActionCommand
|
||||
var result: String.AbbreviatedMatchResult
|
||||
var id: UUID { self.command.id }
|
||||
}
|
||||
|
||||
|
||||
@Environment(\.controlActiveState) private var controlActiveState
|
||||
|
||||
weak var parent: NSWindow?
|
||||
|
||||
@State var command: String = ""
|
||||
@State var candidates: [Candidate] = []
|
||||
|
||||
@State private var commands: [ActionCommand] = []
|
||||
@State private var selection: ActionCommand.ID?
|
||||
|
||||
@State private var keyMonitor: Any?
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
TextField("Quick Actions", text: $command)
|
||||
.onSubmit(self.perform)
|
||||
.fontWeight(.light)
|
||||
.textFieldStyle(.plain)
|
||||
}
|
||||
.font(.system(size: 22))
|
||||
.padding(12)
|
||||
|
||||
if !self.candidates.isEmpty {
|
||||
Divider()
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.vertical) {
|
||||
Group {
|
||||
ForEach(self.candidates) { candidate in
|
||||
ActionCommandView(command: candidate.command, matchedRanges: candidate.result.ranges)
|
||||
.selected(candidate.id == self.selection)
|
||||
.id(candidate.id)
|
||||
.onTapGesture {
|
||||
self.selection = candidate.id
|
||||
self.perform()
|
||||
}
|
||||
}
|
||||
}.padding(.horizontal, 12)
|
||||
}.onChange(of: self.selection) { id in
|
||||
proxy.scrollTo(id)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.frame(maxHeight: 300)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
.onChange(of: self.command) { command in
|
||||
self.candidates = self.commands
|
||||
.compactMap {
|
||||
guard let result = $0.title.abbreviatedMatch(with: command) else { return nil }
|
||||
return Candidate(command: $0, result: result)
|
||||
}
|
||||
.sorted(\.result.score)
|
||||
self.selection = self.candidates.first?.id
|
||||
}
|
||||
.onChange(of: self.controlActiveState) { state in
|
||||
switch state {
|
||||
case .key, .active:
|
||||
self.commands = NSApp.mainMenu?.items
|
||||
.flatMap(\.actionCommands) ?? []
|
||||
self.keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
|
||||
if let key = event.specialKey,
|
||||
event.modifierFlags.isDisjoint(with: [.shift, .control, .option, .command])
|
||||
{
|
||||
switch key {
|
||||
case .downArrow, .upArrow:
|
||||
self.move(down: (key == .downArrow))
|
||||
return nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
case .inactive:
|
||||
self.command = ""
|
||||
self.candidates.removeAll()
|
||||
if let keyMonitor {
|
||||
NSEvent.removeMonitor(keyMonitor)
|
||||
self.keyMonitor = nil
|
||||
}
|
||||
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.frame(width: 500)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
|
||||
|
||||
/// Move the selection to the next one, if any exists.
|
||||
///
|
||||
/// - Parameter down: Whether move down or up the selection.
|
||||
private func move(down: Bool) {
|
||||
|
||||
guard
|
||||
let index = self.candidates.firstIndex(where: { $0.id == self.selection }),
|
||||
let candidate = self.candidates[safe: index + (down ? 1 : -1)]
|
||||
else { return }
|
||||
|
||||
self.selection = candidate.id
|
||||
}
|
||||
|
||||
|
||||
/// Perform the selected command and close the view.
|
||||
private func perform() {
|
||||
|
||||
if let command = self.candidates.first(where: { $0.id == self.selection })?.command {
|
||||
command.perform()
|
||||
}
|
||||
self.parent?.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private struct ActionCommandView: View {
|
||||
|
||||
@Environment(\.colorSchemeContrast) var colorContrast
|
||||
|
||||
let command: ActionCommand
|
||||
let matchedRanges: [Range<String.Index>]
|
||||
|
||||
var isSelected = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
|
||||
HStack {
|
||||
Image(systemName: self.command.kind.systemImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.fontWeight(.light)
|
||||
.foregroundStyle(self.isSelected ? .primary : .secondary)
|
||||
.frame(width: 32, height: 20, alignment: .center)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Text(self.attributed(self.command.title, in: self.matchedRanges, font: .body))
|
||||
}
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.foregroundStyle((self.isSelected && self.colorContrast == .standard) ? Color.selectedMenuItemText.opacity(0.8) : .primary)
|
||||
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
ForEach(Array(self.command.paths.enumerated()), id: \.offset) { (offset, path) in
|
||||
if offset > 0 {
|
||||
Image(systemName: "chevron.compact.right")
|
||||
.controlSize(.mini)
|
||||
.opacity(0.5)
|
||||
}
|
||||
Text(path)
|
||||
}
|
||||
}
|
||||
.foregroundStyle(self.isSelected ? .primary : .secondary)
|
||||
.controlSize(.small)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let shortcut = self.command.shortcut {
|
||||
Text(shortcut.symbol)
|
||||
.foregroundStyle(self.isSelected ? .primary : .secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal)
|
||||
.contentShape(Rectangle()) // for click
|
||||
.foregroundStyle(self.isSelected ? Color.selectedMenuItemText : .primary)
|
||||
.background(self.isSelected ? Color.accentColor : .clear,
|
||||
in: RoundedRectangle(cornerRadius: 6))
|
||||
}
|
||||
|
||||
|
||||
/// Set the selecting state of the receiver.
|
||||
///
|
||||
/// - Parameter selected: The selecting state to change.
|
||||
func selected(_ selected: Bool = true) -> Self {
|
||||
|
||||
var view = self
|
||||
view.isSelected = selected
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
/// Return an AttributedString by highlighting the given ranges by taking the view states into account.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - string: The base string to create the attributed string.
|
||||
/// - ranges: The ranges for the given `string` to highlight.
|
||||
/// - font: The base font.
|
||||
/// - Returns: An attributed string.
|
||||
private func attributed(_ string: String, in ranges: [Range<String.Index>], font: Font) -> AttributedString {
|
||||
|
||||
let attributed = AttributedString(string)
|
||||
|
||||
return ranges
|
||||
.compactMap { Range($0, in: attributed) }
|
||||
.reduce(into: attributed) { (string, range) in
|
||||
string[range].font = font.weight(.bold)
|
||||
string[range].foregroundColor = self.isSelected ? Color.selectedMenuItemText : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension ActionCommand.Kind {
|
||||
|
||||
var systemImage: String {
|
||||
|
||||
switch self {
|
||||
case .command: "filemenu.and.selection"
|
||||
case .outline: "list.bullet.rectangle"
|
||||
case .script: "applescript.fill"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private extension Color {
|
||||
|
||||
static let selectedMenuItemText = Color(nsColor: .selectedMenuItemTextColor)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
let candidates: [QuickActionView.Candidate] = [
|
||||
.init(command: .init(kind: .command, title: "Find…",
|
||||
paths: ["Find"],
|
||||
shortcut: Shortcut("f", modifiers: .command),
|
||||
action: #selector(NSResponder.yank)),
|
||||
result: String.AbbreviatedMatchResult([], 0)),
|
||||
.init(command: .init(kind: .command, title: "Fortran",
|
||||
paths: ["Format", "Syntax"],
|
||||
action: #selector(NSResponder.yank)),
|
||||
result: String.AbbreviatedMatchResult([], 0)),
|
||||
]
|
||||
|
||||
return QuickActionView(candidates: candidates)
|
||||
}
|
||||
|
||||
|
||||
#Preview("Command View") {
|
||||
ActionCommandView(
|
||||
command: .init(kind: .command, title: "Swift",
|
||||
paths: ["Format", "Syntax"],
|
||||
shortcut: Shortcut("s", modifiers: [.command]),
|
||||
action: #selector(NSResponder.yank)),
|
||||
matchedRanges: [],
|
||||
isSelected: true
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(width: 300)
|
||||
.padding(12)
|
||||
}
|
131
CotEditor/Sources/QuickActionWindowController.swift
Normal file
131
CotEditor/Sources/QuickActionWindowController.swift
Normal file
@ -0,0 +1,131 @@
|
||||
//
|
||||
// QuickActionWindowController.swift
|
||||
//
|
||||
// CotEditor
|
||||
// https://coteditor.com
|
||||
//
|
||||
// Created by 1024jp on 2023-11-20.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// © 2023 1024jp
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
final class QuickActionWindowController: NSWindowController {
|
||||
|
||||
static let shared = QuickActionWindowController()
|
||||
|
||||
|
||||
init() {
|
||||
|
||||
let view = NSHostingViewSuppressingSafeArea(rootView: QuickActionView())
|
||||
let panel = CommandBarPanel(contentRect: .zero, styleMask: [.titled, .fullSizeContentView], backing: .buffered, defer: false)
|
||||
panel.contentView = view
|
||||
panel.animationBehavior = .utilityWindow
|
||||
panel.collectionBehavior.insert(.fullScreenAuxiliary)
|
||||
panel.isFloatingPanel = true
|
||||
panel.isMovableByWindowBackground = true
|
||||
panel.titlebarAppearsTransparent = true
|
||||
panel.titleVisibility = .hidden
|
||||
panel.standardWindowButton(.closeButton)?.isHidden = true
|
||||
panel.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
panel.center()
|
||||
view.rootView.parent = panel
|
||||
|
||||
super.init(window: panel)
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private final class CommandBarPanel: NSPanel {
|
||||
|
||||
override func cancelOperation(_ sender: Any?) {
|
||||
|
||||
super.cancelOperation(sender)
|
||||
|
||||
// -> Needs to close manually when a panel does not have a close button.
|
||||
self.close()
|
||||
}
|
||||
|
||||
|
||||
override func resignKey() {
|
||||
|
||||
super.resignKey()
|
||||
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Workaround for the issue that a window still keeps content height for the title bar even with `.ignoresSafeArea()`.
|
||||
private final class NSHostingViewSuppressingSafeArea<T: View>: NSHostingView<T> {
|
||||
|
||||
private lazy var layoutGuide = NSLayoutGuide()
|
||||
|
||||
|
||||
required init(rootView: T) {
|
||||
|
||||
super.init(rootView: rootView)
|
||||
|
||||
self.addLayoutGuide(self.layoutGuide)
|
||||
NSLayoutConstraint.activate([
|
||||
self.leadingAnchor.constraint(equalTo: self.layoutGuide.leadingAnchor),
|
||||
self.topAnchor.constraint(equalTo: self.layoutGuide.topAnchor),
|
||||
self.trailingAnchor.constraint(equalTo: self.layoutGuide.trailingAnchor),
|
||||
self.bottomAnchor.constraint(equalTo: self.layoutGuide.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
override var safeAreaRect: NSRect {
|
||||
|
||||
self.frame
|
||||
}
|
||||
|
||||
|
||||
override var safeAreaInsets: NSEdgeInsets {
|
||||
|
||||
.zero
|
||||
}
|
||||
|
||||
|
||||
override var safeAreaLayoutGuide: NSLayoutGuide {
|
||||
|
||||
self.layoutGuide
|
||||
}
|
||||
|
||||
|
||||
override var additionalSafeAreaInsets: NSEdgeInsets {
|
||||
|
||||
get { .zero }
|
||||
set { _ = newValue }
|
||||
}
|
||||
}
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "Schnellaktionen";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "Der absolute Pfad der abgelegten Datei.";
|
||||
|
@ -828,6 +828,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "Quick Actions";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "The dropped file absolute path.";
|
||||
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "Acciones rápidas";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "La ruta absoluta del archivo eliminado.";
|
||||
|
@ -830,6 +830,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "Actions rapides";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "Le chemin absolu du fichier déposé.";
|
||||
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "Azioni rapide";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "Percorso assoluto del file trascinato.";
|
||||
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "クイックアクション";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "ドロップ元ファイルの絶対パス";
|
||||
|
@ -1297,6 +1297,78 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"7Su-OV-TgR.title" : {
|
||||
"comment" : "Class = \"NSMenuItem\"; title = \"Show Quick Actions…\"; ObjectID = \"7Su-OV-TgR\";",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Schnellaktionen einblenden …"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Show Quick Actions…"
|
||||
}
|
||||
},
|
||||
"en-GB" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Show Quick Actions…"
|
||||
}
|
||||
},
|
||||
"es" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mostrar las acciones rápidas…"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Afficher les actions rapides…"
|
||||
}
|
||||
},
|
||||
"it" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mostra azioni rapide…"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "クイックアクションを表示…"
|
||||
}
|
||||
},
|
||||
"pt" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mostrar Ações Rápidas…"
|
||||
}
|
||||
},
|
||||
"tr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Hızlı Eylemler’i Göster…"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "显示快速操作…"
|
||||
}
|
||||
},
|
||||
"zh-Hant" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "顯示快速動作…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"8z7-Jc-agP.title" : {
|
||||
"comment" : "Class = \"NSMenuItem\"; title = \"Theme\"; ObjectID = \"8z7-Jc-agP\";",
|
||||
"extractionState" : "extracted_with_value",
|
||||
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "Ações Rápidas";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "O caminho absoluto do arquivo solto.";
|
||||
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "Hızlı Eylemler";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "Bırakılan dosya mutlak yolu.";
|
||||
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "快速操作";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "拖拽的文件绝对路径";
|
||||
|
@ -829,6 +829,11 @@
|
||||
|
||||
|
||||
|
||||
/* MARK: Quick Action Bar */
|
||||
"Quick Actions" = "快速動作";
|
||||
|
||||
|
||||
|
||||
/* MARK: FileDropItem */
|
||||
// Descriptions about variables in the file drop feature
|
||||
"The dropped file absolute path." = "拖拽的檔案絕對路徑";
|
||||
|
Loading…
Reference in New Issue
Block a user