From 6dcb24144e9d1fdf7387308dfe4296a353f6b8dc Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 23 Apr 2026 16:37:32 +0200 Subject: [PATCH] icons from extensions, settings-based tab ordering, static context tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Icons now declared on TabContribution via the icon field — the hardcoded switch in _BottomRail is a fallback only. All sidebar and context extensions set their Phosphor icon explicitly. PanelRegistry supports setTabOrder(slot, List) — tabs render in the order specified by project.layout.sidebar.order / context.order in settings.yaml. Falls back to registration order. Priority field removed from all contributions. Context tabs are static again — persist with their last content, selection just loads new data and switches focus. No spawn/despawn. Tickets sidebar priority set to -200 (leftmost, default open). Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/builtin/decisions/src/extension.dart | 67 +++---------------- lib/builtin/default_layout/src/extension.dart | 8 +++ lib/builtin/files/src/extension.dart | 2 + lib/builtin/git/src/extension.dart | 2 + lib/builtin/pql/src/extension.dart | 56 +++------------- lib/builtin/problems/src/extension.dart | 2 + lib/builtin/tickets/src/extension.dart | 67 +++---------------- lib/kernel/src/panels/registry.dart | 35 ++++++---- 8 files changed, 66 insertions(+), 173 deletions(-) diff --git a/lib/builtin/decisions/src/extension.dart b/lib/builtin/decisions/src/extension.dart index dec06b9f..a15385b3 100644 --- a/lib/builtin/decisions/src/extension.dart +++ b/lib/builtin/decisions/src/extension.dart @@ -1,11 +1,8 @@ -import 'dart:async'; - import 'package:clide/builtin/decisions/src/decision_detail_view.dart'; import 'package:clide/builtin/decisions/src/decisions_view.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; -import 'package:clide/kernel/src/events/message_bus.dart'; -import 'package:flutter/foundation.dart' show VoidCallback; +import 'package:clide/widgets/widgets.dart'; class DecisionsExtension extends ClideExtension { @override @@ -13,15 +10,10 @@ class DecisionsExtension extends ClideExtension { @override String get title => 'Decisions'; @override - String get version => '0.3.0'; + String get version => '0.5.0'; @override List get dependsOn => const []; - ClideExtensionContext? _ctx; - StreamSubscription? _selectionSub; - VoidCallback? _panelListener; - bool _detailSpawned = false; - @override List get contributions => [ TabContribution( @@ -30,54 +22,15 @@ class DecisionsExtension extends ClideExtension { title: 'Decisions', titleKey: 'tab.title', i18nNamespace: id, - priority: -20, + icon: PhosphorIcons.lightbulb, build: (_) => const DecisionsView(), ), + TabContribution( + id: 'decisions.detail', + slot: Slots.contextPanel, + title: 'Decision', + icon: PhosphorIcons.lightbulb, + build: (_) => const DecisionDetailView(), + ), ]; - - @override - Future activate(ClideExtensionContext ctx) async { - _ctx = ctx; - _selectionSub = ctx.messages.subscribe(publisher: id, channel: 'selection').listen(_onSelection); - _panelListener = () { - if (_detailSpawned && ctx.panels.activeTabIn(Slots.contextPanel) != 'decisions.detail') { - _despawnDetail(); - } - }; - ctx.panels.addListener(_panelListener!); - } - - @override - Future deactivate() async { - _selectionSub?.cancel(); - if (_panelListener != null) _ctx?.panels.removeListener(_panelListener!); - _despawnDetail(); - } - - void _onSelection(Message msg) { - final ctx = _ctx; - if (ctx == null) return; - final selectedId = msg.data['id'] as String?; - if (selectedId == null) return; - - if (_detailSpawned) { - _despawnDetail(); - } - ctx.panels.contribute(TabContribution( - id: 'decisions.detail', - slot: Slots.contextPanel, - title: 'Decision', - priority: -50, - build: (_) => DecisionDetailView(initialId: selectedId), - )); - _detailSpawned = true; - ctx.panels.activateTab(Slots.contextPanel, 'decisions.detail'); - } - - void _despawnDetail() { - if (_detailSpawned) { - _ctx?.panels.uncontribute('decisions.detail'); - _detailSpawned = false; - } - } } diff --git a/lib/builtin/default_layout/src/extension.dart b/lib/builtin/default_layout/src/extension.dart index 4e094d9c..7b9abe2a 100644 --- a/lib/builtin/default_layout/src/extension.dart +++ b/lib/builtin/default_layout/src/extension.dart @@ -120,6 +120,14 @@ class DefaultLayoutExtension extends ClideExtension { void _restoreLayout(ClideExtensionContext ctx) { final s = ctx.settings; + final sidebarOrder = s.get('project.layout.sidebar.order'); + if (sidebarOrder != null) { + ctx.panels.setTabOrder(Slots.sidebar, sidebarOrder.cast()); + } + final contextOrder = s.get('project.layout.context.order'); + if (contextOrder != null) { + ctx.panels.setTabOrder(Slots.contextPanel, contextOrder.cast()); + } final sidebarCollapsed = s.get(_kSidebarCollapsed); if (sidebarCollapsed != null) { ctx.arrangement.setCollapsed(Slots.sidebar, sidebarCollapsed); diff --git a/lib/builtin/files/src/extension.dart b/lib/builtin/files/src/extension.dart index a78b6808..59b6be34 100644 --- a/lib/builtin/files/src/extension.dart +++ b/lib/builtin/files/src/extension.dart @@ -1,6 +1,7 @@ import 'package:clide/builtin/files/src/file_tree_view.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; +import 'package:clide/widgets/widgets.dart'; /// Workspace filesystem panel. Contributes a sidebar tab that renders /// the workspace file tree rooted at the git root, powered by the @@ -22,6 +23,7 @@ class FilesExtension extends ClideExtension { id: 'files.tree', slot: Slots.sidebar, title: 'Files', + icon: PhosphorIcons.folder, titleKey: 'tab.title', i18nNamespace: id, priority: -100, diff --git a/lib/builtin/git/src/extension.dart b/lib/builtin/git/src/extension.dart index a5d0e796..ec9b523e 100644 --- a/lib/builtin/git/src/extension.dart +++ b/lib/builtin/git/src/extension.dart @@ -2,6 +2,7 @@ import 'package:clide/builtin/git/src/git_panel_view.dart'; import 'package:clide/builtin/git/src/git_status_item.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; +import 'package:clide/widgets/widgets.dart'; class GitExtension extends ClideExtension { @override @@ -19,6 +20,7 @@ class GitExtension extends ClideExtension { id: 'git.panel', slot: Slots.sidebar, title: 'Git', + icon: PhosphorIcons.gitBranch, titleKey: 'tab.title', i18nNamespace: id, priority: -80, diff --git a/lib/builtin/pql/src/extension.dart b/lib/builtin/pql/src/extension.dart index 32985f2e..fc070b6d 100644 --- a/lib/builtin/pql/src/extension.dart +++ b/lib/builtin/pql/src/extension.dart @@ -1,9 +1,8 @@ -import 'dart:async'; - import 'package:clide/builtin/pql/src/backlinks_view.dart'; import 'package:clide/builtin/pql/src/pql_panel_view.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; +import 'package:clide/widgets/widgets.dart'; class PqlExtension extends ClideExtension { @override @@ -11,14 +10,10 @@ class PqlExtension extends ClideExtension { @override String get title => 'pql'; @override - String get version => '0.2.0'; + String get version => '0.3.0'; @override List get dependsOn => const []; - ClideExtensionContext? _ctx; - StreamSubscription? _editorSub; - bool _backlinksSpawned = false; - @override List get contributions => [ TabContribution( @@ -27,46 +22,17 @@ class PqlExtension extends ClideExtension { title: 'pql', titleKey: 'tab.title', i18nNamespace: id, + icon: PhosphorIcons.magnifyingGlass, priority: -60, build: (_) => const PqlPanelView(), ), + TabContribution( + id: 'pql.backlinks', + slot: Slots.contextPanel, + title: 'Links', + icon: PhosphorIcons.link, + priority: -80, + build: (_) => const BacklinksView(), + ), ]; - - @override - Future activate(ClideExtensionContext ctx) async { - _ctx = ctx; - _editorSub = ctx.events.on().listen((e) { - if (e.subsystem != 'editor') return; - if (e.kind == 'editor.active-changed' || e.kind == 'editor.opened') { - _spawnBacklinks(); - } - }); - } - - @override - Future deactivate() async { - _editorSub?.cancel(); - _despawnBacklinks(); - } - - void _spawnBacklinks() { - final ctx = _ctx; - if (ctx == null || _backlinksSpawned) return; - ctx.panels.contribute(TabContribution( - id: 'pql.backlinks', - slot: Slots.contextPanel, - title: 'Links', - priority: -80, - build: (_) => const BacklinksView(), - )); - _backlinksSpawned = true; - ctx.panels.activateTab(Slots.contextPanel, 'pql.backlinks'); - } - - void _despawnBacklinks() { - if (_backlinksSpawned) { - _ctx?.panels.uncontribute('pql.backlinks'); - _backlinksSpawned = false; - } - } } diff --git a/lib/builtin/problems/src/extension.dart b/lib/builtin/problems/src/extension.dart index 5226ff96..b1897232 100644 --- a/lib/builtin/problems/src/extension.dart +++ b/lib/builtin/problems/src/extension.dart @@ -1,6 +1,7 @@ import 'package:clide/builtin/problems/src/problems_view.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; +import 'package:clide/widgets/widgets.dart'; class ProblemsExtension extends ClideExtension { @override @@ -18,6 +19,7 @@ class ProblemsExtension extends ClideExtension { id: 'problems.panel', slot: Slots.sidebar, title: 'Problems', + icon: PhosphorIcons.warningCircle, titleKey: 'tab.title', i18nNamespace: id, priority: -50, diff --git a/lib/builtin/tickets/src/extension.dart b/lib/builtin/tickets/src/extension.dart index f8076e97..e5f0f903 100644 --- a/lib/builtin/tickets/src/extension.dart +++ b/lib/builtin/tickets/src/extension.dart @@ -1,11 +1,8 @@ -import 'dart:async'; - import 'package:clide/builtin/tickets/src/ticket_detail_view.dart'; import 'package:clide/builtin/tickets/src/tickets_view.dart'; import 'package:clide/extension/extension.dart'; import 'package:clide/kernel/kernel.dart'; -import 'package:clide/kernel/src/events/message_bus.dart'; -import 'package:flutter/foundation.dart' show VoidCallback; +import 'package:clide/widgets/widgets.dart'; class TicketsExtension extends ClideExtension { @override @@ -13,15 +10,10 @@ class TicketsExtension extends ClideExtension { @override String get title => 'Tickets'; @override - String get version => '0.4.0'; + String get version => '0.5.0'; @override List get dependsOn => const []; - ClideExtensionContext? _ctx; - StreamSubscription? _selectionSub; - VoidCallback? _panelListener; - bool _detailSpawned = false; - @override List get contributions => [ TabContribution( @@ -30,54 +22,15 @@ class TicketsExtension extends ClideExtension { title: 'Tickets', titleKey: 'tab.title', i18nNamespace: id, - priority: -10, + icon: PhosphorIcons.ticket, build: (_) => const TicketsView(), ), + TabContribution( + id: 'tickets.detail', + slot: Slots.contextPanel, + title: 'Ticket', + icon: PhosphorIcons.ticket, + build: (_) => const TicketDetailView(), + ), ]; - - @override - Future activate(ClideExtensionContext ctx) async { - _ctx = ctx; - _selectionSub = ctx.messages.subscribe(publisher: id, channel: 'selection').listen(_onSelection); - _panelListener = () { - if (_detailSpawned && ctx.panels.activeTabIn(Slots.contextPanel) != 'tickets.detail') { - _despawnDetail(); - } - }; - ctx.panels.addListener(_panelListener!); - } - - @override - Future deactivate() async { - _selectionSub?.cancel(); - if (_panelListener != null) _ctx?.panels.removeListener(_panelListener!); - _despawnDetail(); - } - - void _onSelection(Message msg) { - final ctx = _ctx; - if (ctx == null) return; - final selectedId = msg.data['id'] as String?; - if (selectedId == null) return; - - if (_detailSpawned) { - _despawnDetail(); - } - ctx.panels.contribute(TabContribution( - id: 'tickets.detail', - slot: Slots.contextPanel, - title: 'Ticket', - priority: -60, - build: (_) => TicketDetailView(initialId: selectedId), - )); - _detailSpawned = true; - ctx.panels.activateTab(Slots.contextPanel, 'tickets.detail'); - } - - void _despawnDetail() { - if (_detailSpawned) { - _ctx?.panels.uncontribute('tickets.detail'); - _detailSpawned = false; - } - } } diff --git a/lib/kernel/src/panels/registry.dart b/lib/kernel/src/panels/registry.dart index 16c5c0d2..0c47af46 100644 --- a/lib/kernel/src/panels/registry.dart +++ b/lib/kernel/src/panels/registry.dart @@ -23,6 +23,7 @@ class PanelRegistry extends ChangeNotifier { final Map _defs = {}; final Map> _mounts = {}; final Map _activeTab = {}; + final Map> _order = {}; void registerSlot(SlotDefinition def) { _defs[def.id] = def; @@ -30,14 +31,16 @@ class PanelRegistry extends ChangeNotifier { notifyListeners(); } + void setTabOrder(SlotId slot, List order) { + _order[slot] = order; + notifyListeners(); + } + void contribute(ContributionPoint point) { final slot = point.slot; - if (slot == null) return; // non-slot contributions go elsewhere + if (slot == null) return; final list = _mounts.putIfAbsent(slot, () => []); list.add(point); - list.sort((a, b) => _priority(a).compareTo(_priority(b))); - // first tab-contribution in the sidebar/workspace/context becomes the - // default active tab until the user picks another if (_activeTab[slot] == null && point is TabContribution) { _activeTab[slot] = point.id; } @@ -66,8 +69,20 @@ class PanelRegistry extends ChangeNotifier { List contributionsFor(SlotId id) => List.unmodifiable(_mounts[id] ?? const []); - List tabsFor(SlotId id) => - contributionsFor(id).whereType().toList(); + List tabsFor(SlotId id) { + final tabs = contributionsFor(id).whereType().toList(); + final order = _order[id]; + if (order == null || order.isEmpty) return tabs; + tabs.sort((a, b) { + final ai = order.indexOf(a.id); + final bi = order.indexOf(b.id); + if (ai < 0 && bi < 0) return 0; + if (ai < 0) return 1; + if (bi < 0) return 1; + return ai.compareTo(bi); + }); + return tabs; + } String? activeTabIn(SlotId id) => _activeTab[id]; @@ -76,12 +91,4 @@ class PanelRegistry extends ChangeNotifier { _activeTab[id] = tabId; notifyListeners(); } - - int _priority(ContributionPoint p) { - if (p is TabContribution) return p.priority; - if (p is StatusItemContribution) return p.priority; - if (p is ToolbarButtonContribution) return p.priority; - if (p is TrayItemContribution) return p.priority; - return 0; - } }