// OpenTween - Client of Twitter // Copyright (c) 2014 spx (@5px) // Copyright (c) 2023 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace OpenTween { public partial class MediaSelectorPanel : UserControl { public event EventHandler? BeginSelecting; public event EventHandler? EndSelecting; public event EventHandler? FilePickDialogOpening; public event EventHandler? FilePickDialogClosed; public event EventHandler? SelectedServiceChanged; [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public MediaSelector Model { get; } = new(); [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public OpenFileDialog? FilePickDialog { get; set; } public MediaSelectorPanel() { this.InitializeComponent(); this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage; this.MediaListView.LargeImageList = this.Model.ThumbnailList.ImageList; var thumbnailWidth = 75 * this.DeviceDpi / 96; this.Model.ThumbnailList.ImageList.ColorDepth = ColorDepth.Depth24Bit; this.Model.ThumbnailList.ImageList.ImageSize = new(thumbnailWidth, thumbnailWidth); this.Model.PropertyChanged += (s, e) => this.TryInvoke(() => this.Model_PropertyChanged(s, e)); this.Model.MediaItems.ListChanged += (s, e) => this.TryInvoke(() => this.Model_MediaItems_ListChanged(s, e)); this.UpdateSelectedMedia(); this.UpdateAltTextPanelVisible(); } /// /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。 /// public void BeginSelection() { this.BeginSelecting?.Invoke(this, EventArgs.Empty); this.Enabled = true; this.Visible = true; } /// /// 選択処理を終了してコントロールを隠す。 /// public void EndSelection() { this.EndSelecting?.Invoke(this, EventArgs.Empty); this.Visible = false; this.Enabled = false; this.Model.ClearMediaItems(); } /// /// 選択された投稿先名と投稿する MediaItem を取得する。MediaItem は不要になったら呼び出し側にて破棄すること。 /// public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [NotNullWhen(true)] out IMediaItem[]? mediaItems) { var selectedServiceName = this.Model.SelectedMediaServiceName; var error = this.Model.Validate(out var rejectedMedia); if (error != MediaSelectorErrorType.None) { var message = error switch { MediaSelectorErrorType.MediaItemNotSet => Properties.Resources.PostPictureWarn1, MediaSelectorErrorType.ServiceNotSelected => Properties.Resources.PostPictureWarn1, MediaSelectorErrorType.UnsupportedFileExtension => string.Format( Properties.Resources.PostPictureWarn3, selectedServiceName, this.MakeAvailableServiceText(rejectedMedia!), rejectedMedia!.Extension, rejectedMedia!.Name ), MediaSelectorErrorType.FileSizeExceeded => string.Format( Properties.Resources.PostPictureWarn5, selectedServiceName, this.MakeAvailableServiceText(rejectedMedia!), rejectedMedia!.Name ), _ => throw new NotImplementedException(), }; MessageBox.Show( message, Properties.Resources.PostPictureWarn2, MessageBoxButtons.OK, MessageBoxIcon.Warning ); imageService = null; mediaItems = null; return false; } imageService = selectedServiceName; mediaItems = this.Model.DetachMediaItems(); return true; } private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { case nameof(MediaSelector.MediaServices): this.UpdateImageServiceComboItems(); break; case nameof(MediaSelector.SelectedMediaServiceName): this.UpdateImageServiceComboSelection(); this.UpdateAltTextPanelVisible(); this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty); break; case nameof(MediaSelector.SelectedMediaItemId): this.UpdateSelectedMedia(); break; case nameof(MediaSelector.SelectedMediaItemImage): this.UpdateSelectedMediaImage(); break; default: break; } } private void Model_MediaItems_ListChanged(object sender, ListChangedEventArgs e) { void AddMediaListViewItem(IMediaItem media, int index) => this.MediaListView.Items.Insert(index, media.Name, media.Id.ToString()); switch (e.ListChangedType) { case ListChangedType.ItemAdded: var addedMedia = this.Model.MediaItems[e.NewIndex]; AddMediaListViewItem(addedMedia, e.NewIndex); this.CorrectMediaListViewItemPositions(e.NewIndex); break; case ListChangedType.ItemDeleted: this.MediaListView.Items.RemoveAt(e.NewIndex); break; case ListChangedType.Reset: this.MediaListView.Items.Clear(); foreach (var (media, index) in this.Model.MediaItems.WithIndex()) AddMediaListViewItem(media, index); break; default: throw new NotImplementedException(); } } /// /// にアイテムを追加した後の座標を修正する /// /// /// でインデックスを指定してアイテムを追加した場合、 /// コレクションの中では正しい位置にアイテムが挿入されるが、 の座標は /// 挿入した位置にかかわらず常に最後尾のアイテムとして配置されてしまう。 /// このメソッドではアイテム追加後にコレクションのインデックスと表示上の順序が一致するように整列する処理を行う。 /// private void CorrectMediaListViewItemPositions(int startIndex) { if (startIndex == this.Model.MediaItems.Count - 1) return; var items = this.MediaListView.Items.Cast() .Skip(startIndex).ToArray(); var orderedPositions = items.Select(x => x.Position) .OrderBy(x => x.Y).ThenBy(x => x.X).ToArray(); this.MediaListView.AutoArrange = false; foreach (var i in Enumerable.Range(0, items.Length)) items[i].Position = orderedPositions[i]; this.MediaListView.AutoArrange = true; } private void UpdateImageServiceComboItems() { using (ControlTransaction.Update(this.ImageServiceCombo)) { this.ImageServiceCombo.Items.Clear(); // Add service names to combobox var serviceNames = this.Model.MediaServices.Select(x => x.Key).ToArray(); this.ImageServiceCombo.Items.AddRange(serviceNames); this.UpdateImageServiceComboSelection(); } } private void UpdateImageServiceComboSelection() => this.ImageServiceCombo.SelectedIndex = this.Model.SelectedMediaServiceIndex; private void AddMediaButton_Click(object sender, EventArgs e) { var service = this.Model.SelectedMediaService; if (this.FilePickDialog == null || service == null) return; this.FilePickDialog.Filter = service.SupportedFormatsStrForDialog; this.FilePickDialog.Title = Properties.Resources.PickPictureDialog1; this.FilePickDialog.FileName = ""; this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty); try { if (this.FilePickDialog.ShowDialog() == DialogResult.Cancel) return; } finally { this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty); } this.Model.AddMediaItemFromFilePath(this.FilePickDialog.FileNames); } private string MakeAvailableServiceText(IMediaItem media) { var ext = media.Extension; var fileSize = media.Size; var availableServiceNames = this.Model.GetAvailableServiceNames(ext, fileSize); if (availableServiceNames.Length == 0) return Properties.Resources.PostPictureWarn6; return string.Join(", ", availableServiceNames); } private void ImageCancelButton_Click(object sender, EventArgs e) => this.EndSelection(); private void UpdateAltTextPanelVisible() => this.AlternativeTextPanel.Visible = this.Model.CanUseAltText; private void UpdateSelectedMedia() { using (ControlTransaction.Update(this)) { var selectedMedia = this.Model.SelectedMediaItem; if (selectedMedia == null) { this.AlternativeTextBox.Text = ""; this.AlternativeTextPanel.Enabled = false; } else { this.AlternativeTextBox.Text = selectedMedia.AltText; this.AlternativeTextPanel.Enabled = true; } var index = this.Model.SelectedMediaItemIndex; var listViewSelectedIndex = this.MediaListView.SelectedIndices.Cast().DefaultIfEmpty(-1).Single(); if (listViewSelectedIndex != index) { this.MediaListView.SelectedIndices.Clear(); if (index != -1) this.MediaListView.SelectedIndices.Add(index); } } } private void UpdateSelectedMediaImage() => this.ImageSelectedPicture.Image = this.Model.SelectedMediaItemImage; private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e) => this.Model.SelectedMediaServiceName = this.ImageServiceCombo.Text; private void MediaListView_SelectedIndexChanged(object sender, EventArgs e) { var indices = this.MediaListView.SelectedIndices; if (indices.Count == 0) return; this.Model.SelectedMediaItemIndex = indices[0]; } private void AlternativeTextBox_Validated(object sender, EventArgs e) => this.Model.SetSelectedMediaAltText(this.AlternativeTextBox.Text); private void MoveToBackMenuItem_Click(object sender, EventArgs e) => this.Model.MoveSelectedMediaItemToPrevious(); private void MoveToNextMenuItem_Click(object sender, EventArgs e) => this.Model.MoveSelectedMediaItemToNext(); private void DeleteMediaMenuItem_Click(object sender, EventArgs e) => this.Model.RemoveSelectedMediaItem(); protected override void Dispose(bool disposing) { if (disposing) { this.components?.Dispose(); this.Model.Dispose(); } base.Dispose(disposing); } } }