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.
23 using System.Collections.Generic;
24 using System.ComponentModel;
30 using System.Threading.Tasks;
31 using System.Windows.Forms;
32 using OpenTween.Api.DataModel;
33 using OpenTween.Connection;
37 public partial class MediaSelector : UserControl
39 public event EventHandler<EventArgs> BeginSelecting;
40 public event EventHandler<EventArgs> EndSelecting;
42 public event EventHandler<EventArgs> FilePickDialogOpening;
43 public event EventHandler<EventArgs> FilePickDialogClosed;
45 public event EventHandler<EventArgs> SelectedServiceChanged;
48 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
49 public OpenFileDialog FilePickDialog { get; set; }
55 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
56 public string ServiceName
57 => this.ImageServiceCombo.Text;
60 /// 選択されている投稿先を示すインデックスを取得する。
63 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
64 public int ServiceIndex
65 => this.ImageServiceCombo.SelectedIndex;
68 /// 選択されている投稿先の IMediaUploadService を取得する。
71 [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
72 public IMediaUploadService SelectedService
76 var serviceName = this.ServiceName;
77 if (string.IsNullOrEmpty(serviceName))
80 return this.pictureService.TryGetValue(serviceName, out var service)
86 /// 指定された投稿先名から、作成済みの IMediaUploadService インスタンスを取得する。
88 public IMediaUploadService GetService(string serviceName)
90 this.pictureService.TryGetValue(serviceName, out var service);
95 /// 利用可能な全ての IMediaUploadService インスタンスを取得する。
97 public ICollection<IMediaUploadService> GetServices()
98 => this.pictureService.Values;
100 private class SelectedMedia
102 public IMediaItem Item { get; set; }
103 public MyCommon.UploadFileType Type { get; set; }
104 public string Text { get; set; }
106 public SelectedMedia(IMediaItem item, MyCommon.UploadFileType type, string text)
113 public SelectedMedia(string text)
114 : this(null, MyCommon.UploadFileType.Invalid, text)
119 => this.Item != null && this.Type != MyCommon.UploadFileType.Invalid;
122 => this.Item?.Path ?? "";
124 public string AltText => this.Item?.AltText ?? "";
126 public override string ToString()
130 private Dictionary<string, IMediaUploadService> pictureService;
132 private void CreateServices(Twitter tw, TwitterConfiguration twitterConfig)
134 this.pictureService?.Clear();
135 this.pictureService = null;
137 this.pictureService = new Dictionary<string, IMediaUploadService> {
138 ["Twitter"] = new TwitterPhoto(tw, twitterConfig),
139 ["Imgur"] = new Imgur(twitterConfig),
140 ["Mobypicture"] = new Mobypicture(tw, twitterConfig),
144 public MediaSelector()
146 InitializeComponent();
148 this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage;
154 public void Initialize(Twitter tw, TwitterConfiguration twitterConfig, string svc, int? index = null)
156 CreateServices(tw, twitterConfig);
158 SetImageServiceCombo();
161 SelectImageServiceComboItem(svc, index);
167 public void Reset(Twitter tw, TwitterConfiguration twitterConfig)
169 CreateServices(tw, twitterConfig);
171 SetImageServiceCombo();
175 /// 指定されたファイルの投稿に対応した投稿先があるかどうかを示す値を取得する。
177 public bool HasUploadableService(string fileName, bool ignoreSize)
179 var fl = new FileInfo(fileName);
180 var ext = fl.Extension;
181 var size = ignoreSize ? (long?)null : fl.Length;
183 if (IsUploadable(this.ServiceName, ext, size))
186 foreach (string svc in ImageServiceCombo.Items)
188 if (IsUploadable(svc, ext, size))
196 /// 指定された投稿先に投稿可能かどうかを示す値を取得する。
197 /// ファイルサイズの指定がなければ拡張子だけで判定する。
199 private bool IsUploadable(string serviceName, string ext, long? size)
201 if (!string.IsNullOrEmpty(serviceName))
203 var imageService = this.pictureService[serviceName];
204 if (imageService.CheckFileExtension(ext))
209 if (imageService.CheckFileSize(ext, size.Value))
217 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
219 private void BeginSelection(IMediaItem[] items)
221 if (items == null || items.Length == 0)
227 var service = this.SelectedService;
228 if (service == null) return;
230 var count = Math.Min(items.Length, service.MaxMediaCount);
231 if (!this.Visible || count > 1)
233 // 非表示時または複数のファイル指定は新規選択として扱う
236 this.BeginSelecting?.Invoke(this, EventArgs.Empty);
244 ImagefilePathText.Text = items[0].Path;
245 AlternativeTextBox.Text = items[0].AltText;
246 ImageFromSelectedFile(items[0], false);
250 for (var i = 0; i < count; i++)
252 var index = ImagePageCombo.Items.Count - 1;
255 ImagefilePathText.Text = items[i].Path;
256 AlternativeTextBox.Text = items[i].AltText;
258 ImageFromSelectedFile(index, items[i], false);
264 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する(主にD&D用)。
266 public void BeginSelection(string[] fileNames)
268 if (fileNames == null || fileNames.Length == 0)
274 var items = fileNames.Select(x => CreateFileMediaItem(x, false)).OfType<IMediaItem>().ToArray();
275 BeginSelection(items);
279 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
281 public void BeginSelection(Image image)
289 var items = new [] { CreateMemoryImageMediaItem(image, false) }.OfType<IMediaItem>().ToArray();
290 BeginSelection(items);
294 /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
296 public void BeginSelection()
300 this.BeginSelecting?.Invoke(this, EventArgs.Empty);
305 var media = (SelectedMedia)ImagePageCombo.SelectedItem;
306 ImageFromSelectedFile(media.Item, true);
307 ImagefilePathText.Focus();
312 /// 選択処理を終了してコントロールを隠す。
314 public void EndSelection()
318 ImagefilePathText.CausesValidation = false;
320 this.EndSelecting?.Invoke(this, EventArgs.Empty);
322 this.Visible = false;
323 this.Enabled = false;
324 ClearImageSelectedPicture();
326 ImagefilePathText.CausesValidation = true;
331 /// 選択された投稿先名と投稿する MediaItem を取得する。MediaItem は不要になったら呼び出し側にて破棄すること。
333 public bool TryGetSelectedMedia(out string imageService, out IMediaItem[] mediaItems)
335 var validItems = ImagePageCombo.Items.Cast<SelectedMedia>()
336 .Where(x => x.IsValid).Select(x => x.Item).OfType<IMediaItem>().ToArray();
338 if (validItems.Length > 0 &&
339 ImageServiceCombo.SelectedIndex > -1)
341 var serviceName = this.ServiceName;
342 if (MessageBox.Show(string.Format(Properties.Resources.PostPictureConfirm1, serviceName, validItems.Length),
343 Properties.Resources.PostPictureConfirm2,
344 MessageBoxButtons.OKCancel,
345 MessageBoxIcon.Question,
346 MessageBoxDefaultButton.Button1)
349 //収集した MediaItem が破棄されないように、予め null を代入しておく
350 foreach (SelectedMedia media in ImagePageCombo.Items)
352 if (media != null) media.Item = null;
355 imageService = serviceName;
356 mediaItems = validItems;
364 MessageBox.Show(Properties.Resources.PostPictureWarn1, Properties.Resources.PostPictureWarn2);
372 private MemoryImageMediaItem CreateMemoryImageMediaItem(Image image, bool noMsgBox)
374 if (image == null) return null;
376 MemoryImage memoryImage = null;
379 // image から png 形式の MemoryImage を生成
380 memoryImage = MemoryImage.CopyFromImage(image);
382 return new MemoryImageMediaItem(memoryImage);
386 memoryImage?.Dispose();
388 if (!noMsgBox) MessageBox.Show("Unable to create MemoryImage.");
393 private IMediaItem CreateFileMediaItem(string path, bool noMsgBox)
395 if (string.IsNullOrEmpty(path)) return null;
399 return new FileMediaItem(path);
403 if (!noMsgBox) MessageBox.Show("Invalid file path: " + path);
408 private void ValidateNewFileMediaItem(string path, string altText, bool noMsgBox)
410 var media = (SelectedMedia)ImagePageCombo.SelectedItem;
411 var item = media.Item;
413 if (path != media.Path)
415 DisposeMediaItem(media.Item);
418 item = CreateFileMediaItem(path, noMsgBox);
422 item.AltText = altText;
424 ImagefilePathText.Text = path;
425 AlternativeTextBox.Text = altText;
426 ImageFromSelectedFile(item, noMsgBox);
429 private void DisposeMediaItem(IMediaItem item)
431 var disposableItem = item as IDisposable;
432 disposableItem?.Dispose();
435 private void FilePickButton_Click(object sender, EventArgs e)
437 var service = this.SelectedService;
439 if (FilePickDialog == null || service == null) return;
440 FilePickDialog.Filter = service.SupportedFormatsStrForDialog;
441 FilePickDialog.Title = Properties.Resources.PickPictureDialog1;
442 FilePickDialog.FileName = "";
444 this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty);
448 if (FilePickDialog.ShowDialog() == DialogResult.Cancel) return;
452 this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty);
455 ValidateNewFileMediaItem(FilePickDialog.FileName, AlternativeTextBox.Text.Trim(), false);
458 private void ImagefilePathText_Validating(object sender, CancelEventArgs e)
460 if (ImageCancelButton.Focused)
462 ImagefilePathText.CausesValidation = false;
466 ValidateNewFileMediaItem(ImagefilePathText.Text.Trim(), AlternativeTextBox.Text.Trim(), false);
469 private void ImageFromSelectedFile(IMediaItem item, bool noMsgBox)
470 => this.ImageFromSelectedFile(-1, item, noMsgBox);
472 private void ImageFromSelectedFile(int index, IMediaItem item, bool noMsgBox)
478 var imageService = this.SelectedService;
479 if (imageService == null) return;
481 var selectedIndex = ImagePageCombo.SelectedIndex;
482 if (index < 0) index = selectedIndex;
484 if (index >= ImagePageCombo.Items.Count)
485 throw new ArgumentOutOfRangeException(nameof(index));
487 var isSelectedPage = (index == selectedIndex);
490 this.ClearImageSelectedPicture();
492 if (item == null || string.IsNullOrEmpty(item.Path)) return;
496 var ext = item.Extension;
497 var size = item.Size;
499 if (!imageService.CheckFileExtension(ext))
505 string.Format(Properties.Resources.PostPictureWarn3, this.ServiceName, MakeAvailableServiceText(ext, size), ext, item.Name),
506 Properties.Resources.PostPictureWarn4,
507 MessageBoxButtons.OK,
508 MessageBoxIcon.Warning);
513 if (!imageService.CheckFileSize(ext, size))
519 string.Format(Properties.Resources.PostPictureWarn5, this.ServiceName, MakeAvailableServiceText(ext, size), item.Name),
520 Properties.Resources.PostPictureWarn4,
521 MessageBoxButtons.OK,
522 MessageBoxIcon.Warning);
530 ImageSelectedPicture.Image = item.CreateImage();
531 SetImagePage(index, item, MyCommon.UploadFileType.Picture);
535 SetImagePage(index, item, MyCommon.UploadFileType.MultiMedia);
540 catch (FileNotFoundException)
542 if (!noMsgBox) MessageBox.Show("File not found.");
546 if (!noMsgBox) MessageBox.Show("The type of this file is not image.");
553 ClearImagePage(index);
554 DisposeMediaItem(item);
559 private string MakeAvailableServiceText(string ext, long fileSize)
561 var text = string.Join(", ",
562 ImageServiceCombo.Items.Cast<string>()
563 .Where(serviceName =>
564 !string.IsNullOrEmpty(serviceName) &&
565 this.pictureService[serviceName].CheckFileExtension(ext) &&
566 this.pictureService[serviceName].CheckFileSize(ext, fileSize)));
568 if (string.IsNullOrEmpty(text))
569 return Properties.Resources.PostPictureWarn6;
574 private void ClearImageSelectedPicture()
576 var oldImage = this.ImageSelectedPicture.Image;
577 this.ImageSelectedPicture.Image = null;
580 this.ImageSelectedPicture.ShowInitialImage();
583 private void ImageCancelButton_Click(object sender, EventArgs e)
584 => this.EndSelection();
586 private void ImageSelection_KeyDown(object sender, KeyEventArgs e)
588 if (e.KeyCode == Keys.Escape)
594 private void ImageSelection_KeyPress(object sender, KeyPressEventArgs e)
596 if (Convert.ToInt32(e.KeyChar) == 0x1B)
598 ImagefilePathText.CausesValidation = false;
603 private void ImageSelection_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
605 if (e.KeyCode == Keys.Escape)
607 ImagefilePathText.CausesValidation = false;
611 private void SetImageServiceCombo()
613 using (ControlTransaction.Update(ImageServiceCombo))
616 if (ImageServiceCombo.SelectedIndex > -1) svc = ImageServiceCombo.Text;
617 ImageServiceCombo.Items.Clear();
619 // Add service names to combobox
620 foreach (var key in pictureService.Keys)
622 ImageServiceCombo.Items.Add(key);
625 SelectImageServiceComboItem(svc);
629 private void SelectImageServiceComboItem(string svc, int? index = null)
632 if (string.IsNullOrEmpty(svc))
638 idx = ImageServiceCombo.Items.IndexOf(svc);
640 // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる
641 // (廃止されたサービスを選択していた場合の対応)
642 if (idx == -1) idx = 0;
647 ImageServiceCombo.SelectedIndex = idx;
649 catch (ArgumentOutOfRangeException)
651 ImageServiceCombo.SelectedIndex = 0;
654 this.UpdateAltTextPanelVisible();
657 private void UpdateAltTextPanelVisible()
658 => this.AlternativeTextPanel.Visible = this.SelectedService.CanUseAltText;
660 private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
664 var imageService = this.SelectedService;
665 if (imageService != null)
667 this.UpdateAltTextPanelVisible();
669 if (ImagePageCombo.Items.Count > 0)
671 // 画像が選択された投稿先に対応しているかをチェックする
672 // TODO: 複数の選択済み画像があるなら、できれば全てを再チェックしたほうがいい
673 if (this.ServiceName == "Twitter")
675 ValidateSelectedImagePage();
679 if (ImagePageCombo.Items.Count > 1)
681 // 複数の選択済み画像のうち、1枚目のみを残す
682 SetImagePageCombo((SelectedMedia)ImagePageCombo.Items[0]);
686 ImagePageCombo.Enabled = false;
691 var item = ((SelectedMedia)ImagePageCombo.Items[0]).Item;
694 var ext = item.Extension;
695 if (imageService.CheckFileExtension(ext) &&
696 imageService.CheckFileSize(ext, item.Size))
709 ClearImageSelectedPicture();
710 ClearSelectedImagePage();
719 this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty);
722 private void SetImagePageCombo(SelectedMedia media = null)
724 using (ControlTransaction.Update(ImagePageCombo))
726 ImagePageCombo.Enabled = false;
728 foreach (SelectedMedia oldMedia in ImagePageCombo.Items)
730 if (oldMedia == null || oldMedia == media) continue;
731 DisposeMediaItem(oldMedia.Item);
733 ImagePageCombo.Items.Clear();
736 media = new SelectedMedia("1");
738 ImagePageCombo.Items.Add(media);
739 ImagefilePathText.Text = media.Path;
740 AlternativeTextBox.Text = media.AltText;
742 ImagePageCombo.SelectedIndex = 0;
746 private void AddNewImagePage(int selectedIndex)
748 var service = this.SelectedService;
749 if (service == null) return;
751 if (selectedIndex < service.MaxMediaCount - 1)
753 // 投稿先の投稿可能枚数まで選択できるようにする
754 var count = ImagePageCombo.Items.Count;
755 if (selectedIndex == count - 1)
758 ImagePageCombo.Items.Add(new SelectedMedia(count.ToString()));
759 ImagePageCombo.Enabled = true;
764 private void SetSelectedImagePage(IMediaItem item, MyCommon.UploadFileType type)
765 => this.SetImagePage(-1, item, type);
767 private void SetImagePage(int index, IMediaItem item, MyCommon.UploadFileType type)
769 var selectedIndex = ImagePageCombo.SelectedIndex;
770 if (index < 0) index = selectedIndex;
772 var media = (SelectedMedia)ImagePageCombo.Items[index];
773 if (media.Item != item)
775 DisposeMediaItem(media.Item);
780 AddNewImagePage(index);
783 private void ClearSelectedImagePage()
784 => this.ClearImagePage(-1);
786 private void ClearImagePage(int index)
788 var selectedIndex = ImagePageCombo.SelectedIndex;
789 if (index < 0) index = selectedIndex;
791 var media = (SelectedMedia)ImagePageCombo.Items[index];
792 DisposeMediaItem(media.Item);
794 media.Type = MyCommon.UploadFileType.Invalid;
796 if (index == selectedIndex)
798 ImagefilePathText.Text = "";
799 AlternativeTextBox.Text = "";
803 private void ValidateSelectedImagePage()
805 var idx = ImagePageCombo.SelectedIndex;
806 var media = (SelectedMedia)ImagePageCombo.Items[idx];
807 ImageServiceCombo.Enabled = (idx == 0); // idx == 0 以外では投稿先サービスを選べないようにする
808 ImagefilePathText.Text = media.Path;
809 AlternativeTextBox.Text = media.AltText;
810 ImageFromSelectedFile(media.Item, true);
813 private void ImagePageCombo_SelectedIndexChanged(object sender, EventArgs e)
814 => this.ValidateSelectedImagePage();
816 private void AlternativeTextBox_Validating(object sender, CancelEventArgs e)
818 var imageFilePath = this.ImagefilePathText.Text.Trim();
819 var altText = this.AlternativeTextBox.Text.Trim();
820 this.ValidateNewFileMediaItem(imageFilePath, altText, noMsgBox: false);