OSDN Git Service

投稿画像選択のUIをMediaSelectorクラスとして分離
[opentween/open-tween.git] / OpenTween / MediaSelector.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2014 spx (@5px)
3 // All rights reserved.
4 // 
5 // This file is part of OpenTween.
6 // 
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)
10 // any later version.
11 // 
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
15 // for more details. 
16 // 
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.
21
22 using System;
23 using System.Collections.Generic;
24 using System.ComponentModel;
25 using System.Drawing;
26 using System.Data;
27 using System.IO;
28 using System.Linq;
29 using System.Text;
30 using System.Threading.Tasks;
31 using System.Windows.Forms;
32 using OpenTween.Connection;
33
34 namespace OpenTween
35 {
36     public partial class MediaSelector : UserControl
37     {
38         public event EventHandler BeginSelecting;
39         public event EventHandler EndSelecting;
40
41         public event EventHandler FilePickDialogOpening;
42         public event EventHandler FilePickDialogClosed;
43
44         public event EventHandler SelectedServiceChanged;
45
46         [Browsable(false)]
47         [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
48         public OpenFileDialog FilePickDialog { get; set; }
49
50         /// <summary>
51         /// 選択されている投稿先名を取得する。
52         /// </summary>
53         [Browsable(false)]
54         [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
55         public string ServiceName
56         {
57             get { return ImageServiceCombo.Text; }
58         }
59
60         /// <summary>
61         /// 選択されている投稿先を示すインデックスを取得する。
62         /// </summary>
63         [Browsable(false)]
64         [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
65         public int ServiceIndex
66         {
67             get { return ImageServiceCombo.SelectedIndex; }
68         }
69
70         /// <summary>
71         /// 指定された投稿先名から、作成済みの IMultimediaShareService インスタンスを取得する。
72         /// </summary>
73         public IMultimediaShareService GetService(string serviceName)
74         {
75             IMultimediaShareService service;
76             this.pictureService.TryGetValue(serviceName, out service);
77             return service;
78         }
79
80         private class SelectedMedia
81         {
82             public string Path { get; set; }
83             public MyCommon.UploadFileType Type { get; set; }
84             public string Text { get; set; }
85
86             public SelectedMedia(string path, MyCommon.UploadFileType type, string text)
87             {
88                 this.Path = path;
89                 this.Type = type;
90                 this.Text = text;
91             }
92
93             public SelectedMedia(string text)
94                 : this("", MyCommon.UploadFileType.Invalid, text)
95             {
96             }
97
98             public bool IsValid
99             {
100                 get { return this.Type != MyCommon.UploadFileType.Invalid; }
101             }
102
103             public override string ToString()
104             {
105                 return this.Text;
106             }
107         }
108
109         private Dictionary<string, IMultimediaShareService> pictureService;
110
111         private void CreateServices(Twitter tw)
112         {
113             if (this.pictureService != null) this.pictureService.Clear();
114             this.pictureService = null;
115
116             this.pictureService = new Dictionary<string, IMultimediaShareService> {
117                 {"TwitPic", new TwitPic(tw)},
118                 {"img.ly", new imgly(tw)},
119                 {"yfrog", new yfrog(tw)},
120                 {"Twitter", new TwitterPhoto(tw)},
121                 {"ついっぷるフォト", new TwipplePhoto(tw)},
122                 {"Imgur", new Imgur(tw)},
123             };
124         }
125
126         public MediaSelector()
127         {
128             InitializeComponent();
129
130             this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage;
131         }
132
133         /// <summary>
134         /// 投稿先サービスなどを初期化する。
135         /// </summary>
136         public void Initialize(Twitter tw, string svc, int? index = null)
137         {
138             CreateServices(tw);
139
140             SetImageServiceCombo();
141             SetImagePageCombo();
142
143             SelectImageServiceComboItem(svc, index);
144         }
145
146         /// <summary>
147         /// 投稿先サービスを再作成する。
148         /// </summary>
149         public void Reset(Twitter tw)
150         {
151             CreateServices(tw);
152
153             SetImageServiceCombo();
154         }
155
156         /// <summary>
157         /// 指定されたファイルの投稿に対応した投稿先があるかどうかを示す値を取得する。
158         /// </summary>
159         public bool HasUploadableService(string fileName)
160         {
161             FileInfo fl = new FileInfo(fileName);
162             string ext = fl.Extension;
163
164             var serviceName = this.ServiceName;
165             if (!string.IsNullOrEmpty(serviceName) &&
166                 this.pictureService[serviceName].CheckValidFilesize(ext, fl.Length))
167             {
168                 return true;
169             }
170
171             foreach (string svc in ImageServiceCombo.Items)
172             {
173                 if (!string.IsNullOrEmpty(svc) &&
174                     this.pictureService[svc].CheckValidFilesize(ext, fl.Length))
175                 {
176                     return true;
177                 }
178             }
179
180             return false;
181         }
182
183         /// <summary>
184         /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
185         /// D&Dをサポートする場合は引数にドロップされたファイル名を指定して呼ぶこと。
186         /// </summary>
187         public void BeginSelection(string fileName = null)
188         {
189             if (!string.IsNullOrEmpty(fileName))
190             {
191                 if (!this.Visible)
192                 {
193                     // 非表示時のファイル指定は新規選択として扱う
194                     SetImagePageCombo();
195
196                     if (this.BeginSelecting != null)
197                         this.BeginSelecting(this, EventArgs.Empty);
198
199                     this.Visible = true;
200                 }
201                 this.Enabled = true;
202                 ImagefilePathText.Text = fileName;
203                 ImageFromSelectedFile(false);
204             }
205             else
206             {
207                 if (!this.Visible)
208                 {
209                     if (this.BeginSelecting != null)
210                         this.BeginSelecting(this, EventArgs.Empty);
211
212                     this.Visible = true;
213                     this.Enabled = true;
214                     ImageFromSelectedFile(true);
215                     ImagefilePathText.Focus();
216                 }
217             }
218         }
219
220         /// <summary>
221         /// 選択処理を終了してコントロールを隠す。
222         /// </summary>
223         public void EndSelection()
224         {
225             if (this.Visible)
226             {
227                 ImagefilePathText.CausesValidation = false;
228
229                 if (this.EndSelecting != null)
230                     this.EndSelecting(this, EventArgs.Empty);
231
232                 this.Visible = false;
233                 this.Enabled = false;
234                 ClearImageSelectedPicture();
235
236                 ImagefilePathText.CausesValidation = true;
237             }
238         }
239
240         /// <summary>
241         /// 選択された投稿先名と投稿ファイル名を取得する。
242         /// </summary>
243         public bool TryGetSelectedMedia(out string imageService, out string[] imagePaths)
244         {
245             var validPaths = ImagePageCombo.Items.Cast<SelectedMedia>()
246                              .Where(x => x.IsValid).Select(x => x.Path).ToArray();
247
248             if (validPaths.Length > 0 &&
249                 ImageServiceCombo.SelectedIndex > -1)
250             {
251                 var serviceName = this.ServiceName;
252                 if (MessageBox.Show(string.Format(Properties.Resources.PostPictureConfirm1, serviceName, validPaths.Length),
253                                    Properties.Resources.PostPictureConfirm2,
254                                    MessageBoxButtons.OKCancel,
255                                    MessageBoxIcon.Question,
256                                    MessageBoxDefaultButton.Button1)
257                                == DialogResult.OK)
258                 {
259                     imageService = serviceName;
260                     imagePaths = validPaths;
261                     EndSelection();
262                     SetImagePageCombo();
263                     return true;
264                 }
265             }
266             else
267             {
268                 MessageBox.Show(Properties.Resources.PostPictureWarn1, Properties.Resources.PostPictureWarn2);
269             }
270
271             EndSelection();
272             imageService = null;
273             imagePaths = null;
274             return false;
275         }
276
277         private void FilePickButton_Click(object sender, EventArgs e)
278         {
279             if (FilePickDialog == null || string.IsNullOrEmpty(this.ServiceName)) return;
280             FilePickDialog.Filter = this.pictureService[this.ServiceName].GetFileOpenDialogFilter();
281             FilePickDialog.Title = Properties.Resources.PickPictureDialog1;
282             FilePickDialog.FileName = "";
283
284             if (this.FilePickDialogOpening != null)
285                 this.FilePickDialogOpening(this, EventArgs.Empty);
286
287             try
288             {
289                 if (FilePickDialog.ShowDialog() == DialogResult.Cancel) return;
290             }
291             finally
292             {
293                 if (this.FilePickDialogClosed != null)
294                     this.FilePickDialogClosed(this, EventArgs.Empty);
295             }
296
297             ImagefilePathText.Text = FilePickDialog.FileName;
298             ImageFromSelectedFile(false);
299         }
300
301         private void ImagefilePathText_Validating(object sender, CancelEventArgs e)
302         {
303             if (ImageCancelButton.Focused)
304             {
305                 ImagefilePathText.CausesValidation = false;
306                 return;
307             }
308
309             ImageFromSelectedFile(false);
310         }
311
312         private void ImageFromSelectedFile(bool suppressMsgBox)
313         {
314             this.ClearImageSelectedPicture();
315
316             try
317             {
318                 ImagefilePathText.Text = ImagefilePathText.Text.Trim();
319                 var fileName = ImagefilePathText.Text;
320                 var serviceName = this.ServiceName;
321                 if (string.IsNullOrEmpty(fileName) || string.IsNullOrEmpty(serviceName))
322                 {
323                     ClearSelectedImagePage();
324                     return;
325                 }
326
327                 FileInfo fl = new FileInfo(fileName);
328                 string ext = fl.Extension;
329                 var imageService = this.pictureService[serviceName];
330
331                 if (!imageService.CheckValidExtension(ext))
332                 {
333                     //画像以外の形式
334                     ClearSelectedImagePage();
335                     if (!suppressMsgBox)
336                     {
337                         MessageBox.Show(
338                             string.Format(Properties.Resources.PostPictureWarn3, serviceName, MakeAvailableServiceText(ext, fl.Length), ext),
339                             Properties.Resources.PostPictureWarn4,
340                             MessageBoxButtons.OK,
341                             MessageBoxIcon.Warning);
342                     }
343                     return;
344                 }
345
346                 if (!imageService.CheckValidFilesize(ext, fl.Length))
347                 {
348                     // ファイルサイズが大きすぎる
349                     ClearSelectedImagePage();
350                     if (!suppressMsgBox)
351                     {
352                         MessageBox.Show(
353                             string.Format(Properties.Resources.PostPictureWarn5, serviceName, MakeAvailableServiceText(ext, fl.Length)),
354                             Properties.Resources.PostPictureWarn4,
355                             MessageBoxButtons.OK,
356                             MessageBoxIcon.Warning);
357                     }
358                     return;
359                 }
360
361                 switch (imageService.GetFileType(ext))
362                 {
363                     case MyCommon.UploadFileType.Picture:
364                         using (var fs = File.OpenRead(fileName))
365                         {
366                             ImageSelectedPicture.Image = MemoryImage.CopyFromStream(fs);
367                         }
368                         SetSelectedImagePage(fileName, MyCommon.UploadFileType.Picture);
369                         break;
370                     case MyCommon.UploadFileType.MultiMedia:
371                         SetSelectedImagePage(fileName, MyCommon.UploadFileType.MultiMedia);
372                         break;
373                     default:
374                         ClearSelectedImagePage();
375                         break;
376                 }
377             }
378             catch (FileNotFoundException)
379             {
380                 ClearSelectedImagePage();
381                 if (!suppressMsgBox) MessageBox.Show("File not found.");
382             }
383             catch (Exception)
384             {
385                 ClearSelectedImagePage();
386                 if (!suppressMsgBox) MessageBox.Show("The type of this file is not image.");
387             }
388         }
389
390         private string MakeAvailableServiceText(string ext, long fileSize)
391         {
392             var text = string.Join(", ",
393                 ImageServiceCombo.Items.Cast<string>()
394                     .Where(x => !string.IsNullOrEmpty(x) && this.pictureService[x].CheckValidFilesize(ext, fileSize)));
395
396             if (string.IsNullOrEmpty(text))
397                 return Properties.Resources.PostPictureWarn6;
398
399             return text;
400         }
401
402         private void ClearImageSelectedPicture()
403         {
404             var oldImage = this.ImageSelectedPicture.Image;
405             if (oldImage != null)
406             {
407                 this.ImageSelectedPicture.Image = null;
408                 oldImage.Dispose();
409             }
410
411             this.ImageSelectedPicture.ShowInitialImage();
412         }
413
414         private void ImageCancelButton_Click(object sender, EventArgs e)
415         {
416             EndSelection();
417         }
418
419         private void ImageSelection_KeyDown(object sender, KeyEventArgs e)
420         {
421             if (e.KeyCode == Keys.Escape)
422             {
423                 EndSelection();
424             }
425         }
426
427         private void ImageSelection_KeyPress(object sender, KeyPressEventArgs e)
428         {
429             if (Convert.ToInt32(e.KeyChar) == 0x1B)
430             {
431                 ImagefilePathText.CausesValidation = false;
432                 e.Handled = true;
433             }
434         }
435
436         private void ImageSelection_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
437         {
438             if (e.KeyCode == Keys.Escape)
439             {
440                 ImagefilePathText.CausesValidation = false;
441             }
442         }
443
444         private void SetImageServiceCombo()
445         {
446             using (ControlTransaction.Update(ImageServiceCombo))
447             {
448                 string svc = "";
449                 if (ImageServiceCombo.SelectedIndex > -1) svc = ImageServiceCombo.Text;
450                 ImageServiceCombo.Items.Clear();
451
452                 // Add service names to combobox
453                 foreach (var key in pictureService.Keys)
454                 {
455                     ImageServiceCombo.Items.Add(key);
456                 }
457
458                 SelectImageServiceComboItem(svc);
459             }
460         }
461
462         private void SelectImageServiceComboItem(string svc, int? index = null)
463         {
464             int idx;
465             if (string.IsNullOrEmpty(svc))
466             {
467                 idx = index ?? 0;
468             }
469             else
470             {
471                 idx = ImageServiceCombo.Items.IndexOf(svc);
472                 if (idx == -1) idx = index ?? 0;
473             }
474
475             try
476             {
477                 ImageServiceCombo.SelectedIndex = idx;
478             }
479             catch (ArgumentOutOfRangeException)
480             {
481                 ImageServiceCombo.SelectedIndex = 0;
482             }
483         }
484
485         private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
486         {
487             if (this.Visible)
488             {
489                 var serviceName = this.ServiceName;
490                 if (!string.IsNullOrEmpty(serviceName))
491                 {
492                     if (ImagePageCombo.Items.Count > 0)
493                     {
494                         // 画像が選択された投稿先に対応しているかをチェックする
495                         // TODO: 複数の選択済み画像があるなら、できれば全てを再チェックしたほうがいい
496                         if (serviceName.Equals("Twitter"))
497                         {
498                             ValidateSelectedImagePage();
499                         }
500                         else
501                         {
502                             if (ImagePageCombo.Items.Count > 1)
503                             {
504                                 // 複数の選択済み画像のうち、1枚目のみを残す
505                                 SetImagePageCombo((SelectedMedia)ImagePageCombo.Items[0]);
506                             }
507                             else
508                             {
509                                 ImagePageCombo.Enabled = false;
510
511                                 try
512                                 {
513                                     FileInfo fi = new FileInfo(ImagefilePathText.Text.Trim());
514                                     string ext = fi.Extension;
515                                     var imageService = this.pictureService[serviceName];
516                                     if (!imageService.CheckValidFilesize(ext, fi.Length))
517                                     {
518                                         ClearImageSelectedPicture();
519                                         ClearSelectedImagePage();
520                                     }
521                                 }
522                                 catch (Exception)
523                                 {
524                                     ClearImageSelectedPicture();
525                                     ClearSelectedImagePage();
526                                 }
527                             }
528                         }
529                     }
530                 }
531             }
532
533             if (this.SelectedServiceChanged != null)
534                 this.SelectedServiceChanged(this, EventArgs.Empty);
535         }
536
537         private void SetImagePageCombo(SelectedMedia media = null)
538         {
539             using (ControlTransaction.Update(ImagePageCombo))
540             {
541                 ImagePageCombo.Enabled = false;
542                 ImagePageCombo.Items.Clear();
543                 if (media != null)
544                 {
545                     ImagePageCombo.Items.Add(media);
546                     ImagefilePathText.Text = media.Path;
547                 }
548                 else
549                 {
550                     ImagePageCombo.Items.Add(new SelectedMedia("1"));
551                     ImagefilePathText.Text = "";
552                 }
553                 ImagePageCombo.SelectedIndex = 0;
554             }
555         }
556
557         private void AddNewImagePage(int selectedIndex)
558         {
559             if (this.ServiceName.Equals("Twitter") && selectedIndex < 3)
560             {
561                 // 投稿先が Twitter であれば、最大 4 枚まで選択できるようにする
562                 var count = ImagePageCombo.Items.Count;
563                 if (selectedIndex == count - 1)
564                 {
565                     count++;
566                     ImagePageCombo.Items.Add(new SelectedMedia(count.ToString()));
567                     ImagePageCombo.Enabled = true;
568                 }
569             }
570         }
571
572         private void SetSelectedImagePage(string path, MyCommon.UploadFileType type)
573         {
574             var idx = ImagePageCombo.SelectedIndex;
575             var item = (SelectedMedia)ImagePageCombo.Items[idx];
576             item.Path = path;
577             item.Type = type;
578
579             AddNewImagePage(idx);
580         }
581
582         private void ClearSelectedImagePage()
583         {
584             var item = (SelectedMedia)ImagePageCombo.SelectedItem;
585             item.Path = "";
586             item.Type = MyCommon.UploadFileType.Invalid;
587             ImagefilePathText.Text = "";
588         }
589
590         private void ValidateSelectedImagePage()
591         {
592             var idx = ImagePageCombo.SelectedIndex;
593             var item = (SelectedMedia)ImagePageCombo.Items[idx];
594             ImageServiceCombo.Enabled = (idx == 0);  // idx == 0 以外では投稿先サービスを選べないようにする
595             ImagefilePathText.Text = item.Path;
596             ImageFromSelectedFile(true);
597         }
598
599         private void ImagePageCombo_SelectedIndexChanged(object sender, EventArgs e)
600         {
601             ValidateSelectedImagePage();
602         }
603     }
604 }