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;
32 using System.Threading.Tasks;
33 using System.Windows.Forms;
34 using OpenTween.Api.DataModel;
35 using OpenTween.Connection;
36 using System.Diagnostics.CodeAnalysis;
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 (string.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> {
140 ["Twitter"] = new TwitterPhoto(tw, twitterConfig),
141 ["Imgur"] = new Imgur(twitterConfig),
142 ["Mobypicture"] = new Mobypicture(tw, twitterConfig),
146 public MediaSelector()
148 InitializeComponent();
150 this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage;
156 public void Initialize(Twitter tw, TwitterConfiguration twitterConfig, string svc, int? index = null)
158 CreateServices(tw, twitterConfig);
160 SetImageServiceCombo();
163 SelectImageServiceComboItem(svc, index);
169 public void Reset(Twitter tw, TwitterConfiguration twitterConfig)
171 CreateServices(tw, twitterConfig);
173 SetImageServiceCombo();
177 /// 指定されたファイルの投稿に対応した投稿先があるかどうかを示す値を取得する。
179 public bool HasUploadableService(string fileName, bool ignoreSize)
181 var fl = new FileInfo(fileName);
182 var ext = fl.Extension;
183 var size = ignoreSize ? (long?)null : fl.Length;
185 if (IsUploadable(this.ServiceName, ext, size))
188 foreach (string svc in ImageServiceCombo.Items)
190 if (IsUploadable(svc, ext, size))
198 /// 指定された投稿先に投稿可能かどうかを示す値を取得する。
199 /// ファイルサイズの指定がなければ拡張子だけで判定する。
201 private bool IsUploadable(string serviceName, string ext, long? size)
203 if (!string.IsNullOrEmpty(serviceName))
205 var imageService = this.pictureService[serviceName];
206 if (imageService.CheckFileExtension(ext))
211 if (imageService.CheckFileSize(ext, size.Value))
219 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
221 private void BeginSelection(IMediaItem[] items)
223 if (items == null || items.Length == 0)
229 var service = this.SelectedService;
230 if (service == null) return;
232 var count = Math.Min(items.Length, service.MaxMediaCount);
233 if (!this.Visible || count > 1)
235 // 非表示時または複数のファイル指定は新規選択として扱う
238 this.BeginSelecting?.Invoke(this, EventArgs.Empty);
246 ImagefilePathText.Text = items[0].Path;
247 AlternativeTextBox.Text = items[0].AltText;
248 ImageFromSelectedFile(items[0], false);
252 for (var i = 0; i < count; i++)
254 var index = ImagePageCombo.Items.Count - 1;
257 ImagefilePathText.Text = items[i].Path;
258 AlternativeTextBox.Text = items[i].AltText;
260 ImageFromSelectedFile(index, items[i], false);
266 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する(主にD&D用)。
268 public void BeginSelection(string[] fileNames)
270 if (fileNames == null || fileNames.Length == 0)
276 var items = fileNames.Select(x => CreateFileMediaItem(x, false)).OfType<IMediaItem>().ToArray();
277 BeginSelection(items);
281 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
283 public void BeginSelection(Image image)
291 var items = new [] { CreateMemoryImageMediaItem(image, false) }.OfType<IMediaItem>().ToArray();
292 BeginSelection(items);
296 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
298 public void BeginSelection()
302 this.BeginSelecting?.Invoke(this, EventArgs.Empty);
307 var media = (SelectedMedia)ImagePageCombo.SelectedItem;
308 ImageFromSelectedFile(media.Item, true);
309 ImagefilePathText.Focus();
314 /// 選択処理を終了してコントロールを隠す。
316 public void EndSelection()
320 ImagefilePathText.CausesValidation = false;
322 this.EndSelecting?.Invoke(this, EventArgs.Empty);
324 this.Visible = false;
325 this.Enabled = false;
326 ClearImageSelectedPicture();
328 ImagefilePathText.CausesValidation = true;
333 /// 選択された投稿先名と投稿する MediaItem を取得する。MediaItem は不要になったら呼び出し側にて破棄すること。
335 public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [NotNullWhen(true)] out IMediaItem[]? mediaItems)
337 var validItems = ImagePageCombo.Items.Cast<SelectedMedia>()
338 .Where(x => x.IsValid).Select(x => x.Item).OfType<IMediaItem>().ToArray();
340 if (validItems.Length > 0 &&
341 ImageServiceCombo.SelectedIndex > -1)
343 var serviceName = this.ServiceName;
344 if (MessageBox.Show(string.Format(Properties.Resources.PostPictureConfirm1, serviceName, validItems.Length),
345 Properties.Resources.PostPictureConfirm2,
346 MessageBoxButtons.OKCancel,
347 MessageBoxIcon.Question,
348 MessageBoxDefaultButton.Button1)
351 //収集した MediaItem が破棄されないように、予め null を代入しておく
352 foreach (SelectedMedia media in ImagePageCombo.Items)
354 if (media != null) media.Item = null;
357 imageService = serviceName;
358 mediaItems = validItems;
366 MessageBox.Show(Properties.Resources.PostPictureWarn1, Properties.Resources.PostPictureWarn2);
374 private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image, bool noMsgBox)
376 if (image == null) return null;
378 MemoryImage? memoryImage = null;
381 // image から png 形式の MemoryImage を生成
382 memoryImage = MemoryImage.CopyFromImage(image);
384 return new MemoryImageMediaItem(memoryImage);
388 memoryImage?.Dispose();
390 if (!noMsgBox) MessageBox.Show("Unable to create MemoryImage.");
395 private IMediaItem? CreateFileMediaItem(string path, bool noMsgBox)
397 if (string.IsNullOrEmpty(path)) return null;
401 return new FileMediaItem(path);
405 if (!noMsgBox) MessageBox.Show("Invalid file path: " + path);
410 private void ValidateNewFileMediaItem(string path, string altText, bool noMsgBox)
412 var media = (SelectedMedia)ImagePageCombo.SelectedItem;
413 var item = media.Item;
415 if (path != media.Path)
417 DisposeMediaItem(media.Item);
420 item = CreateFileMediaItem(path, noMsgBox);
424 item.AltText = altText;
426 ImagefilePathText.Text = path;
427 AlternativeTextBox.Text = altText;
428 ImageFromSelectedFile(item, noMsgBox);
431 private void DisposeMediaItem(IMediaItem? item)
433 var disposableItem = item as IDisposable;
434 disposableItem?.Dispose();
437 private void FilePickButton_Click(object sender, EventArgs e)
439 var service = this.SelectedService;
441 if (FilePickDialog == null || service == null) return;
442 FilePickDialog.Filter = service.SupportedFormatsStrForDialog;
443 FilePickDialog.Title = Properties.Resources.PickPictureDialog1;
444 FilePickDialog.FileName = "";
446 this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty);
450 if (FilePickDialog.ShowDialog() == DialogResult.Cancel) return;
454 this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty);
457 ValidateNewFileMediaItem(FilePickDialog.FileName, AlternativeTextBox.Text.Trim(), false);
460 private void ImagefilePathText_Validating(object sender, CancelEventArgs e)
462 if (ImageCancelButton.Focused)
464 ImagefilePathText.CausesValidation = false;
468 ValidateNewFileMediaItem(ImagefilePathText.Text.Trim(), AlternativeTextBox.Text.Trim(), false);
471 private void ImageFromSelectedFile(IMediaItem? item, bool noMsgBox)
472 => this.ImageFromSelectedFile(-1, item, noMsgBox);
474 private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox)
480 var imageService = this.SelectedService;
481 if (imageService == null) return;
483 var selectedIndex = ImagePageCombo.SelectedIndex;
484 if (index < 0) index = selectedIndex;
486 if (index >= ImagePageCombo.Items.Count)
487 throw new ArgumentOutOfRangeException(nameof(index));
489 var isSelectedPage = (index == selectedIndex);
492 this.ClearImageSelectedPicture();
494 if (item == null || string.IsNullOrEmpty(item.Path)) return;
498 var ext = item.Extension;
499 var size = item.Size;
501 if (!imageService.CheckFileExtension(ext))
507 string.Format(Properties.Resources.PostPictureWarn3, this.ServiceName, MakeAvailableServiceText(ext, size), ext, item.Name),
508 Properties.Resources.PostPictureWarn4,
509 MessageBoxButtons.OK,
510 MessageBoxIcon.Warning);
515 if (!imageService.CheckFileSize(ext, size))
521 string.Format(Properties.Resources.PostPictureWarn5, this.ServiceName, MakeAvailableServiceText(ext, size), item.Name),
522 Properties.Resources.PostPictureWarn4,
523 MessageBoxButtons.OK,
524 MessageBoxIcon.Warning);
532 ImageSelectedPicture.Image = item.CreateImage();
533 SetImagePage(index, item, MyCommon.UploadFileType.Picture);
537 SetImagePage(index, item, MyCommon.UploadFileType.MultiMedia);
542 catch (FileNotFoundException)
544 if (!noMsgBox) MessageBox.Show("File not found.");
548 if (!noMsgBox) MessageBox.Show("The type of this file is not image.");
555 ClearImagePage(index);
556 DisposeMediaItem(item);
561 private string MakeAvailableServiceText(string ext, long fileSize)
563 var text = string.Join(", ",
564 ImageServiceCombo.Items.Cast<string>()
565 .Where(serviceName =>
566 !string.IsNullOrEmpty(serviceName) &&
567 this.pictureService[serviceName].CheckFileExtension(ext) &&
568 this.pictureService[serviceName].CheckFileSize(ext, fileSize)));
570 if (string.IsNullOrEmpty(text))
571 return Properties.Resources.PostPictureWarn6;
576 private void ClearImageSelectedPicture()
578 var oldImage = this.ImageSelectedPicture.Image;
579 this.ImageSelectedPicture.Image = null;
582 this.ImageSelectedPicture.ShowInitialImage();
585 private void ImageCancelButton_Click(object sender, EventArgs e)
586 => this.EndSelection();
588 private void ImageSelection_KeyDown(object sender, KeyEventArgs e)
590 if (e.KeyCode == Keys.Escape)
596 private void ImageSelection_KeyPress(object sender, KeyPressEventArgs e)
598 if (Convert.ToInt32(e.KeyChar) == 0x1B)
600 ImagefilePathText.CausesValidation = false;
605 private void ImageSelection_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
607 if (e.KeyCode == Keys.Escape)
609 ImagefilePathText.CausesValidation = false;
613 private void SetImageServiceCombo()
615 using (ControlTransaction.Update(ImageServiceCombo))
618 if (ImageServiceCombo.SelectedIndex > -1) svc = ImageServiceCombo.Text;
619 ImageServiceCombo.Items.Clear();
621 // Add service names to combobox
622 foreach (var key in pictureService.Keys)
624 ImageServiceCombo.Items.Add(key);
627 SelectImageServiceComboItem(svc);
631 private void SelectImageServiceComboItem(string svc, int? index = null)
634 if (string.IsNullOrEmpty(svc))
640 idx = ImageServiceCombo.Items.IndexOf(svc);
642 // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる
643 // (廃止されたサービスを選択していた場合の対応)
644 if (idx == -1) idx = 0;
649 ImageServiceCombo.SelectedIndex = idx;
651 catch (ArgumentOutOfRangeException)
653 ImageServiceCombo.SelectedIndex = 0;
656 this.UpdateAltTextPanelVisible();
659 private void UpdateAltTextPanelVisible()
660 => this.AlternativeTextPanel.Visible = this.SelectedService switch
663 var service => service.CanUseAltText,
666 private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
670 var imageService = this.SelectedService;
671 if (imageService != null)
673 this.UpdateAltTextPanelVisible();
675 if (ImagePageCombo.Items.Count > 0)
677 // 画像が選択された投稿先に対応しているかをチェックする
678 // TODO: 複数の選択済み画像があるなら、できれば全てを再チェックしたほうがいい
679 if (this.ServiceName == "Twitter")
681 ValidateSelectedImagePage();
685 if (ImagePageCombo.Items.Count > 1)
687 // 複数の選択済み画像のうち、1枚目のみを残す
688 SetImagePageCombo((SelectedMedia)ImagePageCombo.Items[0]);
692 ImagePageCombo.Enabled = false;
697 var item = ((SelectedMedia)ImagePageCombo.Items[0]).Item;
700 var ext = item.Extension;
701 if (imageService.CheckFileExtension(ext) &&
702 imageService.CheckFileSize(ext, item.Size))
715 ClearImageSelectedPicture();
716 ClearSelectedImagePage();
725 this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty);
728 private void SetImagePageCombo(SelectedMedia? media = null)
730 using (ControlTransaction.Update(ImagePageCombo))
732 ImagePageCombo.Enabled = false;
734 foreach (SelectedMedia oldMedia in ImagePageCombo.Items)
736 if (oldMedia == null || oldMedia == media) continue;
737 DisposeMediaItem(oldMedia.Item);
739 ImagePageCombo.Items.Clear();
742 media = new SelectedMedia("1");
744 ImagePageCombo.Items.Add(media);
745 ImagefilePathText.Text = media.Path;
746 AlternativeTextBox.Text = media.AltText;
748 ImagePageCombo.SelectedIndex = 0;
752 private void AddNewImagePage(int selectedIndex)
754 var service = this.SelectedService;
755 if (service == null) return;
757 if (selectedIndex < service.MaxMediaCount - 1)
759 // 投稿先の投稿可能枚数まで選択できるようにする
760 var count = ImagePageCombo.Items.Count;
761 if (selectedIndex == count - 1)
764 ImagePageCombo.Items.Add(new SelectedMedia(count.ToString()));
765 ImagePageCombo.Enabled = true;
770 private void SetSelectedImagePage(IMediaItem item, MyCommon.UploadFileType type)
771 => this.SetImagePage(-1, item, type);
773 private void SetImagePage(int index, IMediaItem item, MyCommon.UploadFileType type)
775 var selectedIndex = ImagePageCombo.SelectedIndex;
776 if (index < 0) index = selectedIndex;
778 var media = (SelectedMedia)ImagePageCombo.Items[index];
779 if (media.Item != item)
781 DisposeMediaItem(media.Item);
786 AddNewImagePage(index);
789 private void ClearSelectedImagePage()
790 => this.ClearImagePage(-1);
792 private void ClearImagePage(int index)
794 var selectedIndex = ImagePageCombo.SelectedIndex;
795 if (index < 0) index = selectedIndex;
797 var media = (SelectedMedia)ImagePageCombo.Items[index];
798 DisposeMediaItem(media.Item);
800 media.Type = MyCommon.UploadFileType.Invalid;
802 if (index == selectedIndex)
804 ImagefilePathText.Text = "";
805 AlternativeTextBox.Text = "";
809 private void ValidateSelectedImagePage()
811 var idx = ImagePageCombo.SelectedIndex;
812 var media = (SelectedMedia)ImagePageCombo.Items[idx];
813 ImageServiceCombo.Enabled = (idx == 0); // idx == 0 以外では投稿先サービスを選べないようにする
814 ImagefilePathText.Text = media.Path;
815 AlternativeTextBox.Text = media.AltText;
816 ImageFromSelectedFile(media.Item, true);
819 private void ImagePageCombo_SelectedIndexChanged(object sender, EventArgs e)
820 => this.ValidateSelectedImagePage();
822 private void AlternativeTextBox_Validating(object sender, CancelEventArgs e)
824 var imageFilePath = this.ImagefilePathText.Text.Trim();
825 var altText = this.AlternativeTextBox.Text.Trim();
826 this.ValidateNewFileMediaItem(imageFilePath, altText, noMsgBox: false);