diff --git a/Sources/MrEditorCore/MainWindowController.swift b/Sources/MrEditorCore/MainWindowController.swift index c50526e..208912f 100644 --- a/Sources/MrEditorCore/MainWindowController.swift +++ b/Sources/MrEditorCore/MainWindowController.swift @@ -1256,14 +1256,24 @@ public final class MainWindowController: NSWindowController, NSWindowDelegate { persistSession() } - /// 読み取り専用バナーの表示可否を更新する(編集不可のペイン=LargeFileViewer のときだけ出す)。 + /// 読み取り専用バナーの表示可否・文面を更新する。 + /// **理由によって文面を変える**──フィルタ中や JSON クエリ中の小さなファイルにまで + /// 「大きすぎる」を出すと、その場で嘘になってバグに見える(本人の指摘、2026-09-24)。 private func updateReadOnlyBanner() { // 構造化表示による読み取り専用は専用バナーで案内するため除外する。 // diff も除外する(「大きすぎて編集できません」は嘘。そもそも編集する画面ではない)。 - let isReadOnly = activeViewer != nil && !(activeViewer?.canEdit ?? false) - && activeViewer?.structuredMode == nil - && !(activeViewer is DiffViewer) - readOnlyBanner.isHidden = !(isReadOnly && !readOnlyBannerDismissed) + guard let v = activeViewer, !v.canEdit, v.structuredMode == nil, !(v is DiffViewer) else { + readOnlyBanner.isHidden = true + return + } + if v.filterMatchLines != nil { + readOnlyBanner.reason = .filtered + } else if v.jsonQueryIsActive { + readOnlyBanner.reason = .jsonQuery + } else { + readOnlyBanner.reason = .tooLarge + } + readOnlyBanner.isHidden = readOnlyBannerDismissed } /// アクティブなドキュメントを閉じる(なければウィンドウを閉じる)。未保存なら確認する。 @@ -1664,11 +1674,15 @@ public final class MainWindowController: NSWindowController, NSWindowDelegate { func showSearch() { guard let v = activeViewer, v.supportsSearch else { NSSound.beep(); return } - refreshSearchBarCapabilities() searchBar.isHidden = false // 前に自分で漏斗を入れていたなら、その状態で開く。構造化されたものを読むとき // 主目的は「絞る」ほうで、そこへ毎回 1 手かけ直すのが B2 の詰まりだった。 + // **置換の可否を引き直すより先に当てる**── 順序が逆だと、フィルタ適用前の + // 「置換できる」判定のまま置換ボタンが有効に残り、押すと canEdit のガードで + // 問答無用にビープする(フィルタ中に編集を許さないのは正しいが、ボタンだけ + // 古い状態のままになる不具合だった)。 applyRememberedFilter(to: v) + refreshSearchBarCapabilities() searchBar.focusField() } @@ -1691,6 +1705,8 @@ public final class MainWindowController: NSWindowController, NSWindowDelegate { // 漏斗が使えるペインへ戻ってきたら、覚えている意図をもう一度当てる。 // 検索バーが出ている間だけ ── 閉じているのに本文が絞られるのは事故に見える。 if !searchBar.isHidden { applyRememberedFilter(to: v) } + // フィルタで置換が落ちたのが**なぜか**を言う(本人の指摘: 黙って落とすとバグに見える)。 + updateReadOnlyBanner() } /// 一致行の前後に出す行数を変える(検索バーの「±」欄・メニューの増減)。 @@ -1778,6 +1794,7 @@ public final class MainWindowController: NSWindowController, NSWindowDelegate { v.setCaseSensitive(true) v.setSearchQuery(value) v.setFilterMode(true) + refreshSearchBarCapabilities() } /// 時間帯で選んだ行だけを表示する(時間分布のドラッグ選択)。行は **0 始まり・昇順**。 @@ -1804,6 +1821,7 @@ public final class MainWindowController: NSWindowController, NSWindowDelegate { v.focusContent() } searchBar.clear() + updateReadOnlyBanner() // フィルタを解いたので、それで出ていたバナーも引っ込める } // MARK: - 開く @@ -1937,8 +1955,8 @@ extension MainWindowController: NSToolbarItemValidation, NSMenuDelegate { guard let v = activeViewer, v.supportsSearch, v.supportsSearchFilter else { NSSound.beep(); return } showSearch() searchBar.setFilterOn(true) - refreshSearchBarCapabilities() v.setFilterMode(true) + refreshSearchBarCapabilities() } @objc func toolbarSetStructuredMode(_ sender: Any?) { diff --git a/Sources/MrEditorCore/Resources/en.lproj/Localizable.strings b/Sources/MrEditorCore/Resources/en.lproj/Localizable.strings index 0e3ad9c..d98165a 100644 --- a/Sources/MrEditorCore/Resources/en.lproj/Localizable.strings +++ b/Sources/MrEditorCore/Resources/en.lproj/Localizable.strings @@ -60,6 +60,8 @@ "doc.untitled" = "Untitled"; "sidebar.close" = "Close"; "readonly.banner" = "Read-only — this file is too large to edit."; +"readonly.banner.filter" = "Read-only — can't edit while showing matching lines only (filter)."; +"readonly.banner.jsonQuery" = "Read-only — can't edit while the JSON query is open."; /* Status bar */ "status.placeholder" = "Drag a file here, or press ⌘O to open"; diff --git a/Sources/MrEditorCore/Resources/ja.lproj/Localizable.strings b/Sources/MrEditorCore/Resources/ja.lproj/Localizable.strings index 5d45d0d..b24eb48 100644 --- a/Sources/MrEditorCore/Resources/ja.lproj/Localizable.strings +++ b/Sources/MrEditorCore/Resources/ja.lproj/Localizable.strings @@ -60,6 +60,8 @@ "doc.untitled" = "名称未設定"; "sidebar.close" = "閉じる"; "readonly.banner" = "読み取り専用 — このファイルは大きすぎて編集できません。"; +"readonly.banner.filter" = "読み取り専用 — 一致行だけ表示(フィルタ)中は編集できません。"; +"readonly.banner.jsonQuery" = "読み取り専用 — JSON その場クエリ中は編集できません。"; /* ステータスバー */ "status.placeholder" = "ファイルをドラッグ、または ⌘O で開く"; diff --git a/Sources/MrEditorCore/UI/ReadOnlyBanner.swift b/Sources/MrEditorCore/UI/ReadOnlyBanner.swift index f05c376..00f97d5 100644 --- a/Sources/MrEditorCore/UI/ReadOnlyBanner.swift +++ b/Sources/MrEditorCore/UI/ReadOnlyBanner.swift @@ -8,6 +8,29 @@ final class ReadOnlyBanner: NSView { static let height: CGFloat = 26 + private let label = NSTextField(labelWithString: L("readonly.banner")) + + /// なぜ編集できないか。**理由によって文面を変える**──「大きすぎる」を、フィルタ中や + /// JSON その場クエリ中の小さなファイルにまで出すと、その場で嘘になって + /// 「バグでは」と誤解される(本人の指摘)。 + enum Reason { + case tooLarge + case filtered + case jsonQuery + + var messageKey: String { + switch self { + case .tooLarge: return "readonly.banner" + case .filtered: return "readonly.banner.filter" + case .jsonQuery: return "readonly.banner.jsonQuery" + } + } + } + + var reason: Reason = .tooLarge { + didSet { label.stringValue = L(reason.messageKey) } + } + override init(frame frameRect: NSRect) { super.init(frame: frameRect) setup() @@ -30,7 +53,6 @@ final class ReadOnlyBanner: NSView { icon.contentTintColor = .secondaryLabelColor icon.translatesAutoresizingMaskIntoConstraints = false - let label = NSTextField(labelWithString: L("readonly.banner")) label.font = .systemFont(ofSize: 11) label.textColor = .secondaryLabelColor label.translatesAutoresizingMaskIntoConstraints = false