using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.Maui.Controls; namespace CommanderApp; public partial class MainPage : ContentPage, INotifyPropertyChanged { private readonly IFileSystemService _fileService; public ObservableCollection LeftItems { get; } = new(); public ObservableCollection RightItems { get; } = new(); private string _leftPath = string.Empty; private string _rightPath = string.Empty; // Для управления выделением и фокусом private Button? _currentFocusedButton; private bool _isLeftPanelActive = true; private int _leftSelectedIndex = -1; private int _rightSelectedIndex = -1; public string LeftPath { get => _leftPath; set { if (_leftPath != value) { _leftPath = value; NotifyPropertyChanged(); } } } public string RightPath { get => _rightPath; set { if (_rightPath != value) { _rightPath = value; NotifyPropertyChanged(); } } } private string _currentLeftPath = string.Empty; private string _currentRightPath = string.Empty; private FileSystemItem? _selectedLeftItem; private FileSystemItem? _selectedRightItem; // Для двойного нажатия private FileSystemItem? _lastClickedItem; private int _lastLeftSelectedIndex = 0; private int _lastRightSelectedIndex = 0; private bool? _lastIsLeftPanel; public MainPage(IFileSystemService fileService) { InitializeComponent(); _fileService = fileService; BindingContext = this; // Инициализация шаблонов InitializeCollectionViews(); var root = _fileService.GetRootPath(); LoadDirectory(root, true); LoadDirectory(root, false); // Устанавливаем фокус на страницу this.Focus(); } private void InitializeCollectionViews() { LeftPanel.ItemTemplate = PanelCollectionView.CreateItemTemplate(isLeftPanel: true, page: this); RightPanel.ItemTemplate = PanelCollectionView.CreateItemTemplate(isLeftPanel: true, page: this); } protected override void OnHandlerChanged() { base.OnHandlerChanged(); this.Focus(); } public void HandleItemClick(FileSystemItem item, bool isLeftPanel) { if (_lastIsLeftPanel == isLeftPanel && _lastClickedItem == item) { // Двойной клик if (isLeftPanel) OnLeftItemDoubleTapped(item); else OnRightItemDoubleTapped(item); } else { // Одинарный клик - выделение if (isLeftPanel) { _isLeftPanelActive = true; _leftSelectedIndex = LeftItems.IndexOf(item); _selectedLeftItem = item; _selectedRightItem = null; } else { _isLeftPanelActive = false; _rightSelectedIndex = RightItems.IndexOf(item); _selectedRightItem = item; _selectedLeftItem = null; } UpdateVisualSelection(); } _lastIsLeftPanel = isLeftPanel; _lastClickedItem = item; } private void LoadDirectory(string path, bool isLeft) { var items = _fileService.GetDirectoryContents(path).ToList(); if (isLeft) { LeftItems.Clear(); foreach (var item in items) LeftItems.Add(item); _currentLeftPath = path; LeftPath = path; // Сбрасываем выделение при загрузке новой директории _leftSelectedIndex = items.Count > 0 ? 0 : -1; } else { RightItems.Clear(); foreach (var item in items) RightItems.Add(item); _currentRightPath = path; RightPath = path; _rightSelectedIndex = items.Count > 0 ? 0 : -1; } UpdateVisualSelection(); } // Переключаем фокус с указанием направления private void MoveFocus(bool moveForward = true) { var allFocusable = GetFocusableElements(); var current = allFocusable.FirstOrDefault(x => x.IsFocused); var next = moveForward ? GetNextFocusable(current, allFocusable) : GetPreviousFocusable(current, allFocusable); next?.Focus(); } // Следующий элемент (для Down/Tab) private View GetNextFocusable(View current, List allElements) { if (current == null) return allElements.FirstOrDefault(); var currentIndex = allElements.IndexOf(current); if (currentIndex == -1) return allElements.FirstOrDefault(); var nextIndex = (currentIndex + 1) % allElements.Count; return allElements[nextIndex]; } // Предыдущий элемент (для Up/Shift+Tab) private View GetPreviousFocusable(View current, List allElements) { if (current == null) return allElements.LastOrDefault(); var currentIndex = allElements.IndexOf(current); if (currentIndex == -1) return allElements.LastOrDefault(); var prevIndex = (currentIndex - 1 + allElements.Count) % allElements.Count; return allElements[prevIndex]; } private List GetFocusableElements() { return this.GetVisualTreeDescendants() .OfType() .Where(x => x.IsEnabled && x.IsVisible) .OrderBy(GetVisualTreeOrder) // Сортируем по порядку в визуальном дереве .ToList(); } private int GetVisualTreeOrder(View view) { // Простой способ - используем порядок в визуальном дереве var parent = view.Parent as Layout; if (parent != null) { var index = parent.Children.IndexOf(view); return index >= 0 ? index : 0; } return 0; } // Обработчики выделения (вызываются при изменении Selection в CollectionView) private void OnLeftSelectionChanged(object sender, SelectionChangedEventArgs e) { if (e.CurrentSelection.FirstOrDefault() is FileSystemItem selectedItem) { _leftSelectedIndex = LeftItems.IndexOf(selectedItem); _isLeftPanelActive = true; System.Diagnostics.Debug.WriteLine($"L !!! {_leftSelectedIndex} {_lastLeftSelectedIndex}"); if (_lastLeftSelectedIndex < LeftItems.Count && _lastLeftSelectedIndex >= 0) { // Автоматически переключаем фокус при достижении границ if (_leftSelectedIndex > _lastLeftSelectedIndex) { // Достигли конца списка - переходим к следующему элементу MoveFocus(moveForward: true); } else if (_leftSelectedIndex < _lastLeftSelectedIndex) { // Достигли начала списка - переходим к предыдущему элементу MoveFocus(moveForward: false); } } UpdateVisualSelection(); } } private void OnRightSelectionChanged(object sender, SelectionChangedEventArgs e) { if (e.CurrentSelection.FirstOrDefault() is FileSystemItem selectedItem) { _rightSelectedIndex = RightItems.IndexOf(selectedItem); _isLeftPanelActive = false; System.Diagnostics.Debug.WriteLine($"R !!! {_rightSelectedIndex} {_lastRightSelectedIndex}"); if (_rightSelectedIndex < RightItems.Count && _rightSelectedIndex >= 0) { // Автоматически переключаем фокус при достижении границ if (_rightSelectedIndex > _lastRightSelectedIndex) { // Достигли конца списка - переходим к следующему элементу MoveFocus(moveForward: true); } else if (_rightSelectedIndex < _lastRightSelectedIndex) { // Достигли начала списка - переходим к предыдущему элементу MoveFocus(moveForward: false); } } UpdateVisualSelection(); } } private void UpdateVisualSelection() { //Сбрасываем предыдущий фокус if (_currentFocusedButton != null) { VisualStateManager.GoToState(_currentFocusedButton, "Normal"); _currentFocusedButton = null; } if (_isLeftPanelActive && _leftSelectedIndex >= 0 && _leftSelectedIndex < LeftItems.Count) { var item = LeftItems[_leftSelectedIndex]; SetFocusToItem(LeftPanel, item); _selectedLeftItem = item; _selectedRightItem = null; } else if (!_isLeftPanelActive && _rightSelectedIndex >= 0 && _rightSelectedIndex < RightItems.Count) { var item = RightItems[_rightSelectedIndex]; SetFocusToItem(RightPanel, item); _selectedRightItem = item; _selectedLeftItem = null; } } private void SetFocusToItem(CollectionView collectionView, FileSystemItem item) { // Используем Dispatcher чтобы дождаться рендеринга //Dispatcher.Dispatch(() => //{ var container = FindButtonContainer(collectionView, item); if (container is Button button) { VisualStateManager.GoToState(button, "Focused"); _currentFocusedButton = button; _lastClickedItem = item; // Простой способ - используем текущие индексы if (_isLeftPanelActive) { _lastLeftSelectedIndex = _leftSelectedIndex; } else { _lastRightSelectedIndex = _rightSelectedIndex; } // Прокручиваем к выбранному элементу collectionView.ScrollTo(item, position: ScrollToPosition.MakeVisible, animate: false); } //}); } private Button? FindButtonContainer(CollectionView collectionView, FileSystemItem item) { // Ищем кнопку в логических дочерних элементах foreach (var child in collectionView.LogicalChildren) { if (child is Button button && button.BindingContext == item) { return button; } } return null; } public void OnLeftItemDoubleTapped(FileSystemItem item) { if (item.IsDirectory) { LoadDirectory(item.FullName, true); } } public void OnRightItemDoubleTapped(FileSystemItem item) { if (item.IsDirectory) { LoadDirectory(item.FullName, false); } } // Остальные методы без изменений private async void OnCopyClicked(object sender, EventArgs e) { await ProcessFileOperation(async (src, destDir) => { if (Directory.Exists(src)) await CopyDirectory(src, Path.Combine(destDir, Path.GetFileName(src))); else File.Copy(src, Path.Combine(destDir, Path.GetFileName(src)), overwrite: true); }, "Copy"); } private async void OnMoveClicked(object sender, EventArgs e) { await ProcessFileOperation((src, destDir) => { var dest = Path.Combine(destDir, Path.GetFileName(src)); if (Directory.Exists(src)) Directory.Move(src, dest); else File.Move(src, dest, overwrite: true); return Task.CompletedTask; }, "Move"); } private async void OnDeleteClicked(object sender, EventArgs e) { var item = _selectedLeftItem ?? _selectedRightItem; if (item == null) return; var confirm = await DisplayAlert("Delete", $"Delete '{item.Name}'?", "Yes", "No"); if (confirm) { if (item.IsDirectory) Directory.Delete(item.FullName, recursive: true); else File.Delete(item.FullName); LoadDirectory(_currentLeftPath, true); LoadDirectory(_currentRightPath, false); } } private async void OnMkdirClicked(object sender, EventArgs e) { var result = await DisplayPromptAsync("New Folder", "Folder name:", "Create", "Cancel"); if (string.IsNullOrWhiteSpace(result)) return; string targetPath = (_selectedLeftItem != null || LeftItems.Count > 0) ? _currentLeftPath : _currentRightPath; string newPath = Path.Combine(targetPath, result.Trim()); if (!Directory.Exists(newPath)) { Directory.CreateDirectory(newPath); LoadDirectory(_currentLeftPath, true); LoadDirectory(_currentRightPath, false); } else { await DisplayAlert("Error", "Folder already exists.", "OK"); } } private void OnExitClicked(object sender, EventArgs e) { Application.Current?.Quit(); } private async Task CopyDirectory(string sourceDir, string targetDir) { Directory.CreateDirectory(targetDir); foreach (var file in Directory.GetFiles(sourceDir)) { await Task.Run(() => File.Copy(file, Path.Combine(targetDir, Path.GetFileName(file)), overwrite: true)); } foreach (var dir in Directory.GetDirectories(sourceDir)) { await CopyDirectory(dir, Path.Combine(targetDir, Path.GetFileName(dir))); } } private async Task ProcessFileOperation(Func operation, string actionName) { var srcItem = _selectedLeftItem ?? _selectedRightItem; if (srcItem == null) { await DisplayAlert("Error", "Select a file or folder first.", "OK"); return; } string destDir = (srcItem == _selectedLeftItem) ? _currentRightPath : _currentLeftPath; try { await operation(srcItem.FullName, destDir); LoadDirectory(_currentLeftPath, true); LoadDirectory(_currentRightPath, false); } catch (Exception ex) { await DisplayAlert("Error", $"{actionName} failed: {ex.Message}", "OK"); } } public new event PropertyChangedEventHandler? PropertyChanged; protected virtual void NotifyPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }