OSDN Git Service

PostMultipartRequestクラスを追加
[opentween/open-tween.git] / OpenTween / MediaSelectorPanel.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 spx (@5px)
3 // Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
4 // All rights reserved.
5 //
6 // This file is part of OpenTween.
7 //
8 // This program is free software; you can redistribute it and/or modify it
9 // under the terms of the GNU General Public License as published by the Free
10 // Software Foundation; either version 3 of the License, or (at your option)
11 // any later version.
12 //
13 // This program is distributed in the hope that it will be useful, but
14 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
16 // for more details.
17 //
18 // You should have received a copy of the GNU General Public License along
19 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
20 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
21 // Boston, MA 02110-1301, USA.
22
23 #nullable enable
24
25 using System;
26 using System.Collections.Generic;
27 using System.ComponentModel;
28 using System.Data;
29 using System.Diagnostics.CodeAnalysis;
30 using System.Linq;
31 using System.Text;
32 using System.Threading.Tasks;
33 using System.Windows.Forms;
34
35 namespace OpenTween
36 {
37     public partial class MediaSelectorPanel : UserControl
38     {
39         public event EventHandler<EventArgs>? BeginSelecting;
40
41         public event EventHandler<EventArgs>? EndSelecting;
42
43         public event EventHandler<EventArgs>? FilePickDialogOpening;
44
45         public event EventHandler<EventArgs>? FilePickDialogClosed;
46
47         public event EventHandler<EventArgs>? SelectedServiceChanged;
48
49         [Browsable(false)]
50         [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
51         public MediaSelector Model { get; } = new();
52
53         [Browsable(false)]
54         [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
55         public OpenFileDialog? FilePickDialog { get; set; }
56
57         public MediaSelectorPanel()
58         {
59             this.InitializeComponent();
60
61             this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage;
62
63             this.MediaListView.LargeImageList = this.Model.ThumbnailList.ImageList;
64
65             var thumbnailWidth = 75 * this.DeviceDpi / 96;
66             this.Model.ThumbnailList.ImageList.ColorDepth = ColorDepth.Depth24Bit;
67             this.Model.ThumbnailList.ImageList.ImageSize = new(thumbnailWidth, thumbnailWidth);
68
69             this.Model.PropertyChanged +=
70                 (s, e) => this.TryInvoke(() => this.Model_PropertyChanged(s, e));
71             this.Model.MediaItems.ListChanged +=
72                 (s, e) => this.TryInvoke(() => this.Model_MediaItems_ListChanged(s, e));
73
74             this.UpdateSelectedMedia();
75             this.UpdateAltTextPanelVisible();
76         }
77
78         /// <summary>
79         /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
80         /// </summary>
81         public void BeginSelection()
82         {
83             this.BeginSelecting?.Invoke(this, EventArgs.Empty);
84             this.Enabled = true;
85             this.Visible = true;
86         }
87
88         /// <summary>
89         /// 選択処理を終了してコントロールを隠す。
90         /// </summary>
91         public void EndSelection()
92         {
93             this.EndSelecting?.Invoke(this, EventArgs.Empty);
94             this.Visible = false;
95             this.Enabled = false;
96             this.Model.ClearMediaItems();
97         }
98
99         /// <summary>
100         /// 選択された投稿先名と投稿する MediaItem を取得する。MediaItem は不要になったら呼び出し側にて破棄すること。
101         /// </summary>
102         public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [NotNullWhen(true)] out IMediaItem[]? mediaItems)
103         {
104             var selectedServiceName = this.Model.SelectedMediaServiceName;
105
106             var error = this.Model.Validate(out var rejectedMedia);
107             if (error != MediaSelectorErrorType.None)
108             {
109                 var message = error switch
110                 {
111                     MediaSelectorErrorType.MediaItemNotSet
112                         => Properties.Resources.PostPictureWarn1,
113                     MediaSelectorErrorType.ServiceNotSelected
114                         => Properties.Resources.PostPictureWarn1,
115                     MediaSelectorErrorType.UnsupportedFileExtension
116                         => string.Format(
117                             Properties.Resources.PostPictureWarn3,
118                             selectedServiceName,
119                             this.MakeAvailableServiceText(rejectedMedia!),
120                             rejectedMedia!.Extension,
121                             rejectedMedia!.Name
122                         ),
123                     MediaSelectorErrorType.FileSizeExceeded
124                         => string.Format(
125                             Properties.Resources.PostPictureWarn5,
126                             selectedServiceName,
127                             this.MakeAvailableServiceText(rejectedMedia!),
128                             rejectedMedia!.Name
129                         ),
130                     _ => throw new NotImplementedException(),
131                 };
132
133                 MessageBox.Show(
134                     message,
135                     Properties.Resources.PostPictureWarn2,
136                     MessageBoxButtons.OK,
137                     MessageBoxIcon.Warning
138                 );
139
140                 imageService = null;
141                 mediaItems = null;
142                 return false;
143             }
144
145             imageService = selectedServiceName;
146             mediaItems = this.Model.DetachMediaItems();
147             return true;
148         }
149
150         private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
151         {
152             switch (e.PropertyName)
153             {
154                 case nameof(MediaSelector.MediaServices):
155                     this.UpdateImageServiceComboItems();
156                     break;
157                 case nameof(MediaSelector.SelectedMediaServiceName):
158                     this.UpdateImageServiceComboSelection();
159                     this.UpdateAltTextPanelVisible();
160                     this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty);
161                     break;
162                 case nameof(MediaSelector.SelectedMediaItemId):
163                     this.UpdateSelectedMedia();
164                     break;
165                 case nameof(MediaSelector.SelectedMediaItemImage):
166                     this.UpdateSelectedMediaImage();
167                     break;
168                 default:
169                     break;
170             }
171         }
172
173         private void Model_MediaItems_ListChanged(object sender, ListChangedEventArgs e)
174         {
175             void AddMediaListViewItem(IMediaItem media, int index)
176                 => this.MediaListView.Items.Insert(index, media.Name, media.Id.ToString());
177
178             switch (e.ListChangedType)
179             {
180                 case ListChangedType.ItemAdded:
181                     var addedMedia = this.Model.MediaItems[e.NewIndex];
182                     AddMediaListViewItem(addedMedia, e.NewIndex);
183                     this.CorrectMediaListViewItemPositions(e.NewIndex);
184                     break;
185                 case ListChangedType.ItemDeleted:
186                     this.MediaListView.Items.RemoveAt(e.NewIndex);
187                     break;
188                 case ListChangedType.Reset:
189                     this.MediaListView.Items.Clear();
190                     foreach (var (media, index) in this.Model.MediaItems.WithIndex())
191                         AddMediaListViewItem(media, index);
192                     break;
193                 default:
194                     throw new NotImplementedException();
195             }
196         }
197
198         /// <summary>
199         /// <see cref="MediaListView"/> にアイテムを追加した後の座標を修正する
200         /// </summary>
201         /// <remarks>
202         /// <see cref="ListView.ListViewItemCollection.Insert"/> でインデックスを指定してアイテムを追加した場合、
203         /// コレクションの中では正しい位置にアイテムが挿入されるが、<see cref="ListViewItem.Position"/> の座標は
204         /// 挿入した位置にかかわらず常に最後尾のアイテムとして配置されてしまう。
205         /// このメソッドではアイテム追加後にコレクションのインデックスと表示上の順序が一致するように整列する処理を行う。
206         /// </remarks>
207         private void CorrectMediaListViewItemPositions(int startIndex)
208         {
209             if (startIndex == this.Model.MediaItems.Count - 1)
210                 return;
211
212             var items = this.MediaListView.Items.Cast<ListViewItem>()
213                 .Skip(startIndex).ToArray();
214
215             var orderedPositions = items.Select(x => x.Position)
216                 .OrderBy(x => x.Y).ThenBy(x => x.X).ToArray();
217
218             this.MediaListView.AutoArrange = false;
219
220             foreach (var i in Enumerable.Range(0, items.Length))
221                 items[i].Position = orderedPositions[i];
222
223             this.MediaListView.AutoArrange = true;
224         }
225
226         private void UpdateImageServiceComboItems()
227         {
228             using (ControlTransaction.Update(this.ImageServiceCombo))
229             {
230                 this.ImageServiceCombo.Items.Clear();
231
232                 // Add service names to combobox
233                 var serviceNames = this.Model.MediaServices.Select(x => x.Key).ToArray();
234                 this.ImageServiceCombo.Items.AddRange(serviceNames);
235
236                 this.UpdateImageServiceComboSelection();
237             }
238         }
239
240         private void UpdateImageServiceComboSelection()
241             => this.ImageServiceCombo.SelectedIndex = this.Model.SelectedMediaServiceIndex;
242
243         private void AddMediaButton_Click(object sender, EventArgs e)
244         {
245             var service = this.Model.SelectedMediaService;
246
247             if (this.FilePickDialog == null || service == null) return;
248             this.FilePickDialog.Filter = service.SupportedFormatsStrForDialog;
249             this.FilePickDialog.Title = Properties.Resources.PickPictureDialog1;
250             this.FilePickDialog.FileName = "";
251
252             this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty);
253
254             try
255             {
256                 if (this.FilePickDialog.ShowDialog() == DialogResult.Cancel) return;
257             }
258             finally
259             {
260                 this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty);
261             }
262
263             this.Model.AddMediaItemFromFilePath(this.FilePickDialog.FileNames);
264         }
265
266         private string MakeAvailableServiceText(IMediaItem media)
267         {
268             var ext = media.Extension;
269             var fileSize = media.Size;
270
271             var availableServiceNames = this.Model.GetAvailableServiceNames(ext, fileSize);
272             if (availableServiceNames.Length == 0)
273                 return Properties.Resources.PostPictureWarn6;
274
275             return string.Join(", ", availableServiceNames);
276         }
277
278         private void ImageCancelButton_Click(object sender, EventArgs e)
279             => this.EndSelection();
280
281         private void UpdateAltTextPanelVisible()
282             => this.AlternativeTextPanel.Visible = this.Model.CanUseAltText;
283
284         private void UpdateSelectedMedia()
285         {
286             using (ControlTransaction.Update(this))
287             {
288                 var selectedMedia = this.Model.SelectedMediaItem;
289                 if (selectedMedia == null)
290                 {
291                     this.AlternativeTextBox.Text = "";
292                     this.AlternativeTextPanel.Enabled = false;
293                 }
294                 else
295                 {
296                     this.AlternativeTextBox.Text = selectedMedia.AltText;
297                     this.AlternativeTextPanel.Enabled = true;
298                 }
299
300                 var index = this.Model.SelectedMediaItemIndex;
301                 var listViewSelectedIndex = this.MediaListView.SelectedIndices.Cast<int>().DefaultIfEmpty(-1).Single();
302                 if (listViewSelectedIndex != index)
303                 {
304                     this.MediaListView.SelectedIndices.Clear();
305                     if (index != -1)
306                         this.MediaListView.SelectedIndices.Add(index);
307                 }
308             }
309         }
310
311         private void UpdateSelectedMediaImage()
312             => this.ImageSelectedPicture.Image = this.Model.SelectedMediaItemImage;
313
314         private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
315             => this.Model.SelectedMediaServiceName = this.ImageServiceCombo.Text;
316
317         private void MediaListView_SelectedIndexChanged(object sender, EventArgs e)
318         {
319             var indices = this.MediaListView.SelectedIndices;
320             if (indices.Count == 0)
321                 return;
322
323             this.Model.SelectedMediaItemIndex = indices[0];
324         }
325
326         private void AlternativeTextBox_Validated(object sender, EventArgs e)
327             => this.Model.SetSelectedMediaAltText(this.AlternativeTextBox.Text);
328
329         private void MoveToBackMenuItem_Click(object sender, EventArgs e)
330             => this.Model.MoveSelectedMediaItemToPrevious();
331
332         private void MoveToNextMenuItem_Click(object sender, EventArgs e)
333             => this.Model.MoveSelectedMediaItemToNext();
334
335         private void DeleteMediaMenuItem_Click(object sender, EventArgs e)
336             => this.Model.RemoveSelectedMediaItem();
337
338         protected override void Dispose(bool disposing)
339         {
340             if (disposing)
341             {
342                 this.components?.Dispose();
343                 this.Model.Dispose();
344             }
345
346             base.Dispose(disposing);
347         }
348     }
349 }