1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 spx (@5px)
3 // All rights reserved.
5 // This file is part of OpenTween.
7 // This program is free software; you can redistribute it and/or modify it
8 // under the terms of the GNU General Public License as published by the Free
9 // Software Foundation; either version 3 of the License, or (at your option)
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17 // You should have received a copy of the GNU General Public License along
18 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
19 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
20 // Boston, MA 02110-1301, USA.
25 using System.Collections.Generic;
26 using System.ComponentModel;
28 using System.Diagnostics.CodeAnalysis;
33 using System.Threading.Tasks;
34 using System.Windows.Forms;
35 using OpenTween.Api.DataModel;
36 using OpenTween.MediaUploadServices;
40 public partial class MediaSelector : UserControl
42 public event EventHandler<EventArgs>? BeginSelecting;
43 public event EventHandler<EventArgs>? EndSelecting;
45 public event EventHandler<EventArgs>? FilePickDialogOpening;
46 public event EventHandler<EventArgs>? FilePickDialogClosed;
48 public event EventHandler<EventArgs>? SelectedServiceChanged;
51 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
52 public OpenFileDialog? FilePickDialog { get; set; }
58 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
59 public string ServiceName
60 => this.ImageServiceCombo.Text;
63 /// 選択されている投稿先を示すインデックスを取得する。
66 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
67 public int ServiceIndex
68 => this.ImageServiceCombo.SelectedIndex;
71 /// 選択されている投稿先の IMediaUploadService を取得する。
74 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
75 public IMediaUploadService? SelectedService
79 var serviceName = this.ServiceName;
80 if (MyCommon.IsNullOrEmpty(serviceName))
83 return this.pictureService.TryGetValue(serviceName, out var service)
89 /// 指定された投稿先名から、作成済みの IMediaUploadService インスタンスを取得する。
91 public IMediaUploadService GetService(string serviceName)
93 this.pictureService.TryGetValue(serviceName, out var service);
98 /// 利用可能な全ての IMediaUploadService インスタンスを取得する。
100 public ICollection<IMediaUploadService> GetServices()
101 => this.pictureService.Values;
103 private class SelectedMedia
105 public IMediaItem? Item { get; set; }
106 public MyCommon.UploadFileType Type { get; set; }
107 public string Text { get; set; }
109 public SelectedMedia(IMediaItem? item, MyCommon.UploadFileType type, string text)
116 public SelectedMedia(string text)
117 : this(null, MyCommon.UploadFileType.Invalid, text)
122 => this.Item != null && this.Type != MyCommon.UploadFileType.Invalid;
125 => this.Item?.Path ?? "";
127 public string AltText => this.Item?.AltText ?? "";
129 public override string ToString()
133 private Dictionary<string, IMediaUploadService> pictureService = new Dictionary<string, IMediaUploadService>();
135 private void CreateServices(Twitter tw, TwitterConfiguration twitterConfig)
137 this.pictureService?.Clear();
139 this.pictureService = new Dictionary<string, IMediaUploadService>
141 ["Twitter"] = new TwitterPhoto(tw, twitterConfig),
142 ["Imgur"] = new Imgur(twitterConfig),
143 ["Mobypicture"] = new Mobypicture(tw, twitterConfig),
147 public MediaSelector()
149 this.InitializeComponent();
151 this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage;
157 public void Initialize(Twitter tw, TwitterConfiguration twitterConfig, string svc, int? index = null)
159 this.CreateServices(tw, twitterConfig);
161 this.SetImageServiceCombo();
162 this.SetImagePageCombo();
164 this.SelectImageServiceComboItem(svc, index);
170 public void Reset(Twitter tw, TwitterConfiguration twitterConfig)
172 this.CreateServices(tw, twitterConfig);
174 this.SetImageServiceCombo();
178 /// 指定されたファイルの投稿に対応した投稿先があるかどうかを示す値を取得する。
180 public bool HasUploadableService(string fileName, bool ignoreSize)
182 var fl = new FileInfo(fileName);
183 var ext = fl.Extension;
184 var size = ignoreSize ? (long?)null : fl.Length;
186 if (this.IsUploadable(this.ServiceName, ext, size))
189 foreach (string svc in this.ImageServiceCombo.Items)
191 if (this.IsUploadable(svc, ext, size))
199 /// 指定された投稿先に投稿可能かどうかを示す値を取得する。
200 /// ファイルサイズの指定がなければ拡張子だけで判定する。
202 private bool IsUploadable(string serviceName, string ext, long? size)
204 if (!MyCommon.IsNullOrEmpty(serviceName))
206 var imageService = this.pictureService[serviceName];
207 if (imageService.CheckFileExtension(ext))
212 if (imageService.CheckFileSize(ext, size.Value))
220 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
222 private void BeginSelection(IMediaItem[] items)
224 if (items == null || items.Length == 0)
226 this.BeginSelection();
230 var service = this.SelectedService;
231 if (service == null) return;
233 var count = Math.Min(items.Length, service.MaxMediaCount);
234 if (!this.Visible || count > 1)
236 // 非表示時または複数のファイル指定は新規選択として扱う
237 this.SetImagePageCombo();
239 this.BeginSelecting?.Invoke(this, EventArgs.Empty);
247 this.ImagefilePathText.Text = items[0].Path;
248 this.AlternativeTextBox.Text = items[0].AltText;
249 this.ImageFromSelectedFile(items[0], false);
253 for (var i = 0; i < count; i++)
255 var index = this.ImagePageCombo.Items.Count - 1;
258 this.ImagefilePathText.Text = items[i].Path;
259 this.AlternativeTextBox.Text = items[i].AltText;
261 this.ImageFromSelectedFile(index, items[i], false);
267 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する(主にD&D用)。
269 public void BeginSelection(string[] fileNames)
271 if (fileNames == null || fileNames.Length == 0)
273 this.BeginSelection();
277 var items = fileNames.Select(x => this.CreateFileMediaItem(x, false)).OfType<IMediaItem>().ToArray();
278 this.BeginSelection(items);
282 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
284 public void BeginSelection(Image image)
288 this.BeginSelection();
292 var items = new[] { this.CreateMemoryImageMediaItem(image, false) }.OfType<IMediaItem>().ToArray();
293 this.BeginSelection(items);
297 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
299 public void BeginSelection()
303 this.BeginSelecting?.Invoke(this, EventArgs.Empty);
308 var media = (SelectedMedia)this.ImagePageCombo.SelectedItem;
309 this.ImageFromSelectedFile(media.Item, true);
310 this.ImagefilePathText.Focus();
315 /// 選択処理を終了してコントロールを隠す。
317 public void EndSelection()
321 this.ImagefilePathText.CausesValidation = false;
323 this.EndSelecting?.Invoke(this, EventArgs.Empty);
325 this.Visible = false;
326 this.Enabled = false;
327 this.ClearImageSelectedPicture();
329 this.ImagefilePathText.CausesValidation = true;
334 /// 選択された投稿先名と投稿する MediaItem を取得する。MediaItem は不要になったら呼び出し側にて破棄すること。
336 public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [NotNullWhen(true)] out IMediaItem[]? mediaItems)
338 var validItems = this.ImagePageCombo.Items.Cast<SelectedMedia>()
339 .Where(x => x.IsValid).Select(x => x.Item).OfType<IMediaItem>().ToArray();
341 if (validItems.Length > 0 &&
342 this.ImageServiceCombo.SelectedIndex > -1)
344 var serviceName = this.ServiceName;
345 if (MessageBox.Show(string.Format(Properties.Resources.PostPictureConfirm1, serviceName, validItems.Length),
346 Properties.Resources.PostPictureConfirm2,
347 MessageBoxButtons.OKCancel,
348 MessageBoxIcon.Question,
349 MessageBoxDefaultButton.Button1)
352 // 収集した MediaItem が破棄されないように、予め null を代入しておく
353 foreach (SelectedMedia media in this.ImagePageCombo.Items)
355 if (media != null) media.Item = null;
358 imageService = serviceName;
359 mediaItems = validItems;
361 this.SetImagePageCombo();
367 MessageBox.Show(Properties.Resources.PostPictureWarn1, Properties.Resources.PostPictureWarn2);
375 private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image, bool noMsgBox)
377 if (image == null) return null;
379 MemoryImage? memoryImage = null;
382 // image から png 形式の MemoryImage を生成
383 memoryImage = MemoryImage.CopyFromImage(image);
385 return new MemoryImageMediaItem(memoryImage);
389 memoryImage?.Dispose();
391 if (!noMsgBox) MessageBox.Show("Unable to create MemoryImage.");
396 private IMediaItem? CreateFileMediaItem(string path, bool noMsgBox)
398 if (MyCommon.IsNullOrEmpty(path)) return null;
402 return new FileMediaItem(path);
406 if (!noMsgBox) MessageBox.Show("Invalid file path: " + path);
411 private void ValidateNewFileMediaItem(string path, string altText, bool noMsgBox)
413 var media = (SelectedMedia)this.ImagePageCombo.SelectedItem;
414 var item = media.Item;
416 if (path != media.Path)
418 this.DisposeMediaItem(media.Item);
421 item = this.CreateFileMediaItem(path, noMsgBox);
425 item.AltText = altText;
427 this.ImagefilePathText.Text = path;
428 this.AlternativeTextBox.Text = altText;
429 this.ImageFromSelectedFile(item, noMsgBox);
432 private void DisposeMediaItem(IMediaItem? item)
434 var disposableItem = item as IDisposable;
435 disposableItem?.Dispose();
438 private void FilePickButton_Click(object sender, EventArgs e)
440 var service = this.SelectedService;
442 if (this.FilePickDialog == null || service == null) return;
443 this.FilePickDialog.Filter = service.SupportedFormatsStrForDialog;
444 this.FilePickDialog.Title = Properties.Resources.PickPictureDialog1;
445 this.FilePickDialog.FileName = "";
447 this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty);
451 if (this.FilePickDialog.ShowDialog() == DialogResult.Cancel) return;
455 this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty);
458 this.ValidateNewFileMediaItem(this.FilePickDialog.FileName, this.AlternativeTextBox.Text.Trim(), false);
461 private void ImagefilePathText_Validating(object sender, CancelEventArgs e)
463 if (this.ImageCancelButton.Focused)
465 this.ImagefilePathText.CausesValidation = false;
469 this.ValidateNewFileMediaItem(this.ImagefilePathText.Text.Trim(), this.AlternativeTextBox.Text.Trim(), false);
472 private void ImageFromSelectedFile(IMediaItem? item, bool noMsgBox)
473 => this.ImageFromSelectedFile(-1, item, noMsgBox);
475 private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox)
481 var imageService = this.SelectedService;
482 if (imageService == null) return;
484 var selectedIndex = this.ImagePageCombo.SelectedIndex;
485 if (index < 0) index = selectedIndex;
487 if (index >= this.ImagePageCombo.Items.Count)
488 throw new ArgumentOutOfRangeException(nameof(index));
490 var isSelectedPage = index == selectedIndex;
493 this.ClearImageSelectedPicture();
495 if (item == null || MyCommon.IsNullOrEmpty(item.Path)) return;
499 var ext = item.Extension;
500 var size = item.Size;
502 if (!imageService.CheckFileExtension(ext))
508 string.Format(Properties.Resources.PostPictureWarn3, this.ServiceName, this.MakeAvailableServiceText(ext, size), ext, item.Name),
509 Properties.Resources.PostPictureWarn4,
510 MessageBoxButtons.OK,
511 MessageBoxIcon.Warning);
516 if (!imageService.CheckFileSize(ext, size))
522 string.Format(Properties.Resources.PostPictureWarn5, this.ServiceName, this.MakeAvailableServiceText(ext, size), item.Name),
523 Properties.Resources.PostPictureWarn4,
524 MessageBoxButtons.OK,
525 MessageBoxIcon.Warning);
533 this.ImageSelectedPicture.Image = item.CreateImage();
534 this.SetImagePage(index, item, MyCommon.UploadFileType.Picture);
538 this.SetImagePage(index, item, MyCommon.UploadFileType.MultiMedia);
541 valid = true; // 正常終了
543 catch (FileNotFoundException)
545 if (!noMsgBox) MessageBox.Show("File not found.");
549 if (!noMsgBox) MessageBox.Show("The type of this file is not image.");
556 this.ClearImagePage(index);
557 this.DisposeMediaItem(item);
562 private string MakeAvailableServiceText(string ext, long fileSize)
564 var text = string.Join(", ",
565 this.ImageServiceCombo.Items.Cast<string>()
566 .Where(serviceName =>
567 !MyCommon.IsNullOrEmpty(serviceName) &&
568 this.pictureService[serviceName].CheckFileExtension(ext) &&
569 this.pictureService[serviceName].CheckFileSize(ext, fileSize)));
571 if (MyCommon.IsNullOrEmpty(text))
572 return Properties.Resources.PostPictureWarn6;
577 private void ClearImageSelectedPicture()
579 var oldImage = this.ImageSelectedPicture.Image;
580 this.ImageSelectedPicture.Image = null;
583 this.ImageSelectedPicture.ShowInitialImage();
586 private void ImageCancelButton_Click(object sender, EventArgs e)
587 => this.EndSelection();
589 private void ImageSelection_KeyDown(object sender, KeyEventArgs e)
591 if (e.KeyCode == Keys.Escape)
597 private void ImageSelection_KeyPress(object sender, KeyPressEventArgs e)
599 if (Convert.ToInt32(e.KeyChar) == 0x1B)
601 this.ImagefilePathText.CausesValidation = false;
606 private void ImageSelection_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
608 if (e.KeyCode == Keys.Escape)
610 this.ImagefilePathText.CausesValidation = false;
614 private void SetImageServiceCombo()
616 using (ControlTransaction.Update(this.ImageServiceCombo))
619 if (this.ImageServiceCombo.SelectedIndex > -1) svc = this.ImageServiceCombo.Text;
620 this.ImageServiceCombo.Items.Clear();
622 // Add service names to combobox
623 foreach (var key in this.pictureService.Keys)
625 this.ImageServiceCombo.Items.Add(key);
628 this.SelectImageServiceComboItem(svc);
632 private void SelectImageServiceComboItem(string svc, int? index = null)
635 if (MyCommon.IsNullOrEmpty(svc))
641 idx = this.ImageServiceCombo.Items.IndexOf(svc);
643 // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる
644 // (廃止されたサービスを選択していた場合の対応)
645 if (idx == -1) idx = 0;
650 this.ImageServiceCombo.SelectedIndex = idx;
652 catch (ArgumentOutOfRangeException)
654 this.ImageServiceCombo.SelectedIndex = 0;
657 this.UpdateAltTextPanelVisible();
660 private void UpdateAltTextPanelVisible()
661 => this.AlternativeTextPanel.Visible = this.SelectedService switch
664 var service => service.CanUseAltText,
667 private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
671 var imageService = this.SelectedService;
672 if (imageService != null)
674 this.UpdateAltTextPanelVisible();
676 if (this.ImagePageCombo.Items.Count > 0)
678 // 画像が選択された投稿先に対応しているかをチェックする
679 // TODO: 複数の選択済み画像があるなら、できれば全てを再チェックしたほうがいい
680 if (this.ServiceName == "Twitter")
682 this.ValidateSelectedImagePage();
686 if (this.ImagePageCombo.Items.Count > 1)
688 // 複数の選択済み画像のうち、1枚目のみを残す
689 this.SetImagePageCombo((SelectedMedia)this.ImagePageCombo.Items[0]);
693 this.ImagePageCombo.Enabled = false;
698 var item = ((SelectedMedia)this.ImagePageCombo.Items[0]).Item;
701 var ext = item.Extension;
702 if (imageService.CheckFileExtension(ext) &&
703 imageService.CheckFileSize(ext, item.Size))
716 this.ClearImageSelectedPicture();
717 this.ClearSelectedImagePage();
726 this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty);
729 private void SetImagePageCombo(SelectedMedia? media = null)
731 using (ControlTransaction.Update(this.ImagePageCombo))
733 this.ImagePageCombo.Enabled = false;
735 foreach (SelectedMedia oldMedia in this.ImagePageCombo.Items)
737 if (oldMedia == null || oldMedia == media) continue;
738 this.DisposeMediaItem(oldMedia.Item);
740 this.ImagePageCombo.Items.Clear();
743 media = new SelectedMedia("1");
745 this.ImagePageCombo.Items.Add(media);
746 this.ImagefilePathText.Text = media.Path;
747 this.AlternativeTextBox.Text = media.AltText;
749 this.ImagePageCombo.SelectedIndex = 0;
753 private void AddNewImagePage(int selectedIndex)
755 var service = this.SelectedService;
756 if (service == null) return;
758 if (selectedIndex < service.MaxMediaCount - 1)
760 // 投稿先の投稿可能枚数まで選択できるようにする
761 var count = this.ImagePageCombo.Items.Count;
762 if (selectedIndex == count - 1)
765 this.ImagePageCombo.Items.Add(new SelectedMedia(count.ToString()));
766 this.ImagePageCombo.Enabled = true;
771 private void SetSelectedImagePage(IMediaItem item, MyCommon.UploadFileType type)
772 => this.SetImagePage(-1, item, type);
774 private void SetImagePage(int index, IMediaItem item, MyCommon.UploadFileType type)
776 var selectedIndex = this.ImagePageCombo.SelectedIndex;
777 if (index < 0) index = selectedIndex;
779 var media = (SelectedMedia)this.ImagePageCombo.Items[index];
780 if (media.Item != item)
782 this.DisposeMediaItem(media.Item);
787 this.AddNewImagePage(index);
790 private void ClearSelectedImagePage()
791 => this.ClearImagePage(-1);
793 private void ClearImagePage(int index)
795 var selectedIndex = this.ImagePageCombo.SelectedIndex;
796 if (index < 0) index = selectedIndex;
798 var media = (SelectedMedia)this.ImagePageCombo.Items[index];
799 this.DisposeMediaItem(media.Item);
801 media.Type = MyCommon.UploadFileType.Invalid;
803 if (index == selectedIndex)
805 this.ImagefilePathText.Text = "";
806 this.AlternativeTextBox.Text = "";
810 private void ValidateSelectedImagePage()
812 var idx = this.ImagePageCombo.SelectedIndex;
813 var media = (SelectedMedia)this.ImagePageCombo.Items[idx];
814 this.ImageServiceCombo.Enabled = idx == 0; // idx == 0 以外では投稿先サービスを選べないようにする
815 this.ImagefilePathText.Text = media.Path;
816 this.AlternativeTextBox.Text = media.AltText;
817 this.ImageFromSelectedFile(media.Item, true);
820 private void ImagePageCombo_SelectedIndexChanged(object sender, EventArgs e)
821 => this.ValidateSelectedImagePage();
823 private void AlternativeTextBox_Validating(object sender, CancelEventArgs e)
825 var imageFilePath = this.ImagefilePathText.Text.Trim();
826 var altText = this.AlternativeTextBox.Text.Trim();
827 this.ValidateNewFileMediaItem(imageFilePath, altText, noMsgBox: false);