OSDN Git Service

b4df43f8218bef29d8fec56cbcea9fb4166dba23
[opentween/open-tween.git] / OpenTween / TweetThumbnail.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2012 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
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 #nullable enable
23
24 using System;
25 using System.Collections.Generic;
26 using System.ComponentModel;
27 using System.Diagnostics.CodeAnalysis;
28 using System.Drawing;
29 using System.Data;
30 using System.Linq;
31 using System.Net;
32 using System.Net.Http;
33 using System.Text;
34 using System.Windows.Forms;
35 using System.Text.RegularExpressions;
36 using System.Threading.Tasks;
37 using OpenTween.Thumbnail;
38 using System.Threading;
39 using OpenTween.Models;
40 using System.Runtime.InteropServices;
41
42 namespace OpenTween
43 {
44     public partial class TweetThumbnail : UserControl
45     {
46         protected internal List<OTPictureBox> pictureBox = new List<OTPictureBox>();
47         protected MouseWheelMessageFilter filter = new MouseWheelMessageFilter();
48
49         public event EventHandler<EventArgs> ThumbnailLoading;
50         public event EventHandler<ThumbnailDoubleClickEventArgs> ThumbnailDoubleClick;
51         public event EventHandler<ThumbnailImageSearchEventArgs> ThumbnailImageSearchClick;
52
53         public ThumbnailInfo Thumbnail
54             => (ThumbnailInfo)this.pictureBox[this.scrollBar.Value].Tag;
55
56         public TweetThumbnail()
57             => this.InitializeComponent();
58
59         public Task ShowThumbnailAsync(PostClass post)
60             => this.ShowThumbnailAsync(post, CancellationToken.None);
61
62         public async Task ShowThumbnailAsync(PostClass post, CancellationToken cancelToken)
63         {
64             var loadTasks = new List<Task>();
65
66             this.scrollBar.Enabled = false;
67             this.scrollBar.Visible = false;
68
69             if (post.ExpandedUrls.Length == 0 && post.Media.Count == 0 && post.PostGeo == null)
70             {
71                 this.SetThumbnailCount(0);
72                 return;
73             }
74
75             var thumbnails = (await this.GetThumbailInfoAsync(post, cancelToken))
76                 .ToArray();
77
78             cancelToken.ThrowIfCancellationRequested();
79
80             this.SetThumbnailCount(thumbnails.Length);
81             if (thumbnails.Length == 0)
82                 return;
83
84             for (var i = 0; i < thumbnails.Length; i++)
85             {
86                 var thumb = thumbnails[i];
87                 var picbox = this.pictureBox[i];
88
89                 picbox.Tag = thumb;
90                 picbox.ContextMenuStrip = this.contextMenuStrip;
91
92                 var loadTask = picbox.SetImageFromTask(() => thumb.LoadThumbnailImageAsync(cancelToken));
93                 loadTasks.Add(loadTask);
94
95                 var tooltipText = thumb.TooltipText;
96                 if (!MyCommon.IsNullOrEmpty(tooltipText))
97                 {
98                     this.toolTip.SetToolTip(picbox, tooltipText);
99                     picbox.AccessibleDescription = tooltipText;
100                 }
101
102                 cancelToken.ThrowIfCancellationRequested();
103             }
104
105             if (thumbnails.Length > 1)
106             {
107                 this.scrollBar.Enabled = true;
108                 this.scrollBar.Visible = true;
109             }
110
111             this.ThumbnailLoading?.Invoke(this, EventArgs.Empty);
112
113             await Task.WhenAll(loadTasks).ConfigureAwait(false);
114         }
115
116         private string GetImageSearchUriGoogle(string image_uri)
117             => @"https://www.google.com/searchbyimage?image_url=" + Uri.EscapeDataString(image_uri);
118
119         private string GetImageSearchUriSauceNao(string imageUri)
120             => @"https://saucenao.com/search.php?url=" + Uri.EscapeDataString(imageUri);
121
122         protected virtual Task<IEnumerable<ThumbnailInfo>> GetThumbailInfoAsync(PostClass post, CancellationToken token)
123             => ThumbnailGenerator.GetThumbnailsAsync(post, token);
124
125         /// <summary>
126         /// 表示するサムネイルの数を設定する
127         /// </summary>
128         /// <param name="count">表示するサムネイルの数</param>
129         protected void SetThumbnailCount(int count)
130         {
131             if (count == 0 && this.pictureBox.Count == 0)
132                 return;
133
134             using (ControlTransaction.Layout(this.panelPictureBox, false))
135             {
136                 this.panelPictureBox.Controls.Clear();
137                 foreach (var picbox in this.pictureBox)
138                 {
139                     var memoryImage = picbox.Image;
140
141                     filter.Unregister(picbox);
142
143                     picbox.MouseWheel -= this.pictureBox_MouseWheel;
144                     picbox.DoubleClick -= this.pictureBox_DoubleClick;
145                     picbox.Dispose();
146
147                     memoryImage?.Dispose();
148
149                     // メモリリーク対策 (http://stackoverflow.com/questions/2792427#2793714)
150                     picbox.ContextMenuStrip = null;
151                 }
152                 this.pictureBox.Clear();
153
154                 this.scrollBar.Maximum = (count > 0) ? count - 1 : 0;
155                 this.scrollBar.Value = 0;
156
157                 for (var i = 0; i < count; i++)
158                 {
159                     var picbox = CreatePictureBox("pictureBox" + i);
160                     picbox.Visible = (i == 0);
161                     picbox.MouseWheel += this.pictureBox_MouseWheel;
162                     picbox.DoubleClick += this.pictureBox_DoubleClick;
163
164                     filter.Register(picbox);
165
166                     this.panelPictureBox.Controls.Add(picbox);
167                     this.pictureBox.Add(picbox);
168                 }
169             }
170         }
171
172         [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope")]
173         protected virtual OTPictureBox CreatePictureBox(string name)
174         {
175             return new OTPictureBox
176             {
177                 Name = name,
178                 SizeMode = PictureBoxSizeMode.Zoom,
179                 WaitOnLoad = false,
180                 Dock = DockStyle.Fill,
181                 AccessibleRole = AccessibleRole.Graphic,
182             };
183         }
184
185         public void OpenImage(ThumbnailInfo thumb)
186             => this.ThumbnailDoubleClick?.Invoke(this, new ThumbnailDoubleClickEventArgs(thumb));
187
188         public void ScrollUp()
189         {
190             var newval = this.scrollBar.Value - this.scrollBar.SmallChange;
191
192             if (newval < this.scrollBar.Minimum)
193                 newval = this.scrollBar.Minimum;
194
195             this.scrollBar.Value = newval;
196         }
197
198         public void ScrollDown()
199         {
200             var newval = this.scrollBar.Value + this.scrollBar.SmallChange;
201
202             if (newval > this.scrollBar.Maximum)
203                 newval = this.scrollBar.Maximum;
204
205             this.scrollBar.Value = newval;
206         }
207
208         protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
209         {
210             base.ScaleControl(factor, specified);
211             OTBaseForm.ScaleChildControl(this.scrollBar, factor);
212         }
213
214         private void scrollBar_ValueChanged(object sender, EventArgs e)
215         {
216             using (ControlTransaction.Layout(this, false))
217             {
218                 var value = this.scrollBar.Value;
219                 for (var i = 0; i < this.pictureBox.Count; i++)
220                 {
221                     this.pictureBox[i].Visible = (i == value);
222                 }
223             }
224         }
225
226         private void pictureBox_MouseWheel(object sender, MouseEventArgs e)
227         {
228             if (e.Delta > 0)
229                 this.ScrollUp();
230             else
231                 this.ScrollDown();
232         }
233
234         private void pictureBox_DoubleClick(object sender, EventArgs e)
235         {
236             if (((PictureBox)sender).Tag is ThumbnailInfo thumb)
237                 this.OpenImage(thumb);
238         }
239
240         private void contextMenuStrip_Opening(object sender, CancelEventArgs e)
241         {
242             var picbox = (OTPictureBox)this.contextMenuStrip.SourceControl;
243             var thumb = (ThumbnailInfo)picbox.Tag;
244
245             var searchTargetUri = thumb.FullSizeImageUrl ?? thumb.ThumbnailImageUrl ?? null;
246             if (searchTargetUri != null)
247             {
248                 this.searchImageGoogleMenuItem.Enabled = true;
249                 this.searchImageGoogleMenuItem.Tag = searchTargetUri;
250                 this.searchImageSauceNaoMenuItem.Enabled = true;
251                 this.searchImageSauceNaoMenuItem.Tag = searchTargetUri;
252             }
253             else
254             {
255                 this.searchImageGoogleMenuItem.Enabled = false;
256                 this.searchImageSauceNaoMenuItem.Enabled = false;
257             }
258         }
259
260         private void searchSimilarImageMenuItem_Click(object sender, EventArgs e)
261         {
262             var searchTargetUri = (string)this.searchImageGoogleMenuItem.Tag;
263             var searchUri = this.GetImageSearchUriGoogle(searchTargetUri);
264
265             this.ThumbnailImageSearchClick?.Invoke(this, new ThumbnailImageSearchEventArgs(searchUri));
266         }
267
268         private void searchImageSauceNaoMenuItem_Click(object sender, EventArgs e)
269         {
270             var searchTargetUri = (string)this.searchImageSauceNaoMenuItem.Tag;
271             var searchUri = this.GetImageSearchUriSauceNao(searchTargetUri);
272
273             this.ThumbnailImageSearchClick?.Invoke(this, new ThumbnailImageSearchEventArgs(searchUri));
274         }
275
276         private void openMenuItem_Click(object sender, EventArgs e)
277             => this.OpenImage(this.Thumbnail);
278
279         private void copyUrlMenuItem_Click(object sender, EventArgs e)
280         {
281             try
282             {
283                 Clipboard.SetText(this.Thumbnail.FullSizeImageUrl ?? this.Thumbnail.MediaPageUrl);
284             }
285             catch (ExternalException ex)
286             {
287                 MessageBox.Show(ex.Message);
288             }
289         }
290     }
291
292     public class ThumbnailDoubleClickEventArgs : EventArgs
293     {
294         public ThumbnailInfo Thumbnail { get; }
295
296         public ThumbnailDoubleClickEventArgs(ThumbnailInfo thumbnail)
297             => this.Thumbnail = thumbnail;
298     }
299
300     public class ThumbnailImageSearchEventArgs : EventArgs
301     {
302         public string ImageUrl { get; }
303
304         public ThumbnailImageSearchEventArgs(string url)
305             => this.ImageUrl = url;
306     }
307 }