• R/O
  • HTTP
  • SSH
  • HTTPS

open-tween: Commit

開発に使用するリポジトリ


Commit MetaInfo

Revisión9db5558929e2ced9c0139a97efe0744087faa744 (tree)
Tiempo2023-01-19 10:42:08
AutorKimura Youichi <kim.upsilon@bucy...>
CommiterKimura Youichi

Log Message

Merge branch 'develop' into release

Cambiar Resumen

Diferencia incremental

--- a/OpenTween.Tests/MediaSelectorTest.cs
+++ b/OpenTween.Tests/MediaSelectorTest.cs
@@ -1,13 +1,33 @@
1-using System;
1+// OpenTween - Client of Twitter
2+// Copyright (c) 2014 spx (@5px)
3+// Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
4+// All rights reserved.
5+//
6+// This file is part of OpenTween.
7+//
8+// This program is free software; you can redistribute it and/or modify it
9+// under the terms of the GNU General Public License as published by the Free
10+// Software Foundation; either version 3 of the License, or (at your option)
11+// any later version.
12+//
13+// This program is distributed in the hope that it will be useful, but
14+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
16+// for more details.
17+//
18+// You should have received a copy of the GNU General Public License along
19+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
20+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
21+// Boston, MA 02110-1301, USA.
22+
23+using System;
224 using System.Collections.Generic;
3-using System.ComponentModel;
425 using System.Drawing;
526 using System.IO;
627 using System.Linq;
728 using System.Reflection;
829 using System.Runtime.InteropServices;
930 using System.Text;
10-using System.Text.RegularExpressions;
1131 using Moq;
1232 using OpenTween.Api;
1333 using OpenTween.Api.DataModel;
@@ -29,438 +49,250 @@ namespace OpenTween
2949 }
3050
3151 [Fact]
32- public void Initialize_TwitterTest()
52+ public void SelectMediaService_TwitterTest()
3353 {
3454 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
3555 using var twitter = new Twitter(twitterApi);
3656 using var mediaSelector = new MediaSelector();
3757 twitter.Initialize("", "", "", 0L);
38- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
58+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
59+ mediaSelector.SelectMediaService("Twitter");
3960
40- Assert.NotEqual(-1, mediaSelector.ImageServiceCombo.Items.IndexOf("Twitter"));
61+ Assert.Contains(mediaSelector.MediaServices, x => x.Key == "Twitter");
4162
4263 // 投稿先に Twitter が選択されている
43- Assert.Equal("Twitter", mediaSelector.ImageServiceCombo.Text);
64+ Assert.Equal("Twitter", mediaSelector.SelectedMediaServiceName);
4465
45- // ページ番号が初期化された状態
46- var pages = mediaSelector.ImagePageCombo.Items;
47- Assert.Equal(new[] { "1" }, pages.Cast<object>().Select(x => x.ToString()));
48-
49- // 代替テキストの入力欄が表示された状態
50- Assert.True(mediaSelector.AlternativeTextPanel.Visible);
66+ // 代替テキストが入力可能な状態
67+ Assert.True(mediaSelector.CanUseAltText);
5168 }
5269
5370 [Fact]
54- public void Initialize_ImgurTest()
71+ public void SelectMediaService_ImgurTest()
5572 {
5673 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
5774 using var twitter = new Twitter(twitterApi);
5875 using var mediaSelector = new MediaSelector();
5976 twitter.Initialize("", "", "", 0L);
60- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Imgur");
77+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
78+ mediaSelector.SelectMediaService("Imgur");
6179
6280 // 投稿先に Imgur が選択されている
63- Assert.Equal("Imgur", mediaSelector.ImageServiceCombo.Text);
64-
65- // ページ番号が初期化された状態
66- var pages = mediaSelector.ImagePageCombo.Items;
67- Assert.Equal(new[] { "1" }, pages.Cast<object>().Select(x => x.ToString()));
68-
69- // 代替テキストの入力欄が非表示の状態
70- Assert.False(mediaSelector.AlternativeTextPanel.Visible);
71- }
72-
73- [Fact]
74- public void BeginSelection_BlankTest()
75- {
76- using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
77- using var twitter = new Twitter(twitterApi);
78- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
79- twitter.Initialize("", "", "", 0L);
80- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
81-
82- Assert.Raises<EventArgs>(
83- x => mediaSelector.BeginSelecting += x,
84- x => mediaSelector.BeginSelecting -= x,
85- () => mediaSelector.BeginSelection()
86- );
87-
88- Assert.True(mediaSelector.Visible);
89- Assert.True(mediaSelector.Enabled);
81+ Assert.Equal("Imgur", mediaSelector.SelectedMediaServiceName);
9082
91- // 1 ページ目のみ選択可能な状態
92- var pages = mediaSelector.ImagePageCombo.Items;
93- Assert.Equal(new[] { "1" }, pages.Cast<object>().Select(x => x.ToString()));
94-
95- // 1 ページ目が表示されている
96- Assert.Equal("1", mediaSelector.ImagePageCombo.Text);
97- Assert.Equal("", mediaSelector.ImagefilePathText.Text);
98- Assert.Null(mediaSelector.ImageSelectedPicture.Image);
83+ // 代替テキストが入力できない状態
84+ Assert.False(mediaSelector.CanUseAltText);
9985 }
10086
10187 [Fact]
102- public void BeginSelection_FilePathTest()
88+ public void AddMediaItem_FilePath_SingleTest()
10389 {
10490 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
10591 using var twitter = new Twitter(twitterApi);
106- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
92+ using var mediaSelector = new MediaSelector();
10793 twitter.Initialize("", "", "", 0L);
108- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
94+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
95+ mediaSelector.SelectMediaService("Twitter");
10996
11097 var images = new[] { "Resources/re.gif" };
98+ mediaSelector.AddMediaItemFromFilePath(images);
11199
112- Assert.Raises<EventArgs>(
113- x => mediaSelector.BeginSelecting += x,
114- x => mediaSelector.BeginSelecting -= x,
115- () => mediaSelector.BeginSelection(images)
116- );
117-
118- Assert.True(mediaSelector.Visible);
119- Assert.True(mediaSelector.Enabled);
100+ // 画像が 1 つ追加された状態
101+ Assert.Single(mediaSelector.MediaItems);
120102
121- // 2 ページ目まで選択可能な状態
122- var pages = mediaSelector.ImagePageCombo.Items;
123- Assert.Equal(new[] { "1", "2" }, pages.Cast<object>().Select(x => x.ToString()));
124-
125- // 1 ページ目が表示されている
126- Assert.Equal("1", mediaSelector.ImagePageCombo.Text);
127- Assert.Equal(Path.GetFullPath("Resources/re.gif"), mediaSelector.ImagefilePathText.Text);
103+ // 1 枚目の画像が表示されている
104+ Assert.Equal(0, mediaSelector.SelectedMediaItemIndex);
105+ Assert.Equal(Path.GetFullPath("Resources/re.gif"), mediaSelector.SelectedMediaItem!.Path);
128106
129107 using var imageStream = File.OpenRead("Resources/re.gif");
130- using var image = MemoryImage.CopyFromStream(imageStream);
131- Assert.Equal(image, mediaSelector.ImageSelectedPicture.Image);
108+ using var expectedImage = MemoryImage.CopyFromStream(imageStream);
109+ using var actualImage = mediaSelector.SelectedMediaItem.CreateImage();
110+ Assert.Equal(expectedImage, actualImage);
132111 }
133112
134113 [Fact]
135- public void BeginSelection_MemoryImageTest()
114+ public void AddMediaItem_MemoryImageTest()
136115 {
137116 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
138117 using var twitter = new Twitter(twitterApi);
139- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
118+ using var mediaSelector = new MediaSelector();
140119 twitter.Initialize("", "", "", 0L);
141- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
120+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
121+ mediaSelector.SelectMediaService("Twitter");
142122
143123 using (var bitmap = new Bitmap(width: 200, height: 200))
144- {
145- Assert.Raises<EventArgs>(
146- x => mediaSelector.BeginSelecting += x,
147- x => mediaSelector.BeginSelecting -= x,
148- () => mediaSelector.BeginSelection(bitmap)
149- );
150- }
151-
152- Assert.True(mediaSelector.Visible);
153- Assert.True(mediaSelector.Enabled);
124+ mediaSelector.AddMediaItemFromImage(bitmap);
154125
155- // 2 ページ目まで選択可能な状態
156- var pages = mediaSelector.ImagePageCombo.Items;
157- Assert.Equal(new[] { "1", "2" }, pages.Cast<object>().Select(x => x.ToString()));
126+ // 画像が 1 つ追加された状態
127+ Assert.Single(mediaSelector.MediaItems);
158128
159- // 1 ページ目が表示されている
160- Assert.Equal("1", mediaSelector.ImagePageCombo.Text);
161- Assert.Matches(@"^<>MemoryImage://\d+.png$", mediaSelector.ImagefilePathText.Text);
129+ // 1 枚目の画像が表示されている
130+ Assert.Equal(0, mediaSelector.SelectedMediaItemIndex);
131+ Assert.Matches(@"^<>MemoryImage://\d+.png$", mediaSelector.SelectedMediaItem!.Path);
162132
163133 using (var bitmap = new Bitmap(width: 200, height: 200))
164134 {
165- using var image = MemoryImage.CopyFromImage(bitmap);
166- Assert.Equal(image, mediaSelector.ImageSelectedPicture.Image);
135+ using var expectedImage = MemoryImage.CopyFromImage(bitmap);
136+ using var actualImage = mediaSelector.SelectedMediaItem.CreateImage();
137+ Assert.Equal(expectedImage, actualImage);
167138 }
168139 }
169140
170141 [Fact]
171- public void BeginSelection_MultiImageTest()
142+ public void AddMediaItem_FilePath_MultipleTest()
172143 {
173144 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
174145 using var twitter = new Twitter(twitterApi);
175- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
146+ using var mediaSelector = new MediaSelector();
176147 twitter.Initialize("", "", "", 0L);
177- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
148+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
149+ mediaSelector.SelectMediaService("Twitter");
178150
179151 var images = new[] { "Resources/re.gif", "Resources/re1.png" };
180- mediaSelector.BeginSelection(images);
181-
182- // 3 ページ目まで選択可能な状態
183- var pages = mediaSelector.ImagePageCombo.Items;
184- Assert.Equal(new[] { "1", "2", "3" }, pages.Cast<object>().Select(x => x.ToString()));
185-
186- // 1 ページ目が表示されている
187- Assert.Equal("1", mediaSelector.ImagePageCombo.Text);
188- Assert.Equal(Path.GetFullPath("Resources/re.gif"), mediaSelector.ImagefilePathText.Text);
189-
190- using var imageStream = File.OpenRead("Resources/re.gif");
191- using var image = MemoryImage.CopyFromStream(imageStream);
192- Assert.Equal(image, mediaSelector.ImageSelectedPicture.Image);
193- }
194-
195- [Fact]
196- public void EndSelection_Test()
197- {
198- using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
199- using var twitter = new Twitter(twitterApi);
200- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
201- twitter.Initialize("", "", "", 0L);
202- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
203- mediaSelector.BeginSelection(new[] { "Resources/re.gif" });
204-
205- var displayImage = mediaSelector.ImageSelectedPicture.Image; // 表示中の画像
152+ mediaSelector.AddMediaItemFromFilePath(images);
206153
207- Assert.Raises<EventArgs>(
208- x => mediaSelector.EndSelecting += x,
209- x => mediaSelector.EndSelecting -= x,
210- () => mediaSelector.EndSelection()
211- );
154+ // 画像が 2 つ追加された状態
155+ Assert.Equal(2, mediaSelector.MediaItems.Count);
212156
213- Assert.False(mediaSelector.Visible);
214- Assert.False(mediaSelector.Enabled);
157+ // 最後の画像(2 枚目)が表示されている
158+ Assert.Equal(1, mediaSelector.SelectedMediaItemIndex);
159+ Assert.Equal(Path.GetFullPath("Resources/re1.png"), mediaSelector.SelectedMediaItem!.Path);
215160
216- Assert.True(displayImage!.IsDisposed);
161+ using var imageStream = File.OpenRead("Resources/re1.png");
162+ using var expectedImage = MemoryImage.CopyFromStream(imageStream);
163+ using var actualImage = mediaSelector.SelectedMediaItem.CreateImage();
164+ Assert.Equal(expectedImage, actualImage);
217165 }
218166
219167 [Fact]
220- public void PageChange_Test()
168+ public void ClearMediaItems_Test()
221169 {
222170 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
223171 using var twitter = new Twitter(twitterApi);
224- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
172+ using var mediaSelector = new MediaSelector();
225173 twitter.Initialize("", "", "", 0L);
226- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
227-
228- var images = new[] { "Resources/re.gif", "Resources/re1.png" };
229- mediaSelector.BeginSelection(images);
174+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
175+ mediaSelector.SelectMediaService("Twitter");
230176
231- mediaSelector.ImagePageCombo.SelectedIndex = 0;
177+ mediaSelector.AddMediaItemFromFilePath(new[] { "Resources/re.gif" });
232178
233- // 1 ページ目
234- Assert.Equal("1", mediaSelector.ImagePageCombo.Text);
235- Assert.Equal(Path.GetFullPath("Resources/re.gif"), mediaSelector.ImagefilePathText.Text);
236-
237- using (var imageStream = File.OpenRead("Resources/re.gif"))
238- {
239- using var image = MemoryImage.CopyFromStream(imageStream);
240- Assert.Equal(image, mediaSelector.ImageSelectedPicture.Image);
241- }
242-
243- mediaSelector.ImagePageCombo.SelectedIndex = 1;
244-
245- // 2 ページ目
246- Assert.Equal("2", mediaSelector.ImagePageCombo.Text);
247- Assert.Equal(Path.GetFullPath("Resources/re1.png"), mediaSelector.ImagefilePathText.Text);
248-
249- using (var imageStream = File.OpenRead("Resources/re1.png"))
250- {
251- using var image = MemoryImage.CopyFromStream(imageStream);
252- Assert.Equal(image, mediaSelector.ImageSelectedPicture.Image);
253- }
179+ var mediaItems = mediaSelector.MediaItems.ToArray();
180+ var thumbnailImages = mediaSelector.ThumbnailList.ToArray(); // 表示中の画像
254181
255- mediaSelector.ImagePageCombo.SelectedIndex = 2;
182+ mediaSelector.ClearMediaItems();
256183
257- // 3 ページ目 (新規ページ)
258- Assert.Equal("3", mediaSelector.ImagePageCombo.Text);
259- Assert.Equal("", mediaSelector.ImagefilePathText.Text);
260- Assert.Null(mediaSelector.ImageSelectedPicture.Image);
184+ Assert.True(mediaItems.All(x => x.IsDisposed));
185+ Assert.True(thumbnailImages.All(x => x.IsDisposed));
261186 }
262187
263188 [Fact]
264- public void PageChange_AlternativeTextTest()
189+ public void DetachMediaItems_Test()
265190 {
266191 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
267192 using var twitter = new Twitter(twitterApi);
268- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
193+ using var mediaSelector = new MediaSelector();
269194 twitter.Initialize("", "", "", 0L);
270- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
271-
272- var images = new[] { "Resources/re.gif", "Resources/re1.png" };
273- mediaSelector.BeginSelection(images);
195+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
196+ mediaSelector.SelectMediaService("Twitter");
274197
275- // 1 ページ目
276- mediaSelector.ImagePageCombo.SelectedIndex = 0;
277- mediaSelector.AlternativeTextBox.Text = "Page 1";
278- mediaSelector.ValidateChildren();
279-
280- // 2 ページ目
281- mediaSelector.ImagePageCombo.SelectedIndex = 1;
282- mediaSelector.AlternativeTextBox.Text = "Page 2";
283- mediaSelector.ValidateChildren();
198+ mediaSelector.AddMediaItemFromFilePath(new[] { "Resources/re.gif" });
284199
285- // 3 ページ目 (新規ページ)
286- mediaSelector.ImagePageCombo.SelectedIndex = 2;
287- mediaSelector.AlternativeTextBox.Text = "Page 3";
288- mediaSelector.ValidateChildren();
200+ var thumbnailImages = mediaSelector.ThumbnailList.ToArray();
289201
290- mediaSelector.ImagePageCombo.SelectedIndex = 0;
291- Assert.Equal("Page 1", mediaSelector.AlternativeTextBox.Text);
202+ var detachedMediaItems = mediaSelector.DetachMediaItems();
292203
293- mediaSelector.ImagePageCombo.SelectedIndex = 1;
294- Assert.Equal("Page 2", mediaSelector.AlternativeTextBox.Text);
204+ Assert.Empty(mediaSelector.MediaItems);
205+ Assert.True(thumbnailImages.All(x => x.IsDisposed));
295206
296- // 画像が指定されていないページは入力した代替テキストも保持されない
297- mediaSelector.ImagePageCombo.SelectedIndex = 2;
298- Assert.Equal("", mediaSelector.AlternativeTextBox.Text);
207+ // DetachMediaItems で切り離された MediaItem は破棄しない
208+ Assert.True(detachedMediaItems.All(x => !x.IsDisposed));
299209 }
300210
301211 [Fact]
302- public void PageChange_ImageDisposeTest()
212+ public void SelectedMediaItemChange_Test()
303213 {
304214 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
305215 using var twitter = new Twitter(twitterApi);
306- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
216+ using var mediaSelector = new MediaSelector();
307217 twitter.Initialize("", "", "", 0L);
308- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
218+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
219+ mediaSelector.SelectMediaService("Twitter");
309220
310221 var images = new[] { "Resources/re.gif", "Resources/re1.png" };
311- mediaSelector.BeginSelection(images);
222+ mediaSelector.AddMediaItemFromFilePath(images);
312223
313- mediaSelector.ImagePageCombo.SelectedIndex = 0;
224+ mediaSelector.SelectedMediaItemIndex = 0;
314225
315226 // 1 ページ目
316- var page1Image = mediaSelector.ImageSelectedPicture.Image;
317-
318- mediaSelector.ImagePageCombo.SelectedIndex = 1;
319-
320- // 2 ページ目
321- var page2Image = mediaSelector.ImageSelectedPicture.Image;
322- Assert.True(page1Image!.IsDisposed); // 前ページの画像が破棄されているか
323-
324- mediaSelector.ImagePageCombo.SelectedIndex = 2;
227+ Assert.Equal(Path.GetFullPath("Resources/re.gif"), mediaSelector.SelectedMediaItem!.Path);
325228
326- // 3 ページ目 (新規ページ)
327- Assert.True(page2Image!.IsDisposed); // 前ページの画像が破棄されているか
328- }
229+ using (var imageStream = File.OpenRead("Resources/re.gif"))
230+ {
231+ using var expectedImage = MemoryImage.CopyFromStream(imageStream);
232+ using var actualImage = mediaSelector.SelectedMediaItem.CreateImage();
233+ Assert.Equal(expectedImage, actualImage);
234+ }
329235
330- [Fact]
331- public void ImagePathInput_Test()
332- {
333- using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
334- using var twitter = new Twitter(twitterApi);
335- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
336- twitter.Initialize("", "", "", 0L);
337- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
338- mediaSelector.BeginSelection();
236+ mediaSelector.SelectedMediaItemIndex = 1;
339237
340- // 画像のファイルパスを入力
341- mediaSelector.ImagefilePathText.Text = Path.GetFullPath("Resources/re1.png");
342- TestUtils.Validate(mediaSelector.ImagefilePathText);
238+ // 2 ページ目
239+ Assert.Equal(Path.GetFullPath("Resources/re1.png"), mediaSelector.SelectedMediaItem!.Path);
343240
344- // 入力したパスの画像が表示される
345241 using (var imageStream = File.OpenRead("Resources/re1.png"))
346242 {
347- using var image = MemoryImage.CopyFromStream(imageStream);
348- Assert.Equal(image, mediaSelector.ImageSelectedPicture.Image);
243+ using var expectedImage = MemoryImage.CopyFromStream(imageStream);
244+ using var actualImage = mediaSelector.SelectedMediaItem.CreateImage();
245+ Assert.Equal(expectedImage, actualImage);
349246 }
350-
351- // 2 ページ目まで選択可能な状態
352- var pages = mediaSelector.ImagePageCombo.Items;
353- Assert.Equal(new[] { "1", "2" }, pages.Cast<object>().Select(x => x.ToString()));
354247 }
355248
356249 [Fact]
357- public void ImagePathInput_ReplaceFileMediaItemTest()
250+ public void SelectedMediaItemChange_DisposeTest()
358251 {
359252 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
360253 using var twitter = new Twitter(twitterApi);
361- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
254+ using var mediaSelector = new MediaSelector();
362255 twitter.Initialize("", "", "", 0L);
363- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
364-
365- mediaSelector.BeginSelection(new[] { "Resources/re.gif" });
256+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
257+ mediaSelector.SelectMediaService("Twitter");
366258
367- // 既に入力されているファイルパスの画像
368- var image1 = mediaSelector.ImageSelectedPicture.Image;
259+ var images = new[] { "Resources/re.gif", "Resources/re1.png" };
260+ mediaSelector.AddMediaItemFromFilePath(images);
369261
370- // 別の画像のファイルパスを入力
371- mediaSelector.ImagefilePathText.Text = Path.GetFullPath("Resources/re1.png");
372- TestUtils.Validate(mediaSelector.ImagefilePathText);
262+ // 1 枚目
263+ mediaSelector.SelectedMediaItemIndex = 0;
264+ var firstImage = mediaSelector.SelectedMediaItemImage;
373265
374- // 入力したパスの画像が表示される
375- using (var imageStream = File.OpenRead("Resources/re1.png"))
376- {
377- using var image2 = MemoryImage.CopyFromStream(imageStream);
378- Assert.Equal(image2, mediaSelector.ImageSelectedPicture.Image);
379- }
266+ // 2 枚目
267+ mediaSelector.SelectedMediaItemIndex = 1;
268+ var secondImage = mediaSelector.SelectedMediaItemImage;
380269
381- // 最初に入力されていたファイルパスの表示用の MemoryImage は破棄される
382- Assert.True(image1!.IsDisposed);
270+ Assert.True(firstImage!.IsDisposed);
383271 }
384272
385273 [Fact]
386- public void ImagePathInput_ReplaceMemoryImageMediaItemTest()
274+ public void SetSelectedMediaAltText_Test()
387275 {
388276 using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
389277 using var twitter = new Twitter(twitterApi);
390- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
278+ using var mediaSelector = new MediaSelector();
391279 twitter.Initialize("", "", "", 0L);
392- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
393-
394- using (var bitmap = new Bitmap(width: 200, height: 200))
395- {
396- mediaSelector.BeginSelection(bitmap);
397- }
398-
399- // 既に入力されているファイルパスの画像
400- var image1 = mediaSelector.ImageSelectedPicture.Image;
401-
402- // 内部で保持されている MemoryImageMediaItem を取り出す
403- var selectedMedia = mediaSelector.ImagePageCombo.SelectedItem;
404- var mediaProperty = selectedMedia.GetType().GetProperty("Item");
405- var mediaItem = (MemoryImageMediaItem)mediaProperty.GetValue(selectedMedia);
406-
407- // 別の画像のファイルパスを入力
408- mediaSelector.ImagefilePathText.Text = Path.GetFullPath("Resources/re1.png");
409- TestUtils.Validate(mediaSelector.ImagefilePathText);
280+ mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration());
281+ mediaSelector.SelectMediaService("Twitter");
410282
411- // 入力したパスの画像が表示される
412- using (var imageStream = File.OpenRead("Resources/re1.png"))
413- {
414- using var image2 = MemoryImage.CopyFromStream(imageStream);
415- Assert.Equal(image2, mediaSelector.ImageSelectedPicture.Image);
416- }
283+ var images = new[] { "Resources/re.gif", "Resources/re1.png" };
284+ mediaSelector.AddMediaItemFromFilePath(images);
417285
418- // 最初に入力されていたファイルパスの表示用の MemoryImage は破棄される
419- Assert.True(image1!.IsDisposed);
286+ // 1 ページ目
287+ mediaSelector.SelectedMediaItemIndex = 0;
288+ mediaSelector.SetSelectedMediaAltText("Page 1");
420289
421- // 参照されなくなった MemoryImageMediaItem も破棄される
422- Assert.True(mediaItem.IsDisposed);
423- }
290+ // 2 ページ目
291+ mediaSelector.SelectedMediaItemIndex = 1;
292+ mediaSelector.SetSelectedMediaAltText("Page 2");
424293
425- [Fact]
426- public void ImageServiceChange_Test()
427- {
428- using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create(""));
429- using var twitter = new Twitter(twitterApi);
430- using var mediaSelector = new MediaSelector { Visible = false, Enabled = false };
431- twitter.Initialize("", "", "", 0L);
432- mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter");
433-
434- Assert.Equal("Twitter", mediaSelector.ServiceName);
435-
436- mediaSelector.BeginSelection(new[] { "Resources/re.gif", "Resources/re1.png" });
437-
438- // 3 ページ目まで選択可能な状態
439- var pages = mediaSelector.ImagePageCombo.Items;
440- Assert.Equal(new[] { "1", "2", "3" }, pages.Cast<object>().Select(x => x.ToString()));
441- Assert.True(mediaSelector.ImagePageCombo.Enabled);
442-
443- // 投稿先を Imgur に変更
444- var imgurIndex = mediaSelector.ImageServiceCombo.Items.IndexOf("Imgur");
445- Assert.Raises<EventArgs>(
446- x => mediaSelector.SelectedServiceChanged += x,
447- x => mediaSelector.SelectedServiceChanged -= x,
448- () => mediaSelector.ImageServiceCombo.SelectedIndex = imgurIndex
449- );
450-
451- // 1 ページ目のみ選択可能な状態 (Disabled)
452- pages = mediaSelector.ImagePageCombo.Items;
453- Assert.Equal(new[] { "1" }, pages.Cast<object>().Select(x => x.ToString()));
454- Assert.False(mediaSelector.ImagePageCombo.Enabled);
455-
456- // 投稿先を Twitter に変更
457- mediaSelector.ImageServiceCombo.SelectedIndex =
458- mediaSelector.ImageServiceCombo.Items.IndexOf("Twitter");
459-
460- // 2 ページ目まで選択可能な状態
461- pages = mediaSelector.ImagePageCombo.Items;
462- Assert.Equal(new[] { "1", "2" }, pages.Cast<object>().Select(x => x.ToString()));
463- Assert.True(mediaSelector.ImagePageCombo.Enabled);
294+ Assert.Equal("Page 1", mediaSelector.MediaItems[0].AltText);
295+ Assert.Equal("Page 2", mediaSelector.MediaItems[1].AltText);
464296 }
465297 }
466298 }
--- a/OpenTween/Api/TwitterApiException.cs
+++ b/OpenTween/Api/TwitterApiException.cs
@@ -43,6 +43,9 @@ namespace OpenTween.Api
4343 public TwitterErrorItem[] Errors
4444 => this.ErrorResponse != null ? this.ErrorResponse.Errors : Array.Empty<TwitterErrorItem>();
4545
46+ public string[] LongMessages
47+ => this.Errors.Select(x => x.Message).ToArray();
48+
4649 public TwitterApiException()
4750 {
4851 }
--- a/OpenTween/AppendSettingDialog.cs
+++ b/OpenTween/AppendSettingDialog.cs
@@ -39,6 +39,7 @@ using System.Text;
3939 using System.Threading;
4040 using System.Threading.Tasks;
4141 using System.Windows.Forms;
42+using OpenTween.Api;
4243 using OpenTween.Connection;
4344 using OpenTween.Setting.Panel;
4445 using OpenTween.Thumbnail;
@@ -208,9 +209,11 @@ namespace OpenTween
208209 "Authenticate",
209210 MessageBoxButtons.OK);
210211 }
211- catch (WebApiException ex)
212+ catch (TwitterApiException ex)
212213 {
213- var message = Properties.Resources.AuthorizeButton_Click2 + Environment.NewLine + ex.Message;
214+ var message = Properties.Resources.AuthorizeButton_Click2 + Environment.NewLine +
215+ string.Join(Environment.NewLine, ex.LongMessages);
216+
214217 MessageBox.Show(this, message, "Authenticate", MessageBoxButtons.OK);
215218 }
216219 }
--- a/OpenTween/Connection/TwitterApiConnection.cs
+++ b/OpenTween/Connection/TwitterApiConnection.cs
@@ -107,7 +107,7 @@ namespace OpenTween.Connection
107107 if (endpointName != null)
108108 MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName);
109109
110- await this.CheckStatusCode(response)
110+ await TwitterApiConnection.CheckStatusCode(response)
111111 .ConfigureAwait(false);
112112
113113 using var content = response.Content;
@@ -190,7 +190,7 @@ namespace OpenTween.Connection
190190 var response = await this.HttpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
191191 .ConfigureAwait(false);
192192
193- await this.CheckStatusCode(response)
193+ await TwitterApiConnection.CheckStatusCode(response)
194194 .ConfigureAwait(false);
195195
196196 return await response.Content.ReadAsStreamAsync()
@@ -220,7 +220,7 @@ namespace OpenTween.Connection
220220 response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
221221 .ConfigureAwait(false);
222222
223- await this.CheckStatusCode(response)
223+ await TwitterApiConnection.CheckStatusCode(response)
224224 .ConfigureAwait(false);
225225
226226 var result = new LazyJson<T>(response);
@@ -267,7 +267,7 @@ namespace OpenTween.Connection
267267 response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
268268 .ConfigureAwait(false);
269269
270- await this.CheckStatusCode(response)
270+ await TwitterApiConnection.CheckStatusCode(response)
271271 .ConfigureAwait(false);
272272
273273 var result = new LazyJson<T>(response);
@@ -313,7 +313,7 @@ namespace OpenTween.Connection
313313 using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
314314 .ConfigureAwait(false);
315315
316- await this.CheckStatusCode(response)
316+ await TwitterApiConnection.CheckStatusCode(response)
317317 .ConfigureAwait(false);
318318 }
319319 catch (HttpRequestException ex)
@@ -345,7 +345,7 @@ namespace OpenTween.Connection
345345 response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
346346 .ConfigureAwait(false);
347347
348- await this.CheckStatusCode(response)
348+ await TwitterApiConnection.CheckStatusCode(response)
349349 .ConfigureAwait(false);
350350
351351 var result = new LazyJson<T>(response);
@@ -377,7 +377,7 @@ namespace OpenTween.Connection
377377 using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)
378378 .ConfigureAwait(false);
379379
380- await this.CheckStatusCode(response)
380+ await TwitterApiConnection.CheckStatusCode(response)
381381 .ConfigureAwait(false);
382382 }
383383 catch (HttpRequestException ex)
@@ -390,7 +390,7 @@ namespace OpenTween.Connection
390390 }
391391 }
392392
393- protected async Task CheckStatusCode(HttpResponseMessage response)
393+ protected static async Task CheckStatusCode(HttpResponseMessage response)
394394 {
395395 var statusCode = response.StatusCode;
396396
@@ -546,8 +546,8 @@ namespace OpenTween.Connection
546546 var responseText = await content.ReadAsStringAsync()
547547 .ConfigureAwait(false);
548548
549- if (!response.IsSuccessStatusCode)
550- throw new TwitterApiException(response.StatusCode, responseText);
549+ await TwitterApiConnection.CheckStatusCode(response)
550+ .ConfigureAwait(false);
551551
552552 var responseParams = HttpUtility.ParseQueryString(responseText);
553553
--- a/OpenTween/Extensions.cs
+++ b/OpenTween/Extensions.cs
@@ -158,6 +158,19 @@ namespace OpenTween
158158 return count;
159159 }
160160
161+ public static bool TryInvoke(this Control control, Action action)
162+ {
163+ if (control.IsDisposed)
164+ return false;
165+
166+ if (control.InvokeRequired)
167+ control.Invoke(action);
168+ else
169+ action();
170+
171+ return true;
172+ }
173+
161174 public static Task ForEachAsync<T>(this IObservable<T> observable, Action<T> subscriber)
162175 {
163176 return ForEachAsync(observable, value =>
--- a/OpenTween/MediaItem.cs
+++ b/OpenTween/MediaItem.cs
@@ -30,9 +30,19 @@ using System.Threading;
3030
3131 namespace OpenTween
3232 {
33- public interface IMediaItem
33+ public interface IMediaItem : IDisposable
3434 {
3535 /// <summary>
36+ /// メディアのID
37+ /// </summary>
38+ Guid Id { get; }
39+
40+ /// <summary>
41+ /// メディアが既に破棄されているかを示す真偽値
42+ /// </summary>
43+ bool IsDisposed { get; }
44+
45+ /// <summary>
3646 /// メディアへの絶対パス
3747 /// </summary>
3848 string Path { get; }
@@ -58,11 +68,6 @@ namespace OpenTween
5868 long Size { get; }
5969
6070 /// <summary>
61- /// メディアが画像であるかどうかを示す真偽値
62- /// </summary>
63- bool IsImage { get; }
64-
65- /// <summary>
6671 /// 代替テキスト (アップロード先が対応している必要がある)
6772 /// </summary>
6873 string? AltText { get; set; }
@@ -106,6 +111,10 @@ namespace OpenTween
106111 {
107112 }
108113
114+ public Guid Id { get; } = Guid.NewGuid();
115+
116+ public bool IsDisposed { get; private set; } = false;
117+
109118 public string Path
110119 => this.FileInfo.FullName;
111120
@@ -121,34 +130,6 @@ namespace OpenTween
121130 public long Size
122131 => this.FileInfo.Length;
123132
124- public bool IsImage
125- {
126- get
127- {
128- if (this.isImage == null)
129- {
130- try
131- {
132- // MemoryImage が生成できるかを検証する
133- using (var image = this.CreateImage())
134- {
135- }
136-
137- this.isImage = true;
138- }
139- catch (InvalidImageException)
140- {
141- this.isImage = false;
142- }
143- }
144-
145- return this.isImage.Value;
146- }
147- }
148-
149- /// <summary>IsImage の検証結果をキャッシュする。未検証なら null</summary>
150- private bool? isImage = null;
151-
152133 public MemoryImage CreateImage()
153134 {
154135 using var fs = this.FileInfo.OpenRead();
@@ -163,6 +144,14 @@ namespace OpenTween
163144 using var fs = this.FileInfo.OpenRead();
164145 fs.CopyTo(stream);
165146 }
147+
148+ public void Dispose()
149+ {
150+ if (this.IsDisposed)
151+ return;
152+
153+ this.IsDisposed = true;
154+ }
166155 }
167156
168157 /// <summary>
@@ -171,7 +160,7 @@ namespace OpenTween
171160 /// <remarks>
172161 /// 用途の関係上、メモリ使用量が大きくなるため、不要になればできるだけ破棄すること
173162 /// </remarks>
174- public class MemoryImageMediaItem : IMediaItem, IDisposable
163+ public class MemoryImageMediaItem : IMediaItem
175164 {
176165 public const string PathPrefix = "<>MemoryImage://";
177166 private static int fileNumber = 0;
@@ -187,6 +176,8 @@ namespace OpenTween
187176 this.Path = PathPrefix + num + this.image.ImageFormatExt;
188177 }
189178
179+ public Guid Id { get; } = Guid.NewGuid();
180+
190181 public string Path { get; }
191182
192183 public string? AltText { get; set; }
@@ -203,9 +194,6 @@ namespace OpenTween
203194 public long Size
204195 => this.image.Stream.Length;
205196
206- public bool IsImage
207- => true;
208-
209197 public MemoryImage CreateImage()
210198 => this.image.Clone();
211199
--- a/OpenTween/MediaSelector.cs
+++ b/OpenTween/MediaSelector.cs
@@ -1,5 +1,6 @@
11 // OpenTween - Client of Twitter
22 // Copyright (c) 2014 spx (@5px)
3+// Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
34 // All rights reserved.
45 //
56 // This file is part of OpenTween.
@@ -24,158 +25,127 @@
2425 using System;
2526 using System.Collections.Generic;
2627 using System.ComponentModel;
27-using System.Data;
28-using System.Diagnostics.CodeAnalysis;
2928 using System.Drawing;
3029 using System.IO;
3130 using System.Linq;
3231 using System.Text;
3332 using System.Threading.Tasks;
34-using System.Windows.Forms;
3533 using OpenTween.Api.DataModel;
3634 using OpenTween.MediaUploadServices;
3735
3836 namespace OpenTween
3937 {
40- public partial class MediaSelector : UserControl
38+ public sealed class MediaSelector : NotifyPropertyChangedBase, IDisposable
4139 {
42- public event EventHandler<EventArgs>? BeginSelecting;
40+ private KeyValuePair<string, IMediaUploadService>[] pictureServices = Array.Empty<KeyValuePair<string, IMediaUploadService>>();
41+ private readonly BindingList<IMediaItem> mediaItems = new();
42+ private string selectedMediaServiceName = "";
43+ private Guid? selectedMediaItemId = null;
44+ private MemoryImage? selectedMediaItemImage = null;
4345
44- public event EventHandler<EventArgs>? EndSelecting;
46+ public bool IsDisposed { get; private set; } = false;
4547
46- public event EventHandler<EventArgs>? FilePickDialogOpening;
47-
48- public event EventHandler<EventArgs>? FilePickDialogClosed;
48+ public KeyValuePair<string, IMediaUploadService>[] MediaServices
49+ {
50+ get => this.pictureServices;
51+ private set => this.SetProperty(ref this.pictureServices, value);
52+ }
4953
50- public event EventHandler<EventArgs>? SelectedServiceChanged;
54+ public BindingList<IMediaItem> MediaItems
55+ => this.mediaItems;
5156
52- [Browsable(false)]
53- [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
54- public OpenFileDialog? FilePickDialog { get; set; }
57+ public MemoryImageList ThumbnailList { get; } = new();
5558
5659 /// <summary>
5760 /// 選択されている投稿先名を取得する。
5861 /// </summary>
59- [Browsable(false)]
60- [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
61- public string ServiceName
62- => this.ImageServiceCombo.Text;
62+ public string SelectedMediaServiceName
63+ {
64+ get => this.selectedMediaServiceName;
65+ set => this.SetProperty(ref this.selectedMediaServiceName, value);
66+ }
6367
6468 /// <summary>
6569 /// 選択されている投稿先を示すインデックスを取得する。
6670 /// </summary>
67- [Browsable(false)]
68- [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
69- public int ServiceIndex
70- => this.ImageServiceCombo.SelectedIndex;
71+ public int SelectedMediaServiceIndex
72+ => this.MediaServices.FindIndex(x => x.Key == this.SelectedMediaServiceName);
7173
7274 /// <summary>
7375 /// 選択されている投稿先の IMediaUploadService を取得する。
7476 /// </summary>
75- [Browsable(false)]
76- [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
77- public IMediaUploadService? SelectedService
78- {
79- get
80- {
81- var serviceName = this.ServiceName;
82- if (MyCommon.IsNullOrEmpty(serviceName))
83- return null;
84-
85- return this.pictureService.TryGetValue(serviceName, out var service)
86- ? service : null;
87- }
88- }
89-
90- /// <summary>
91- /// 指定された投稿先名から、作成済みの IMediaUploadService インスタンスを取得する。
92- /// </summary>
93- public IMediaUploadService GetService(string serviceName)
94- {
95- this.pictureService.TryGetValue(serviceName, out var service);
96- return service;
97- }
77+ public IMediaUploadService? SelectedMediaService
78+ => this.GetService(this.SelectedMediaServiceName);
9879
99- /// <summary>
100- /// 利用可能な全ての IMediaUploadService インスタンスを取得する。
101- /// </summary>
102- public ICollection<IMediaUploadService> GetServices()
103- => this.pictureService.Values;
80+ public bool CanUseAltText
81+ => this.SelectedMediaService?.CanUseAltText ?? false;
10482
105- private class SelectedMedia
83+ public Guid? SelectedMediaItemId
10684 {
107- public IMediaItem? Item { get; set; }
108-
109- public MyCommon.UploadFileType Type { get; set; }
110-
111- public string Text { get; set; }
112-
113- public SelectedMedia(IMediaItem? item, MyCommon.UploadFileType type, string text)
85+ get => this.selectedMediaItemId;
86+ set
11487 {
115- this.Item = item;
116- this.Type = type;
117- this.Text = text;
118- }
88+ if (this.selectedMediaItemId == value)
89+ return;
11990
120- public SelectedMedia(string text)
121- : this(null, MyCommon.UploadFileType.Invalid, text)
122- {
91+ this.SetProperty(ref this.selectedMediaItemId, value);
92+ this.LoadSelectedMediaItemImage();
12393 }
124-
125- public bool IsValid
126- => this.Item != null && this.Type != MyCommon.UploadFileType.Invalid;
127-
128- public string Path
129- => this.Item?.Path ?? "";
130-
131- public string AltText => this.Item?.AltText ?? "";
132-
133- public override string ToString()
134- => this.Text;
13594 }
13695
137- private Dictionary<string, IMediaUploadService> pictureService = new();
96+ public IMediaItem? SelectedMediaItem
97+ => this.SelectedMediaItemId != null ? this.MediaItems.First(x => x.Id == this.SelectedMediaItemId) : null;
13898
139- private void CreateServices(Twitter tw, TwitterConfiguration twitterConfig)
99+ public int SelectedMediaItemIndex
140100 {
141- this.pictureService?.Clear();
142-
143- this.pictureService = new Dictionary<string, IMediaUploadService>
144- {
145- ["Twitter"] = new TwitterPhoto(tw, twitterConfig),
146- ["Imgur"] = new Imgur(twitterConfig),
147- ["Mobypicture"] = new Mobypicture(tw, twitterConfig),
148- };
101+ get => this.MediaItems.FindIndex(x => x.Id == this.SelectedMediaItemId);
102+ set => this.SelectedMediaItemId = value != -1 ? this.MediaItems[value].Id : null;
149103 }
150104
151- public MediaSelector()
105+ public MemoryImage? SelectedMediaItemImage
152106 {
153- this.InitializeComponent();
154-
155- this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage;
107+ get => this.selectedMediaItemImage;
108+ set => this.SetProperty(ref this.selectedMediaItemImage, value);
156109 }
157110
158111 /// <summary>
159- /// 投稿先サービスなどを初期化する。
112+ /// 指定された投稿先名から、作成済みの IMediaUploadService インスタンスを取得する。
160113 /// </summary>
161- public void Initialize(Twitter tw, TwitterConfiguration twitterConfig, string svc, int? index = null)
114+ public IMediaUploadService? GetService(string serviceName)
162115 {
163- this.CreateServices(tw, twitterConfig);
164-
165- this.SetImageServiceCombo();
166- this.SetImagePageCombo();
116+ var index = this.MediaServices.FindIndex(x => x.Key == serviceName);
117+ return index != -1 ? this.MediaServices[index].Value : null;
118+ }
167119
168- this.SelectImageServiceComboItem(svc, index);
120+ public void InitializeServices(Twitter tw, TwitterConfiguration twitterConfig)
121+ {
122+ this.MediaServices = new KeyValuePair<string, IMediaUploadService>[]
123+ {
124+ new("Twitter", new TwitterPhoto(tw, twitterConfig)),
125+ new("Imgur", new Imgur(twitterConfig)),
126+ new("Mobypicture", new Mobypicture(tw, twitterConfig)),
127+ };
169128 }
170129
171- /// <summary>
172- /// 投稿先サービスを再作成する。
173- /// </summary>
174- public void Reset(Twitter tw, TwitterConfiguration twitterConfig)
130+ public void SelectMediaService(string serviceName, int? index = null)
175131 {
176- this.CreateServices(tw, twitterConfig);
132+ int idx;
133+ if (MyCommon.IsNullOrEmpty(serviceName))
134+ {
135+ // 引数の index は serviceName が空の場合のみ使用する
136+ idx = index ?? 0;
137+ }
138+ else
139+ {
140+ idx = this.MediaServices.FindIndex(x => x.Key == serviceName);
177141
178- this.SetImageServiceCombo();
142+ // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる
143+ // (廃止されたサービスを選択していた場合の対応)
144+ if (idx == -1)
145+ idx = 0;
146+ }
147+
148+ this.SelectedMediaServiceName = this.MediaServices[idx].Key;
179149 }
180150
181151 /// <summary>
@@ -187,198 +157,116 @@ namespace OpenTween
187157 var ext = fl.Extension;
188158 var size = ignoreSize ? (long?)null : fl.Length;
189159
190- if (this.IsUploadable(this.ServiceName, ext, size))
191- return true;
192-
193- foreach (string svc in this.ImageServiceCombo.Items)
194- {
195- if (this.IsUploadable(svc, ext, size))
196- return true;
197- }
198-
199- return false;
160+ return this.GetAvailableServiceNames(ext, size).Any();
200161 }
201162
202- /// <summary>
203- /// 指定された投稿先に投稿可能かどうかを示す値を取得する。
204- /// ファイルサイズの指定がなければ拡張子だけで判定する。
205- /// </summary>
206- private bool IsUploadable(string serviceName, string ext, long? size)
163+ public string[] GetAvailableServiceNames(string extension, long? fileSize)
164+ => this.MediaServices
165+ .Where(x => x.Value.CheckFileExtension(extension) && (fileSize == null || x.Value.CheckFileSize(extension, fileSize.Value)))
166+ .Select(x => x.Key)
167+ .ToArray();
168+
169+ public void AddMediaItemFromImage(Image image)
207170 {
208- if (!MyCommon.IsNullOrEmpty(serviceName))
209- {
210- var imageService = this.pictureService[serviceName];
211- if (imageService.CheckFileExtension(ext))
212- {
213- if (!size.HasValue)
214- return true;
171+ var mediaItem = this.CreateMemoryImageMediaItem(image);
172+ if (mediaItem == null)
173+ return;
215174
216- if (imageService.CheckFileSize(ext, size.Value))
217- return true;
218- }
219- }
220- return false;
175+ this.AddMediaItem(mediaItem);
176+ this.SelectedMediaItemId = mediaItem.Id;
221177 }
222178
223- /// <summary>
224- /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
225- /// </summary>
226- private void BeginSelection(IMediaItem[] items)
179+ public void AddMediaItemFromFilePath(string[] filePathArray)
227180 {
228- if (items == null || items.Length == 0)
229- {
230- this.BeginSelection();
181+ if (filePathArray.Length == 0)
231182 return;
232- }
233-
234- var service = this.SelectedService;
235- if (service == null) return;
236183
237- var count = Math.Min(items.Length, service.MaxMediaCount);
238- if (!this.Visible || count > 1)
239- {
240- // 非表示時または複数のファイル指定は新規選択として扱う
241- this.SetImagePageCombo();
184+ var mediaItems = new IMediaItem[filePathArray.Length];
242185
243- this.BeginSelecting?.Invoke(this, EventArgs.Empty);
186+ // 連番のファイル名を一括でアップロードする場合の利便性のためソートする
187+ var sortedFilePath = filePathArray.OrderBy(x => x);
244188
245- this.Visible = true;
246- }
247- this.Enabled = true;
248-
249- if (count == 1)
250- {
251- this.ImagefilePathText.Text = items[0].Path;
252- this.AlternativeTextBox.Text = items[0].AltText;
253- this.ImageFromSelectedFile(items[0], false);
254- }
255- else
189+ foreach (var (path, index) in sortedFilePath.WithIndex())
256190 {
257- for (var i = 0; i < count; i++)
258- {
259- var index = this.ImagePageCombo.Items.Count - 1;
260- if (index == 0)
261- {
262- this.ImagefilePathText.Text = items[i].Path;
263- this.AlternativeTextBox.Text = items[i].AltText;
264- }
265- this.ImageFromSelectedFile(index, items[i], false);
266- }
267- }
268- }
191+ var mediaItem = this.CreateFileMediaItem(path);
192+ if (mediaItem == null)
193+ continue;
269194
270- /// <summary>
271- /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する(主にD&amp;D用)。
272- /// </summary>
273- public void BeginSelection(string[] fileNames)
274- {
275- if (fileNames == null || fileNames.Length == 0)
276- {
277- this.BeginSelection();
278- return;
195+ mediaItems[index] = mediaItem;
279196 }
280197
281- var items = fileNames.Select(x => this.CreateFileMediaItem(x, false)).OfType<IMediaItem>().ToArray();
282- this.BeginSelection(items);
198+ // 全ての IMediaItem の生成に成功した場合のみ追加する
199+ foreach (var mediaItem in mediaItems)
200+ this.AddMediaItem(mediaItem);
201+
202+ this.SelectedMediaItemId = mediaItems.Last().Id;
283203 }
284204
285- /// <summary>
286- /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
287- /// </summary>
288- public void BeginSelection(Image image)
205+ public void AddMediaItem(IMediaItem item)
289206 {
290- if (image == null)
291- {
292- this.BeginSelection();
293- return;
294- }
295-
296- var items = new[] { this.CreateMemoryImageMediaItem(image, false) }.OfType<IMediaItem>().ToArray();
297- this.BeginSelection(items);
207+ var id = item.Id.ToString();
208+ var thumbnailImage = this.GenerateThumbnailImage(item);
209+ this.ThumbnailList.Add(id, thumbnailImage);
210+ this.MediaItems.Add(item);
298211 }
299212
300- /// <summary>
301- /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
302- /// </summary>
303- public void BeginSelection()
213+ private MemoryImage GenerateThumbnailImage(IMediaItem item)
304214 {
305- if (!this.Visible)
306- {
307- this.BeginSelecting?.Invoke(this, EventArgs.Empty);
215+ using var origImage = this.CreateMediaItemImage(item);
216+ var origSize = origImage.Image.Size;
217+ var thumbSize = this.ThumbnailList.ImageList.ImageSize;
308218
309- this.Visible = true;
310- this.Enabled = true;
219+ using var bitmap = new Bitmap(thumbSize.Width, thumbSize.Height);
311220
312- var media = (SelectedMedia)this.ImagePageCombo.SelectedItem;
313- this.ImageFromSelectedFile(media.Item, true);
314- this.ImagefilePathText.Focus();
221+ // 縦横比を維持したまま thumbSize に収まるサイズに縮小する
222+ using (var g = Graphics.FromImage(bitmap))
223+ {
224+ var scale = Math.Min(
225+ (float)thumbSize.Width / origSize.Width,
226+ (float)thumbSize.Height / origSize.Height
227+ );
228+ var fitSize = new SizeF(origSize.Width * scale, origSize.Height * scale);
229+ var pos = new PointF(
230+ x: (thumbSize.Width - fitSize.Width) / 2.0f,
231+ y: (thumbSize.Height - fitSize.Height) / 2.0f
232+ );
233+ g.DrawImage(origImage.Image, new RectangleF(pos, fitSize));
315234 }
235+
236+ return MemoryImage.CopyFromImage(bitmap);
316237 }
317238
318- /// <summary>
319- /// 選択処理を終了してコントロールを隠す。
320- /// </summary>
321- public void EndSelection()
239+ public void ClearMediaItems()
322240 {
323- if (this.Visible)
324- {
325- this.ImagefilePathText.CausesValidation = false;
241+ this.SelectedMediaItemId = null;
326242
327- this.EndSelecting?.Invoke(this, EventArgs.Empty);
243+ var mediaItems = this.MediaItems.ToList();
244+ this.MediaItems.Clear();
328245
329- this.Visible = false;
330- this.Enabled = false;
331- this.ClearImageSelectedPicture();
246+ foreach (var mediaItem in mediaItems)
247+ mediaItem.Dispose();
332248
333- this.ImagefilePathText.CausesValidation = true;
334- }
249+ var thumbnailImages = this.ThumbnailList.ToList();
250+ this.ThumbnailList.Clear();
251+
252+ foreach (var image in thumbnailImages)
253+ image.Dispose();
335254 }
336255
337- /// <summary>
338- /// 選択された投稿先名と投稿する MediaItem を取得する。MediaItem は不要になったら呼び出し側にて破棄すること。
339- /// </summary>
340- public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [NotNullWhen(true)] out IMediaItem[]? mediaItems)
256+ public IMediaItem[] DetachMediaItems()
341257 {
342- var validItems = this.ImagePageCombo.Items.Cast<SelectedMedia>()
343- .Where(x => x.IsValid).Select(x => x.Item).OfType<IMediaItem>().ToArray();
258+ // ClearMediaItems では MediaItem が破棄されるため、外部で使用する場合はこのメソッドを使用して MediaItems から切り離す
259+ var mediaItems = this.MediaItems.ToArray();
260+ this.MediaItems.Clear();
261+ this.ClearMediaItems();
344262
345- if (validItems.Length > 0 &&
346- this.ImageServiceCombo.SelectedIndex > -1)
347- {
348- var serviceName = this.ServiceName;
349- if (MessageBox.Show(string.Format(Properties.Resources.PostPictureConfirm1, serviceName, validItems.Length),
350- Properties.Resources.PostPictureConfirm2,
351- MessageBoxButtons.OKCancel,
352- MessageBoxIcon.Question,
353- MessageBoxDefaultButton.Button1)
354- == DialogResult.OK)
355- {
356- // 収集した MediaItem が破棄されないように、予め null を代入しておく
357- foreach (SelectedMedia media in this.ImagePageCombo.Items)
358- {
359- if (media != null) media.Item = null;
360- }
361-
362- imageService = serviceName;
363- mediaItems = validItems;
364- this.EndSelection();
365- this.SetImagePageCombo();
366- return true;
367- }
368- }
369- else
370- {
371- MessageBox.Show(Properties.Resources.PostPictureWarn1, Properties.Resources.PostPictureWarn2);
372- }
373-
374- imageService = null;
375- mediaItems = null;
376- return false;
263+ return mediaItems;
377264 }
378265
379- private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image, bool noMsgBox)
266+ private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image)
380267 {
381- if (image == null) return null;
268+ if (image == null)
269+ return null;
382270
383271 MemoryImage? memoryImage = null;
384272 try
@@ -391,15 +279,14 @@ namespace OpenTween
391279 catch
392280 {
393281 memoryImage?.Dispose();
394-
395- if (!noMsgBox) MessageBox.Show("Unable to create MemoryImage.");
396282 return null;
397283 }
398284 }
399285
400- private IMediaItem? CreateFileMediaItem(string path, bool noMsgBox)
286+ private FileMediaItem? CreateFileMediaItem(string path)
401287 {
402- if (MyCommon.IsNullOrEmpty(path)) return null;
288+ if (MyCommon.IsNullOrEmpty(path))
289+ return null;
403290
404291 try
405292 {
@@ -407,428 +294,100 @@ namespace OpenTween
407294 }
408295 catch
409296 {
410- if (!noMsgBox) MessageBox.Show("Invalid file path: " + path);
411297 return null;
412298 }
413299 }
414300
415- private void ValidateNewFileMediaItem(string path, string altText, bool noMsgBox)
301+ private void LoadSelectedMediaItemImage()
416302 {
417- var media = (SelectedMedia)this.ImagePageCombo.SelectedItem;
418- var item = media.Item;
303+ var previousImage = this.selectedMediaItemImage;
419304
420- if (path != media.Path)
305+ if (this.SelectedMediaItem == null)
421306 {
422- this.DisposeMediaItem(media.Item);
423- media.Item = null;
424-
425- item = this.CreateFileMediaItem(path, noMsgBox);
426- }
427-
428- if (item != null)
429- item.AltText = altText;
430-
431- this.ImagefilePathText.Text = path;
432- this.AlternativeTextBox.Text = altText;
433- this.ImageFromSelectedFile(item, noMsgBox);
434- }
435-
436- private void DisposeMediaItem(IMediaItem? item)
437- {
438- var disposableItem = item as IDisposable;
439- disposableItem?.Dispose();
440- }
441-
442- private void FilePickButton_Click(object sender, EventArgs e)
443- {
444- var service = this.SelectedService;
445-
446- if (this.FilePickDialog == null || service == null) return;
447- this.FilePickDialog.Filter = service.SupportedFormatsStrForDialog;
448- this.FilePickDialog.Title = Properties.Resources.PickPictureDialog1;
449- this.FilePickDialog.FileName = "";
450-
451- this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty);
452-
453- try
454- {
455- if (this.FilePickDialog.ShowDialog() == DialogResult.Cancel) return;
456- }
457- finally
458- {
459- this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty);
460- }
461-
462- this.ValidateNewFileMediaItem(this.FilePickDialog.FileName, this.AlternativeTextBox.Text.Trim(), false);
463- }
464-
465- private void ImagefilePathText_Validating(object sender, CancelEventArgs e)
466- {
467- if (this.ImageCancelButton.Focused)
468- {
469- this.ImagefilePathText.CausesValidation = false;
307+ this.SelectedMediaItemImage = null;
308+ previousImage?.Dispose();
470309 return;
471310 }
472311
473- this.ValidateNewFileMediaItem(this.ImagefilePathText.Text.Trim(), this.AlternativeTextBox.Text.Trim(), false);
312+ this.SelectedMediaItemImage = this.CreateMediaItemImage(this.SelectedMediaItem);
313+ previousImage?.Dispose();
474314 }
475315
476- private void ImageFromSelectedFile(IMediaItem? item, bool noMsgBox)
477- => this.ImageFromSelectedFile(-1, item, noMsgBox);
478-
479- private void ImageFromSelectedFile(int index, IMediaItem? item, bool noMsgBox)
316+ private MemoryImage CreateMediaItemImage(IMediaItem media)
480317 {
481- var valid = false;
482-
483318 try
484319 {
485- var imageService = this.SelectedService;
486- if (imageService == null) return;
487-
488- var selectedIndex = this.ImagePageCombo.SelectedIndex;
489- if (index < 0) index = selectedIndex;
490-
491- if (index >= this.ImagePageCombo.Items.Count)
492- throw new ArgumentOutOfRangeException(nameof(index));
493-
494- var isSelectedPage = index == selectedIndex;
495-
496- if (isSelectedPage)
497- this.ClearImageSelectedPicture();
498-
499- if (item == null || MyCommon.IsNullOrEmpty(item.Path)) return;
500-
501- try
502- {
503- var ext = item.Extension;
504- var size = item.Size;
505-
506- if (!imageService.CheckFileExtension(ext))
507- {
508- // 画像以外の形式
509- if (!noMsgBox)
510- {
511- MessageBox.Show(
512- string.Format(Properties.Resources.PostPictureWarn3, this.ServiceName, this.MakeAvailableServiceText(ext, size), ext, item.Name),
513- Properties.Resources.PostPictureWarn4,
514- MessageBoxButtons.OK,
515- MessageBoxIcon.Warning);
516- }
517- return;
518- }
519-
520- if (!imageService.CheckFileSize(ext, size))
521- {
522- // ファイルサイズが大きすぎる
523- if (!noMsgBox)
524- {
525- MessageBox.Show(
526- string.Format(Properties.Resources.PostPictureWarn5, this.ServiceName, this.MakeAvailableServiceText(ext, size), item.Name),
527- Properties.Resources.PostPictureWarn4,
528- MessageBoxButtons.OK,
529- MessageBoxIcon.Warning);
530- }
531- return;
532- }
533-
534- if (item.IsImage)
535- {
536- if (isSelectedPage)
537- this.ImageSelectedPicture.Image = item.CreateImage();
538- this.SetImagePage(index, item, MyCommon.UploadFileType.Picture);
539- }
540- else
541- {
542- this.SetImagePage(index, item, MyCommon.UploadFileType.MultiMedia);
543- }
544-
545- valid = true; // 正常終了
546- }
547- catch (FileNotFoundException)
548- {
549- if (!noMsgBox) MessageBox.Show("File not found.");
550- }
551- catch (Exception)
552- {
553- if (!noMsgBox) MessageBox.Show("The type of this file is not image.");
554- }
320+ return media.CreateImage();
555321 }
556- finally
322+ catch (InvalidImageException)
557323 {
558- if (!valid)
559- {
560- this.ClearImagePage(index);
561- this.DisposeMediaItem(item);
562- }
324+ return MemoryImage.CopyFromImage(Properties.Resources.MultiMediaImage);
563325 }
564326 }
565327
566- private string MakeAvailableServiceText(string ext, long fileSize)
567- {
568- var text = string.Join(", ",
569- this.ImageServiceCombo.Items.Cast<string>()
570- .Where(serviceName =>
571- !MyCommon.IsNullOrEmpty(serviceName) &&
572- this.pictureService[serviceName].CheckFileExtension(ext) &&
573- this.pictureService[serviceName].CheckFileSize(ext, fileSize)));
574-
575- if (MyCommon.IsNullOrEmpty(text))
576- return Properties.Resources.PostPictureWarn6;
577-
578- return text;
579- }
580-
581- private void ClearImageSelectedPicture()
328+ public void SetSelectedMediaAltText(string altText)
582329 {
583- var oldImage = this.ImageSelectedPicture.Image;
584- this.ImageSelectedPicture.Image = null;
585- oldImage?.Dispose();
586-
587- this.ImageSelectedPicture.ShowInitialImage();
588- }
589-
590- private void ImageCancelButton_Click(object sender, EventArgs e)
591- => this.EndSelection();
592-
593- private void ImageSelection_KeyDown(object sender, KeyEventArgs e)
594- {
595- if (e.KeyCode == Keys.Escape)
596- {
597- this.EndSelection();
598- }
599- }
600-
601- private void ImageSelection_KeyPress(object sender, KeyPressEventArgs e)
602- {
603- if (Convert.ToInt32(e.KeyChar) == 0x1B)
604- {
605- this.ImagefilePathText.CausesValidation = false;
606- e.Handled = true;
607- }
608- }
609-
610- private void ImageSelection_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
611- {
612- if (e.KeyCode == Keys.Escape)
613- {
614- this.ImagefilePathText.CausesValidation = false;
615- }
616- }
617-
618- private void SetImageServiceCombo()
619- {
620- using (ControlTransaction.Update(this.ImageServiceCombo))
621- {
622- var svc = "";
623- if (this.ImageServiceCombo.SelectedIndex > -1) svc = this.ImageServiceCombo.Text;
624- this.ImageServiceCombo.Items.Clear();
625-
626- // Add service names to combobox
627- foreach (var key in this.pictureService.Keys)
628- {
629- this.ImageServiceCombo.Items.Add(key);
630- }
330+ var selectedMedia = this.SelectedMediaItem;
331+ if (selectedMedia == null)
332+ return;
631333
632- this.SelectImageServiceComboItem(svc);
633- }
334+ selectedMedia.AltText = altText.Trim();
634335 }
635336
636- private void SelectImageServiceComboItem(string svc, int? index = null)
337+ public MediaSelectorErrorType Validate(out IMediaItem? rejectedMedia)
637338 {
638- int idx;
639- if (MyCommon.IsNullOrEmpty(svc))
640- {
641- idx = index ?? 0;
642- }
643- else
644- {
645- idx = this.ImageServiceCombo.Items.IndexOf(svc);
646-
647- // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる
648- // (廃止されたサービスを選択していた場合の対応)
649- if (idx == -1) idx = 0;
650- }
339+ rejectedMedia = null;
651340
652- try
653- {
654- this.ImageServiceCombo.SelectedIndex = idx;
655- }
656- catch (ArgumentOutOfRangeException)
657- {
658- this.ImageServiceCombo.SelectedIndex = 0;
659- }
341+ if (this.MediaItems.Count == 0)
342+ return MediaSelectorErrorType.MediaItemNotSet;
660343
661- this.UpdateAltTextPanelVisible();
662- }
344+ var uploadService = this.SelectedMediaService;
345+ if (uploadService == null)
346+ return MediaSelectorErrorType.ServiceNotSelected;
663347
664- private void UpdateAltTextPanelVisible()
665- => this.AlternativeTextPanel.Visible = this.SelectedService switch
348+ foreach (var mediaItem in this.MediaItems)
666349 {
667- null => false,
668- var service => service.CanUseAltText,
669- };
670-
671- private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
672- {
673- if (this.Visible)
674- {
675- var imageService = this.SelectedService;
676- if (imageService != null)
350+ var error = this.ValidateMediaItem(uploadService, mediaItem);
351+ if (error != MediaSelectorErrorType.None)
677352 {
678- this.UpdateAltTextPanelVisible();
679-
680- if (this.ImagePageCombo.Items.Count > 0)
681- {
682- // 画像が選択された投稿先に対応しているかをチェックする
683- // TODO: 複数の選択済み画像があるなら、できれば全てを再チェックしたほうがいい
684- if (this.ServiceName == "Twitter")
685- {
686- this.ValidateSelectedImagePage();
687- }
688- else
689- {
690- if (this.ImagePageCombo.Items.Count > 1)
691- {
692- // 複数の選択済み画像のうち、1枚目のみを残す
693- this.SetImagePageCombo((SelectedMedia)this.ImagePageCombo.Items[0]);
694- }
695- else
696- {
697- this.ImagePageCombo.Enabled = false;
698- var valid = false;
699-
700- try
701- {
702- var item = ((SelectedMedia)this.ImagePageCombo.Items[0]).Item;
703- if (item != null)
704- {
705- var ext = item.Extension;
706- if (imageService.CheckFileExtension(ext) &&
707- imageService.CheckFileSize(ext, item.Size))
708- {
709- valid = true;
710- }
711- }
712- }
713- catch
714- {
715- }
716- finally
717- {
718- if (!valid)
719- {
720- this.ClearImageSelectedPicture();
721- this.ClearSelectedImagePage();
722- }
723- }
724- }
725- }
726- }
353+ rejectedMedia = mediaItem;
354+ return error;
727355 }
728356 }
729357
730- this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty);
358+ return MediaSelectorErrorType.None;
731359 }
732360
733- private void SetImagePageCombo(SelectedMedia? media = null)
361+ private MediaSelectorErrorType ValidateMediaItem(IMediaUploadService imageService, IMediaItem item)
734362 {
735- using (ControlTransaction.Update(this.ImagePageCombo))
736- {
737- this.ImagePageCombo.Enabled = false;
363+ var ext = item.Extension;
364+ var size = item.Size;
738365
739- foreach (SelectedMedia oldMedia in this.ImagePageCombo.Items)
740- {
741- if (oldMedia == null || oldMedia == media) continue;
742- this.DisposeMediaItem(oldMedia.Item);
743- }
744- this.ImagePageCombo.Items.Clear();
745-
746- if (media == null)
747- media = new SelectedMedia("1");
366+ if (!imageService.CheckFileExtension(ext))
367+ return MediaSelectorErrorType.UnsupportedFileExtension;
748368
749- this.ImagePageCombo.Items.Add(media);
750- this.ImagefilePathText.Text = media.Path;
751- this.AlternativeTextBox.Text = media.AltText;
369+ if (!imageService.CheckFileSize(ext, size))
370+ return MediaSelectorErrorType.FileSizeExceeded;
752371
753- this.ImagePageCombo.SelectedIndex = 0;
754- }
372+ return MediaSelectorErrorType.None;
755373 }
756374
757- private void AddNewImagePage(int selectedIndex)
375+ public void Dispose()
758376 {
759- var service = this.SelectedService;
760- if (service == null) return;
761-
762- if (selectedIndex < service.MaxMediaCount - 1)
763- {
764- // 投稿先の投稿可能枚数まで選択できるようにする
765- var count = this.ImagePageCombo.Items.Count;
766- if (selectedIndex == count - 1)
767- {
768- count++;
769- this.ImagePageCombo.Items.Add(new SelectedMedia(count.ToString()));
770- this.ImagePageCombo.Enabled = true;
771- }
772- }
773- }
774-
775- private void SetSelectedImagePage(IMediaItem item, MyCommon.UploadFileType type)
776- => this.SetImagePage(-1, item, type);
777-
778- private void SetImagePage(int index, IMediaItem item, MyCommon.UploadFileType type)
779- {
780- var selectedIndex = this.ImagePageCombo.SelectedIndex;
781- if (index < 0) index = selectedIndex;
782-
783- var media = (SelectedMedia)this.ImagePageCombo.Items[index];
784- if (media.Item != item)
785- {
786- this.DisposeMediaItem(media.Item);
787- media.Item = item;
788- }
789- media.Type = type;
790-
791- this.AddNewImagePage(index);
792- }
793-
794- private void ClearSelectedImagePage()
795- => this.ClearImagePage(-1);
796-
797- private void ClearImagePage(int index)
798- {
799- var selectedIndex = this.ImagePageCombo.SelectedIndex;
800- if (index < 0) index = selectedIndex;
801-
802- var media = (SelectedMedia)this.ImagePageCombo.Items[index];
803- this.DisposeMediaItem(media.Item);
804- media.Item = null;
805- media.Type = MyCommon.UploadFileType.Invalid;
806-
807- if (index == selectedIndex)
808- {
809- this.ImagefilePathText.Text = "";
810- this.AlternativeTextBox.Text = "";
811- }
812- }
377+ if (this.IsDisposed)
378+ return;
813379
814- private void ValidateSelectedImagePage()
815- {
816- var idx = this.ImagePageCombo.SelectedIndex;
817- var media = (SelectedMedia)this.ImagePageCombo.Items[idx];
818- this.ImageServiceCombo.Enabled = idx == 0; // idx == 0 以外では投稿先サービスを選べないようにする
819- this.ImagefilePathText.Text = media.Path;
820- this.AlternativeTextBox.Text = media.AltText;
821- this.ImageFromSelectedFile(media.Item, true);
380+ this.IsDisposed = true;
381+ this.ThumbnailList.Dispose();
822382 }
383+ }
823384
824- private void ImagePageCombo_SelectedIndexChanged(object sender, EventArgs e)
825- => this.ValidateSelectedImagePage();
826-
827- private void AlternativeTextBox_Validating(object sender, CancelEventArgs e)
828- {
829- var imageFilePath = this.ImagefilePathText.Text.Trim();
830- var altText = this.AlternativeTextBox.Text.Trim();
831- this.ValidateNewFileMediaItem(imageFilePath, altText, noMsgBox: false);
832- }
385+ public enum MediaSelectorErrorType
386+ {
387+ None,
388+ MediaItemNotSet,
389+ ServiceNotSelected,
390+ UnsupportedFileExtension,
391+ FileSizeExceeded,
833392 }
834393 }
--- a/OpenTween/MediaSelector.Designer.cs
+++ b/OpenTween/MediaSelectorPanel.Designer.cs
@@ -1,25 +1,12 @@
11 namespace OpenTween
22 {
3- partial class MediaSelector
3+ partial class MediaSelectorPanel
44 {
55 /// <summary>
66 /// 必要なデザイナー変数です。
77 /// </summary>
88 private System.ComponentModel.IContainer components = null;
99
10- /// <summary>
11- /// 使用中のリソースをすべてクリーンアップします。
12- /// </summary>
13- /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
14- protected override void Dispose(bool disposing)
15- {
16- if (disposing && (components != null))
17- {
18- components.Dispose();
19- }
20- base.Dispose(disposing);
21- }
22-
2310 #region コンポーネント デザイナーで生成されたコード
2411
2512 /// <summary>
@@ -28,64 +15,26 @@
2815 /// </summary>
2916 private void InitializeComponent()
3017 {
31- System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MediaSelector));
32- this.ImagePathPanel = new System.Windows.Forms.Panel();
33- this.ImagefilePathText = new System.Windows.Forms.TextBox();
34- this.ImagePageCombo = new System.Windows.Forms.ComboBox();
35- this.FilePickButton = new System.Windows.Forms.Button();
18+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MediaSelectorPanel));
3619 this.Label2 = new System.Windows.Forms.Label();
3720 this.ImageServiceCombo = new System.Windows.Forms.ComboBox();
3821 this.ImageCancelButton = new System.Windows.Forms.Button();
3922 this.AlternativeTextPanel = new System.Windows.Forms.Panel();
4023 this.AlternativeTextBox = new System.Windows.Forms.TextBox();
4124 this.AlternativeTextLabel = new System.Windows.Forms.Label();
25+ this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
26+ this.MediaListView = new System.Windows.Forms.ListView();
27+ this.panel1 = new System.Windows.Forms.Panel();
28+ this.AddMediaButton = new System.Windows.Forms.Button();
29+ this.ServiceSelectPanel = new System.Windows.Forms.Panel();
4230 this.ImageSelectedPicture = new OpenTween.OTPictureBox();
43- this.ImagePathPanel.SuspendLayout();
4431 this.AlternativeTextPanel.SuspendLayout();
32+ this.tableLayoutPanel1.SuspendLayout();
33+ this.panel1.SuspendLayout();
34+ this.ServiceSelectPanel.SuspendLayout();
4535 ((System.ComponentModel.ISupportInitialize)(this.ImageSelectedPicture)).BeginInit();
4636 this.SuspendLayout();
4737 //
48- // ImagePathPanel
49- //
50- this.ImagePathPanel.Controls.Add(this.ImagefilePathText);
51- this.ImagePathPanel.Controls.Add(this.ImagePageCombo);
52- this.ImagePathPanel.Controls.Add(this.FilePickButton);
53- this.ImagePathPanel.Controls.Add(this.Label2);
54- this.ImagePathPanel.Controls.Add(this.ImageServiceCombo);
55- this.ImagePathPanel.Controls.Add(this.ImageCancelButton);
56- resources.ApplyResources(this.ImagePathPanel, "ImagePathPanel");
57- this.ImagePathPanel.Name = "ImagePathPanel";
58- //
59- // ImagefilePathText
60- //
61- resources.ApplyResources(this.ImagefilePathText, "ImagefilePathText");
62- this.ImagefilePathText.Name = "ImagefilePathText";
63- this.ImagefilePathText.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ImageSelection_KeyDown);
64- this.ImagefilePathText.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.ImageSelection_KeyPress);
65- this.ImagefilePathText.PreviewKeyDown += new System.Windows.Forms.PreviewKeyDownEventHandler(this.ImageSelection_PreviewKeyDown);
66- this.ImagefilePathText.Validating += new System.ComponentModel.CancelEventHandler(this.ImagefilePathText_Validating);
67- //
68- // ImagePageCombo
69- //
70- resources.ApplyResources(this.ImagePageCombo, "ImagePageCombo");
71- this.ImagePageCombo.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
72- this.ImagePageCombo.FormattingEnabled = true;
73- this.ImagePageCombo.Name = "ImagePageCombo";
74- this.ImagePageCombo.SelectedIndexChanged += new System.EventHandler(this.ImagePageCombo_SelectedIndexChanged);
75- this.ImagePageCombo.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ImageSelection_KeyDown);
76- this.ImagePageCombo.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.ImageSelection_KeyPress);
77- this.ImagePageCombo.PreviewKeyDown += new System.Windows.Forms.PreviewKeyDownEventHandler(this.ImageSelection_PreviewKeyDown);
78- //
79- // FilePickButton
80- //
81- resources.ApplyResources(this.FilePickButton, "FilePickButton");
82- this.FilePickButton.Name = "FilePickButton";
83- this.FilePickButton.UseVisualStyleBackColor = true;
84- this.FilePickButton.Click += new System.EventHandler(this.FilePickButton_Click);
85- this.FilePickButton.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ImageSelection_KeyDown);
86- this.FilePickButton.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.ImageSelection_KeyPress);
87- this.FilePickButton.PreviewKeyDown += new System.Windows.Forms.PreviewKeyDownEventHandler(this.ImageSelection_PreviewKeyDown);
88- //
8938 // Label2
9039 //
9140 resources.ApplyResources(this.Label2, "Label2");
@@ -100,9 +49,6 @@
10049 resources.GetString("ImageServiceCombo.Items")});
10150 this.ImageServiceCombo.Name = "ImageServiceCombo";
10251 this.ImageServiceCombo.SelectedIndexChanged += new System.EventHandler(this.ImageServiceCombo_SelectedIndexChanged);
103- this.ImageServiceCombo.KeyDown += new System.Windows.Forms.KeyEventHandler(this.ImageSelection_KeyDown);
104- this.ImageServiceCombo.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.ImageSelection_KeyPress);
105- this.ImageServiceCombo.PreviewKeyDown += new System.Windows.Forms.PreviewKeyDownEventHandler(this.ImageSelection_PreviewKeyDown);
10652 //
10753 // ImageCancelButton
10854 //
@@ -122,31 +68,73 @@
12268 //
12369 resources.ApplyResources(this.AlternativeTextBox, "AlternativeTextBox");
12470 this.AlternativeTextBox.Name = "AlternativeTextBox";
125- this.AlternativeTextBox.Validating += new System.ComponentModel.CancelEventHandler(this.AlternativeTextBox_Validating);
71+ this.AlternativeTextBox.Validated += new System.EventHandler(this.AlternativeTextBox_Validated);
12672 //
12773 // AlternativeTextLabel
12874 //
12975 resources.ApplyResources(this.AlternativeTextLabel, "AlternativeTextLabel");
13076 this.AlternativeTextLabel.Name = "AlternativeTextLabel";
13177 //
78+ // tableLayoutPanel1
79+ //
80+ resources.ApplyResources(this.tableLayoutPanel1, "tableLayoutPanel1");
81+ this.tableLayoutPanel1.Controls.Add(this.MediaListView, 0, 0);
82+ this.tableLayoutPanel1.Controls.Add(this.panel1, 1, 0);
83+ this.tableLayoutPanel1.Name = "tableLayoutPanel1";
84+ //
85+ // MediaListView
86+ //
87+ resources.ApplyResources(this.MediaListView, "MediaListView");
88+ this.MediaListView.HeaderStyle = System.Windows.Forms.ColumnHeaderStyle.None;
89+ this.MediaListView.HideSelection = false;
90+ this.MediaListView.MultiSelect = false;
91+ this.MediaListView.Name = "MediaListView";
92+ this.MediaListView.ShowGroups = false;
93+ this.MediaListView.UseCompatibleStateImageBehavior = false;
94+ this.MediaListView.SelectedIndexChanged += new System.EventHandler(this.MediaListView_SelectedIndexChanged);
95+ //
96+ // panel1
97+ //
98+ this.panel1.Controls.Add(this.ImageCancelButton);
99+ this.panel1.Controls.Add(this.AddMediaButton);
100+ this.panel1.Controls.Add(this.ServiceSelectPanel);
101+ resources.ApplyResources(this.panel1, "panel1");
102+ this.panel1.Name = "panel1";
103+ //
104+ // AddMediaButton
105+ //
106+ resources.ApplyResources(this.AddMediaButton, "AddMediaButton");
107+ this.AddMediaButton.Name = "AddMediaButton";
108+ this.AddMediaButton.UseVisualStyleBackColor = true;
109+ this.AddMediaButton.Click += new System.EventHandler(this.AddMediaButton_Click);
110+ //
111+ // ServiceSelectPanel
112+ //
113+ resources.ApplyResources(this.ServiceSelectPanel, "ServiceSelectPanel");
114+ this.ServiceSelectPanel.Controls.Add(this.ImageServiceCombo);
115+ this.ServiceSelectPanel.Controls.Add(this.Label2);
116+ this.ServiceSelectPanel.Name = "ServiceSelectPanel";
117+ //
132118 // ImageSelectedPicture
133119 //
134120 resources.ApplyResources(this.ImageSelectedPicture, "ImageSelectedPicture");
135121 this.ImageSelectedPicture.Name = "ImageSelectedPicture";
136122 this.ImageSelectedPicture.TabStop = false;
137123 //
138- // MediaSelector
124+ // MediaSelectorPanel
139125 //
140126 resources.ApplyResources(this, "$this");
141127 this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
142128 this.Controls.Add(this.ImageSelectedPicture);
143129 this.Controls.Add(this.AlternativeTextPanel);
144- this.Controls.Add(this.ImagePathPanel);
145- this.Name = "MediaSelector";
146- this.ImagePathPanel.ResumeLayout(false);
147- this.ImagePathPanel.PerformLayout();
130+ this.Controls.Add(this.tableLayoutPanel1);
131+ this.Name = "MediaSelectorPanel";
148132 this.AlternativeTextPanel.ResumeLayout(false);
149133 this.AlternativeTextPanel.PerformLayout();
134+ this.tableLayoutPanel1.ResumeLayout(false);
135+ this.panel1.ResumeLayout(false);
136+ this.panel1.PerformLayout();
137+ this.ServiceSelectPanel.ResumeLayout(false);
150138 ((System.ComponentModel.ISupportInitialize)(this.ImageSelectedPicture)).EndInit();
151139 this.ResumeLayout(false);
152140 this.PerformLayout();
@@ -156,15 +144,16 @@
156144 #endregion
157145
158146 internal OTPictureBox ImageSelectedPicture;
159- internal System.Windows.Forms.Panel ImagePathPanel;
160- internal System.Windows.Forms.TextBox ImagefilePathText;
161- internal System.Windows.Forms.ComboBox ImagePageCombo;
162- internal System.Windows.Forms.Button FilePickButton;
163147 internal System.Windows.Forms.Label Label2;
164148 internal System.Windows.Forms.ComboBox ImageServiceCombo;
165149 internal System.Windows.Forms.Button ImageCancelButton;
166150 internal System.Windows.Forms.Panel AlternativeTextPanel;
167151 internal System.Windows.Forms.TextBox AlternativeTextBox;
168152 internal System.Windows.Forms.Label AlternativeTextLabel;
153+ private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
154+ private System.Windows.Forms.ListView MediaListView;
155+ private System.Windows.Forms.Panel panel1;
156+ private System.Windows.Forms.Button AddMediaButton;
157+ private System.Windows.Forms.Panel ServiceSelectPanel;
169158 }
170159 }
--- /dev/null
+++ b/OpenTween/MediaSelectorPanel.cs
@@ -0,0 +1,299 @@
1+// OpenTween - Client of Twitter
2+// Copyright (c) 2014 spx (@5px)
3+// Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
4+// All rights reserved.
5+//
6+// This file is part of OpenTween.
7+//
8+// This program is free software; you can redistribute it and/or modify it
9+// under the terms of the GNU General Public License as published by the Free
10+// Software Foundation; either version 3 of the License, or (at your option)
11+// any later version.
12+//
13+// This program is distributed in the hope that it will be useful, but
14+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
16+// for more details.
17+//
18+// You should have received a copy of the GNU General Public License along
19+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
20+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
21+// Boston, MA 02110-1301, USA.
22+
23+#nullable enable
24+
25+using System;
26+using System.Collections.Generic;
27+using System.ComponentModel;
28+using System.Data;
29+using System.Diagnostics.CodeAnalysis;
30+using System.Linq;
31+using System.Text;
32+using System.Threading.Tasks;
33+using System.Windows.Forms;
34+
35+namespace OpenTween
36+{
37+ public partial class MediaSelectorPanel : UserControl
38+ {
39+ public event EventHandler<EventArgs>? BeginSelecting;
40+
41+ public event EventHandler<EventArgs>? EndSelecting;
42+
43+ public event EventHandler<EventArgs>? FilePickDialogOpening;
44+
45+ public event EventHandler<EventArgs>? FilePickDialogClosed;
46+
47+ public event EventHandler<EventArgs>? SelectedServiceChanged;
48+
49+ [Browsable(false)]
50+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
51+ public MediaSelector Model { get; } = new();
52+
53+ [Browsable(false)]
54+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
55+ public OpenFileDialog? FilePickDialog { get; set; }
56+
57+ public MediaSelectorPanel()
58+ {
59+ this.InitializeComponent();
60+
61+ this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage;
62+
63+ this.MediaListView.LargeImageList = this.Model.ThumbnailList.ImageList;
64+
65+ var thumbnailWidth = 75 * this.DeviceDpi / 96;
66+ this.Model.ThumbnailList.ImageList.ColorDepth = ColorDepth.Depth24Bit;
67+ this.Model.ThumbnailList.ImageList.ImageSize = new(thumbnailWidth, thumbnailWidth);
68+
69+ this.Model.PropertyChanged +=
70+ (s, e) => this.TryInvoke(() => this.Model_PropertyChanged(s, e));
71+ this.Model.MediaItems.ListChanged +=
72+ (s, e) => this.TryInvoke(() => this.Model_MediaItems_ListChanged(s, e));
73+
74+ this.UpdateSelectedMedia();
75+ this.UpdateAltTextPanelVisible();
76+ }
77+
78+ /// <summary>
79+ /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。
80+ /// </summary>
81+ public void BeginSelection()
82+ {
83+ this.BeginSelecting?.Invoke(this, EventArgs.Empty);
84+ this.Enabled = true;
85+ this.Visible = true;
86+ }
87+
88+ /// <summary>
89+ /// 選択処理を終了してコントロールを隠す。
90+ /// </summary>
91+ public void EndSelection()
92+ {
93+ this.EndSelecting?.Invoke(this, EventArgs.Empty);
94+ this.Visible = false;
95+ this.Enabled = false;
96+ this.Model.ClearMediaItems();
97+ }
98+
99+ /// <summary>
100+ /// 選択された投稿先名と投稿する MediaItem を取得する。MediaItem は不要になったら呼び出し側にて破棄すること。
101+ /// </summary>
102+ public bool TryGetSelectedMedia([NotNullWhen(true)] out string? imageService, [NotNullWhen(true)] out IMediaItem[]? mediaItems)
103+ {
104+ var selectedServiceName = this.Model.SelectedMediaServiceName;
105+
106+ var error = this.Model.Validate(out var rejectedMedia);
107+ if (error != MediaSelectorErrorType.None)
108+ {
109+ var message = error switch
110+ {
111+ MediaSelectorErrorType.MediaItemNotSet
112+ => Properties.Resources.PostPictureWarn1,
113+ MediaSelectorErrorType.ServiceNotSelected
114+ => Properties.Resources.PostPictureWarn1,
115+ MediaSelectorErrorType.UnsupportedFileExtension
116+ => string.Format(
117+ Properties.Resources.PostPictureWarn3,
118+ selectedServiceName,
119+ this.MakeAvailableServiceText(rejectedMedia!),
120+ rejectedMedia!.Extension,
121+ rejectedMedia!.Name
122+ ),
123+ MediaSelectorErrorType.FileSizeExceeded
124+ => string.Format(
125+ Properties.Resources.PostPictureWarn5,
126+ selectedServiceName,
127+ this.MakeAvailableServiceText(rejectedMedia!),
128+ rejectedMedia!.Name
129+ ),
130+ _ => throw new NotImplementedException(),
131+ };
132+
133+ MessageBox.Show(
134+ message,
135+ Properties.Resources.PostPictureWarn2,
136+ MessageBoxButtons.OK,
137+ MessageBoxIcon.Warning
138+ );
139+
140+ imageService = null;
141+ mediaItems = null;
142+ return false;
143+ }
144+
145+ imageService = selectedServiceName;
146+ mediaItems = this.Model.DetachMediaItems();
147+ return true;
148+ }
149+
150+ private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
151+ {
152+ switch (e.PropertyName)
153+ {
154+ case nameof(MediaSelector.MediaServices):
155+ this.UpdateImageServiceComboItems();
156+ break;
157+ case nameof(MediaSelector.SelectedMediaServiceName):
158+ this.UpdateImageServiceComboSelection();
159+ this.UpdateAltTextPanelVisible();
160+ this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty);
161+ break;
162+ case nameof(MediaSelector.SelectedMediaItemId):
163+ this.UpdateSelectedMedia();
164+ break;
165+ case nameof(MediaSelector.SelectedMediaItemImage):
166+ this.UpdateSelectedMediaImage();
167+ break;
168+ default:
169+ break;
170+ }
171+ }
172+
173+ private void Model_MediaItems_ListChanged(object sender, ListChangedEventArgs e)
174+ {
175+ void AddMediaListViewItem(IMediaItem media, int index)
176+ => this.MediaListView.Items.Insert(index, media.Name, media.Id.ToString());
177+
178+ switch (e.ListChangedType)
179+ {
180+ case ListChangedType.ItemAdded:
181+ var addedMedia = this.Model.MediaItems[e.NewIndex];
182+ AddMediaListViewItem(addedMedia, e.NewIndex);
183+ break;
184+ case ListChangedType.Reset:
185+ this.MediaListView.Items.Clear();
186+ foreach (var (media, index) in this.Model.MediaItems.WithIndex())
187+ AddMediaListViewItem(media, index);
188+ break;
189+ default:
190+ throw new NotImplementedException();
191+ }
192+ }
193+
194+ private void UpdateImageServiceComboItems()
195+ {
196+ using (ControlTransaction.Update(this.ImageServiceCombo))
197+ {
198+ this.ImageServiceCombo.Items.Clear();
199+
200+ // Add service names to combobox
201+ var serviceNames = this.Model.MediaServices.Select(x => x.Key).ToArray();
202+ this.ImageServiceCombo.Items.AddRange(serviceNames);
203+
204+ this.UpdateImageServiceComboSelection();
205+ }
206+ }
207+
208+ private void UpdateImageServiceComboSelection()
209+ => this.ImageServiceCombo.SelectedIndex = this.Model.SelectedMediaServiceIndex;
210+
211+ private void AddMediaButton_Click(object sender, EventArgs e)
212+ {
213+ var service = this.Model.SelectedMediaService;
214+
215+ if (this.FilePickDialog == null || service == null) return;
216+ this.FilePickDialog.Filter = service.SupportedFormatsStrForDialog;
217+ this.FilePickDialog.Title = Properties.Resources.PickPictureDialog1;
218+ this.FilePickDialog.FileName = "";
219+
220+ this.FilePickDialogOpening?.Invoke(this, EventArgs.Empty);
221+
222+ try
223+ {
224+ if (this.FilePickDialog.ShowDialog() == DialogResult.Cancel) return;
225+ }
226+ finally
227+ {
228+ this.FilePickDialogClosed?.Invoke(this, EventArgs.Empty);
229+ }
230+
231+ this.Model.AddMediaItemFromFilePath(this.FilePickDialog.FileNames);
232+ }
233+
234+ private string MakeAvailableServiceText(IMediaItem media)
235+ {
236+ var ext = media.Extension;
237+ var fileSize = media.Size;
238+
239+ var availableServiceNames = this.Model.GetAvailableServiceNames(ext, fileSize);
240+ if (availableServiceNames.Length == 0)
241+ return Properties.Resources.PostPictureWarn6;
242+
243+ return string.Join(", ", availableServiceNames);
244+ }
245+
246+ private void ImageCancelButton_Click(object sender, EventArgs e)
247+ => this.EndSelection();
248+
249+ private void UpdateAltTextPanelVisible()
250+ => this.AlternativeTextPanel.Visible = this.Model.CanUseAltText;
251+
252+ private void UpdateSelectedMedia()
253+ {
254+ using (ControlTransaction.Update(this))
255+ {
256+ var selectedMedia = this.Model.SelectedMediaItem;
257+ if (selectedMedia == null)
258+ {
259+ this.AlternativeTextBox.Text = "";
260+ this.AlternativeTextPanel.Enabled = false;
261+ }
262+ else
263+ {
264+ this.AlternativeTextBox.Text = selectedMedia.AltText;
265+ this.AlternativeTextPanel.Enabled = true;
266+ }
267+ }
268+ }
269+
270+ private void UpdateSelectedMediaImage()
271+ => this.ImageSelectedPicture.Image = this.Model.SelectedMediaItemImage;
272+
273+ private void ImageServiceCombo_SelectedIndexChanged(object sender, EventArgs e)
274+ => this.Model.SelectedMediaServiceName = this.ImageServiceCombo.Text;
275+
276+ private void MediaListView_SelectedIndexChanged(object sender, EventArgs e)
277+ {
278+ var indices = this.MediaListView.SelectedIndices;
279+ if (indices.Count == 0)
280+ return;
281+
282+ this.Model.SelectedMediaItemIndex = indices[0];
283+ }
284+
285+ private void AlternativeTextBox_Validated(object sender, EventArgs e)
286+ => this.Model.SetSelectedMediaAltText(this.AlternativeTextBox.Text);
287+
288+ protected override void Dispose(bool disposing)
289+ {
290+ if (disposing)
291+ {
292+ this.components?.Dispose();
293+ this.Model.Dispose();
294+ }
295+
296+ base.Dispose(disposing);
297+ }
298+ }
299+}
--- a/OpenTween/MediaSelector.resx
+++ b/OpenTween/MediaSelectorPanel.resx
@@ -10,8 +10,12 @@
1010 <data name="$this.AutoScaleDimensions" type="System.Drawing.SizeF, System.Drawing"><value>96, 96</value></data>
1111 <metadata name="$this.Localizable" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"><value>True</value></metadata>
1212 <data name="$this.Size" type="System.Drawing.Size, System.Drawing"><value>608, 280</value></data>
13- <data name="&gt;&gt;$this.Name"><value>MediaSelector</value></data>
13+ <data name="&gt;&gt;$this.Name"><value>MediaSelectorPanel</value></data>
1414 <data name="&gt;&gt;$this.Type"><value>System.Windows.Forms.UserControl, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
15+ <data name="&gt;&gt;AddMediaButton.Name"><value>AddMediaButton</value></data>
16+ <data name="&gt;&gt;AddMediaButton.Parent"><value>panel1</value></data>
17+ <data name="&gt;&gt;AddMediaButton.Type"><value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
18+ <data name="&gt;&gt;AddMediaButton.ZOrder"><value>1</value></data>
1519 <data name="&gt;&gt;AlternativeTextBox.Name"><value>AlternativeTextBox</value></data>
1620 <data name="&gt;&gt;AlternativeTextBox.Parent"><value>AlternativeTextPanel</value></data>
1721 <data name="&gt;&gt;AlternativeTextBox.Type"><value>System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
@@ -24,96 +28,107 @@
2428 <data name="&gt;&gt;AlternativeTextPanel.Parent"><value>$this</value></data>
2529 <data name="&gt;&gt;AlternativeTextPanel.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
2630 <data name="&gt;&gt;AlternativeTextPanel.ZOrder"><value>1</value></data>
27- <data name="&gt;&gt;FilePickButton.Name"><value>FilePickButton</value></data>
28- <data name="&gt;&gt;FilePickButton.Parent"><value>ImagePathPanel</value></data>
29- <data name="&gt;&gt;FilePickButton.Type"><value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
30- <data name="&gt;&gt;FilePickButton.ZOrder"><value>2</value></data>
3131 <data name="&gt;&gt;ImageCancelButton.Name"><value>ImageCancelButton</value></data>
32- <data name="&gt;&gt;ImageCancelButton.Parent"><value>ImagePathPanel</value></data>
32+ <data name="&gt;&gt;ImageCancelButton.Parent"><value>panel1</value></data>
3333 <data name="&gt;&gt;ImageCancelButton.Type"><value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
34- <data name="&gt;&gt;ImageCancelButton.ZOrder"><value>5</value></data>
35- <data name="&gt;&gt;ImagefilePathText.Name"><value>ImagefilePathText</value></data>
36- <data name="&gt;&gt;ImagefilePathText.Parent"><value>ImagePathPanel</value></data>
37- <data name="&gt;&gt;ImagefilePathText.Type"><value>System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
38- <data name="&gt;&gt;ImagefilePathText.ZOrder"><value>0</value></data>
39- <data name="&gt;&gt;ImagePageCombo.Name"><value>ImagePageCombo</value></data>
40- <data name="&gt;&gt;ImagePageCombo.Parent"><value>ImagePathPanel</value></data>
41- <data name="&gt;&gt;ImagePageCombo.Type"><value>System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
42- <data name="&gt;&gt;ImagePageCombo.ZOrder"><value>1</value></data>
43- <data name="&gt;&gt;ImagePathPanel.Name"><value>ImagePathPanel</value></data>
44- <data name="&gt;&gt;ImagePathPanel.Parent"><value>$this</value></data>
45- <data name="&gt;&gt;ImagePathPanel.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
46- <data name="&gt;&gt;ImagePathPanel.ZOrder"><value>2</value></data>
34+ <data name="&gt;&gt;ImageCancelButton.ZOrder"><value>0</value></data>
4735 <data name="&gt;&gt;ImageSelectedPicture.Name"><value>ImageSelectedPicture</value></data>
4836 <data name="&gt;&gt;ImageSelectedPicture.Parent"><value>$this</value></data>
49- <data name="&gt;&gt;ImageSelectedPicture.Type"><value>OpenTween.OTPictureBox, OpenTween, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null</value></data>
37+ <data name="&gt;&gt;ImageSelectedPicture.Type"><value>OpenTween.OTPictureBox, OpenTween, Version=3.1.0.1, Culture=neutral, PublicKeyToken=null</value></data>
5038 <data name="&gt;&gt;ImageSelectedPicture.ZOrder"><value>0</value></data>
5139 <data name="&gt;&gt;ImageServiceCombo.Name"><value>ImageServiceCombo</value></data>
52- <data name="&gt;&gt;ImageServiceCombo.Parent"><value>ImagePathPanel</value></data>
40+ <data name="&gt;&gt;ImageServiceCombo.Parent"><value>ServiceSelectPanel</value></data>
5341 <data name="&gt;&gt;ImageServiceCombo.Type"><value>System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
54- <data name="&gt;&gt;ImageServiceCombo.ZOrder"><value>4</value></data>
42+ <data name="&gt;&gt;ImageServiceCombo.ZOrder"><value>0</value></data>
5543 <data name="&gt;&gt;Label2.Name"><value>Label2</value></data>
56- <data name="&gt;&gt;Label2.Parent"><value>ImagePathPanel</value></data>
44+ <data name="&gt;&gt;Label2.Parent"><value>ServiceSelectPanel</value></data>
5745 <data name="&gt;&gt;Label2.Type"><value>System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
58- <data name="&gt;&gt;Label2.ZOrder"><value>3</value></data>
46+ <data name="&gt;&gt;Label2.ZOrder"><value>1</value></data>
47+ <data name="&gt;&gt;MediaListView.Name"><value>MediaListView</value></data>
48+ <data name="&gt;&gt;MediaListView.Parent"><value>tableLayoutPanel1</value></data>
49+ <data name="&gt;&gt;MediaListView.Type"><value>System.Windows.Forms.ListView, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
50+ <data name="&gt;&gt;MediaListView.ZOrder"><value>0</value></data>
51+ <data name="&gt;&gt;panel1.Name"><value>panel1</value></data>
52+ <data name="&gt;&gt;panel1.Parent"><value>tableLayoutPanel1</value></data>
53+ <data name="&gt;&gt;panel1.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
54+ <data name="&gt;&gt;panel1.ZOrder"><value>1</value></data>
55+ <data name="&gt;&gt;ServiceSelectPanel.Name"><value>ServiceSelectPanel</value></data>
56+ <data name="&gt;&gt;ServiceSelectPanel.Parent"><value>panel1</value></data>
57+ <data name="&gt;&gt;ServiceSelectPanel.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
58+ <data name="&gt;&gt;ServiceSelectPanel.ZOrder"><value>2</value></data>
59+ <data name="&gt;&gt;tableLayoutPanel1.Name"><value>tableLayoutPanel1</value></data>
60+ <data name="&gt;&gt;tableLayoutPanel1.Parent"><value>$this</value></data>
61+ <data name="&gt;&gt;tableLayoutPanel1.Type"><value>System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
62+ <data name="&gt;&gt;tableLayoutPanel1.ZOrder"><value>2</value></data>
63+ <data name="AddMediaButton.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms"><value>Top, Left, Right</value></data>
64+ <data name="AddMediaButton.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data>
65+ <data name="AddMediaButton.Location" type="System.Drawing.Point, System.Drawing"><value>0, 23</value></data>
66+ <data name="AddMediaButton.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms"><value>0, 3, 0, 0</value></data>
67+ <data name="AddMediaButton.Size" type="System.Drawing.Size, System.Drawing"><value>167, 23</value></data>
68+ <data name="AddMediaButton.TabIndex" type="System.Int32, mscorlib"><value>1</value></data>
69+ <data name="AddMediaButton.Text"><value>画像を追加...</value></data>
5970 <data name="AlternativeTextBox.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms"><value>Top, Left, Right</value></data>
6071 <data name="AlternativeTextBox.Location" type="System.Drawing.Point, System.Drawing"><value>109, 3</value></data>
61- <data name="AlternativeTextBox.Size" type="System.Drawing.Size, System.Drawing"><value>496, 19</value></data>
72+ <data name="AlternativeTextBox.Multiline" type="System.Boolean, mscorlib"><value>True</value></data>
73+ <data name="AlternativeTextBox.ScrollBars" type="System.Windows.Forms.ScrollBars, System.Windows.Forms"><value>Vertical</value></data>
74+ <data name="AlternativeTextBox.Size" type="System.Drawing.Size, System.Drawing"><value>496, 40</value></data>
6275 <data name="AlternativeTextBox.TabIndex" type="System.Int32, mscorlib"><value>1</value></data>
6376 <data name="AlternativeTextLabel.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms"><value>Top, Bottom, Left</value></data>
6477 <data name="AlternativeTextLabel.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data>
6578 <data name="AlternativeTextLabel.Location" type="System.Drawing.Point, System.Drawing"><value>3, 3</value></data>
66- <data name="AlternativeTextLabel.Size" type="System.Drawing.Size, System.Drawing"><value>100, 19</value></data>
79+ <data name="AlternativeTextLabel.Size" type="System.Drawing.Size, System.Drawing"><value>100, 40</value></data>
6780 <data name="AlternativeTextLabel.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
6881 <data name="AlternativeTextLabel.Text"><value>代替テキスト(&amp;A):</value></data>
6982 <data name="AlternativeTextLabel.TextAlign" type="System.Drawing.ContentAlignment, System.Drawing"><value>MiddleRight</value></data>
7083 <data name="AlternativeTextPanel.AutoSize" type="System.Boolean, mscorlib"><value>True</value></data>
7184 <data name="AlternativeTextPanel.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Bottom</value></data>
72- <data name="AlternativeTextPanel.Location" type="System.Drawing.Point, System.Drawing"><value>0, 227</value></data>
73- <data name="AlternativeTextPanel.Size" type="System.Drawing.Size, System.Drawing"><value>608, 25</value></data>
74- <data name="AlternativeTextPanel.TabIndex" type="System.Int32, mscorlib"><value>2</value></data>
75- <data name="AlternativeTextPanel.Visible" type="System.Boolean, mscorlib"><value>False</value></data>
76- <data name="FilePickButton.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Right</value></data>
77- <data name="FilePickButton.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data>
78- <data name="FilePickButton.Location" type="System.Drawing.Point, System.Drawing"><value>358, 3</value></data>
79- <data name="FilePickButton.Size" type="System.Drawing.Size, System.Drawing"><value>23, 22</value></data>
80- <data name="FilePickButton.TabIndex" type="System.Int32, mscorlib"><value>2</value></data>
81- <data name="FilePickButton.Text"><value>...</value></data>
82- <data name="ImageCancelButton.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Right</value></data>
85+ <data name="AlternativeTextPanel.Location" type="System.Drawing.Point, System.Drawing"><value>0, 128</value></data>
86+ <data name="AlternativeTextPanel.Size" type="System.Drawing.Size, System.Drawing"><value>608, 46</value></data>
87+ <data name="AlternativeTextPanel.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
88+ <data name="ImageCancelButton.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms"><value>Top, Left, Right</value></data>
8389 <data name="ImageCancelButton.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data>
84- <data name="ImageCancelButton.Location" type="System.Drawing.Point, System.Drawing"><value>545, 3</value></data>
85- <data name="ImageCancelButton.Size" type="System.Drawing.Size, System.Drawing"><value>60, 22</value></data>
86- <data name="ImageCancelButton.TabIndex" type="System.Int32, mscorlib"><value>5</value></data>
87- <data name="ImageCancelButton.Text"><value>Cancel</value></data>
88- <data name="ImagefilePathText.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Fill</value></data>
89- <data name="ImagefilePathText.Location" type="System.Drawing.Point, System.Drawing"><value>61, 3</value></data>
90- <data name="ImagefilePathText.Size" type="System.Drawing.Size, System.Drawing"><value>297, 19</value></data>
91- <data name="ImagefilePathText.TabIndex" type="System.Int32, mscorlib"><value>1</value></data>
92- <data name="ImagePageCombo.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Left</value></data>
93- <data name="ImagePageCombo.Location" type="System.Drawing.Point, System.Drawing"><value>3, 3</value></data>
94- <data name="ImagePageCombo.Size" type="System.Drawing.Size, System.Drawing"><value>58, 20</value></data>
95- <data name="ImagePageCombo.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
96- <data name="ImagePathPanel.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Bottom</value></data>
97- <data name="ImagePathPanel.Location" type="System.Drawing.Point, System.Drawing"><value>0, 252</value></data>
98- <data name="ImagePathPanel.Padding" type="System.Windows.Forms.Padding, System.Windows.Forms"><value>3, 3, 3, 3</value></data>
99- <data name="ImagePathPanel.Size" type="System.Drawing.Size, System.Drawing"><value>608, 28</value></data>
100- <data name="ImagePathPanel.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
90+ <data name="ImageCancelButton.Location" type="System.Drawing.Point, System.Drawing"><value>0, 49</value></data>
91+ <data name="ImageCancelButton.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms"><value>0, 3, 0, 0</value></data>
92+ <data name="ImageCancelButton.Size" type="System.Drawing.Size, System.Drawing"><value>167, 22</value></data>
93+ <data name="ImageCancelButton.TabIndex" type="System.Int32, mscorlib"><value>2</value></data>
94+ <data name="ImageCancelButton.Text"><value>閉じる</value></data>
10195 <data name="ImageSelectedPicture.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Fill</value></data>
10296 <data name="ImageSelectedPicture.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data>
10397 <data name="ImageSelectedPicture.Location" type="System.Drawing.Point, System.Drawing"><value>0, 0</value></data>
104- <data name="ImageSelectedPicture.Size" type="System.Drawing.Size, System.Drawing"><value>608, 227</value></data>
98+ <data name="ImageSelectedPicture.Size" type="System.Drawing.Size, System.Drawing"><value>608, 128</value></data>
10599 <data name="ImageSelectedPicture.SizeMode" type="System.Windows.Forms.PictureBoxSizeMode, System.Windows.Forms"><value>Zoom</value></data>
106100 <data name="ImageSelectedPicture.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
107- <data name="ImageServiceCombo.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Right</value></data>
101+ <data name="ImageServiceCombo.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Fill</value></data>
108102 <data name="ImageServiceCombo.Items"><value>Twitter</value></data>
109- <data name="ImageServiceCombo.Location" type="System.Drawing.Point, System.Drawing"><value>442, 3</value></data>
110- <data name="ImageServiceCombo.Size" type="System.Drawing.Size, System.Drawing"><value>103, 20</value></data>
111- <data name="ImageServiceCombo.TabIndex" type="System.Int32, mscorlib"><value>4</value></data>
112- <data name="Label2.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Right</value></data>
103+ <data name="ImageServiceCombo.Location" type="System.Drawing.Point, System.Drawing"><value>61, 0</value></data>
104+ <data name="ImageServiceCombo.Size" type="System.Drawing.Size, System.Drawing"><value>106, 20</value></data>
105+ <data name="ImageServiceCombo.TabIndex" type="System.Int32, mscorlib"><value>1</value></data>
106+ <data name="Label2.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Left</value></data>
113107 <data name="Label2.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data>
114- <data name="Label2.Location" type="System.Drawing.Point, System.Drawing"><value>381, 3</value></data>
115- <data name="Label2.Size" type="System.Drawing.Size, System.Drawing"><value>61, 22</value></data>
116- <data name="Label2.TabIndex" type="System.Int32, mscorlib"><value>3</value></data>
108+ <data name="Label2.Location" type="System.Drawing.Point, System.Drawing"><value>0, 0</value></data>
109+ <data name="Label2.Size" type="System.Drawing.Size, System.Drawing"><value>61, 20</value></data>
110+ <data name="Label2.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
117111 <data name="Label2.Text"><value>投稿先</value></data>
118- <data name="Label2.TextAlign" type="System.Drawing.ContentAlignment, System.Drawing"><value>MiddleRight</value></data>
112+ <data name="Label2.TextAlign" type="System.Drawing.ContentAlignment, System.Drawing"><value>MiddleLeft</value></data>
113+ <data name="MediaListView.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Fill</value></data>
114+ <data name="MediaListView.Location" type="System.Drawing.Point, System.Drawing"><value>3, 3</value></data>
115+ <data name="MediaListView.Size" type="System.Drawing.Size, System.Drawing"><value>429, 100</value></data>
116+ <data name="MediaListView.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
117+ <data name="panel1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Fill</value></data>
118+ <data name="panel1.Location" type="System.Drawing.Point, System.Drawing"><value>438, 3</value></data>
119+ <data name="panel1.Size" type="System.Drawing.Size, System.Drawing"><value>167, 100</value></data>
120+ <data name="panel1.TabIndex" type="System.Int32, mscorlib"><value>1</value></data>
121+ <data name="ServiceSelectPanel.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms"><value>Top, Left, Right</value></data>
122+ <data name="ServiceSelectPanel.AutoSize" type="System.Boolean, mscorlib"><value>True</value></data>
123+ <data name="ServiceSelectPanel.Location" type="System.Drawing.Point, System.Drawing"><value>0, 0</value></data>
124+ <data name="ServiceSelectPanel.Margin" type="System.Windows.Forms.Padding, System.Windows.Forms"><value>0, 0, 0, 0</value></data>
125+ <data name="ServiceSelectPanel.Size" type="System.Drawing.Size, System.Drawing"><value>167, 20</value></data>
126+ <data name="ServiceSelectPanel.TabIndex" type="System.Int32, mscorlib"><value>0</value></data>
127+ <data name="tableLayoutPanel1.ColumnCount" type="System.Int32, mscorlib"><value>2</value></data>
128+ <data name="tableLayoutPanel1.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Bottom</value></data>
129+ <data name="tableLayoutPanel1.LayoutSettings" type="System.Windows.Forms.TableLayoutSettings, System.Windows.Forms"><value>&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;TableLayoutSettings&gt;&lt;Controls&gt;&lt;Control Name="MediaListView" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /&gt;&lt;Control Name="panel1" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /&gt;&lt;/Controls&gt;&lt;Columns Styles="Percent,100,Absolute,173" /&gt;&lt;Rows Styles="Percent,100" /&gt;&lt;/TableLayoutSettings&gt;</value></data>
130+ <data name="tableLayoutPanel1.Location" type="System.Drawing.Point, System.Drawing"><value>0, 174</value></data>
131+ <data name="tableLayoutPanel1.RowCount" type="System.Int32, mscorlib"><value>1</value></data>
132+ <data name="tableLayoutPanel1.Size" type="System.Drawing.Size, System.Drawing"><value>608, 106</value></data>
133+ <data name="tableLayoutPanel1.TabIndex" type="System.Int32, mscorlib"><value>1</value></data>
119134 </root>
--- /dev/null
+++ b/OpenTween/MemoryImageList.cs
@@ -0,0 +1,73 @@
1+// OpenTween - Client of Twitter
2+// Copyright (c) 2023 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;
26+using System.Collections.Generic;
27+using System.Windows.Forms;
28+
29+namespace OpenTween
30+{
31+ /// <summary>
32+ /// <see cref="System.Windows.Forms.ImageList"/> の画像に <see cref="MemoryImage"/> を使用するためのラッパー
33+ /// </summary>
34+ public sealed class MemoryImageList : IDisposable, IEnumerable<MemoryImage>
35+ {
36+ private readonly ImageList innerImageList = new();
37+ private readonly Dictionary<string, MemoryImage> images = new();
38+
39+ public ImageList ImageList
40+ => this.innerImageList;
41+
42+ public void Add(string key, MemoryImage image)
43+ {
44+ this.images.Add(key, image);
45+ this.innerImageList.Images.Add(key, image.Image);
46+ }
47+
48+ public void Remove(string key)
49+ {
50+ this.images.Remove(key);
51+ this.innerImageList.Images.RemoveByKey(key);
52+ }
53+
54+ public void Clear()
55+ {
56+ this.images.Clear();
57+ this.innerImageList.Images.Clear();
58+ }
59+
60+ public void Dispose()
61+ {
62+ // MemoryImage インスタンスの破棄は行わない
63+ this.Clear();
64+ this.innerImageList.Dispose();
65+ }
66+
67+ public IEnumerator<MemoryImage> GetEnumerator()
68+ => this.images.Values.GetEnumerator();
69+
70+ IEnumerator IEnumerable.GetEnumerator()
71+ => this.GetEnumerator();
72+ }
73+}
--- a/OpenTween/OpenTween.csproj
+++ b/OpenTween/OpenTween.csproj
@@ -118,11 +118,11 @@
118118 <Compile Update="InputDialog.Designer.cs">
119119 <DependentUpon>InputDialog.cs</DependentUpon>
120120 </Compile>
121- <Compile Update="MediaSelector.cs">
121+ <Compile Update="MediaSelectorPanel.cs">
122122 <SubType>UserControl</SubType>
123123 </Compile>
124- <Compile Update="MediaSelector.Designer.cs">
125- <DependentUpon>MediaSelector.cs</DependentUpon>
124+ <Compile Update="MediaSelectorPanel.Designer.cs">
125+ <DependentUpon>MediaSelectorPanel.cs</DependentUpon>
126126 </Compile>
127127 <Compile Update="NativeMethods.cs">
128128 <SubType>Code</SubType>
@@ -386,11 +386,11 @@
386386 <EmbeddedResource Update="LoginDialog.resx">
387387 <DependentUpon>LoginDialog.cs</DependentUpon>
388388 </EmbeddedResource>
389- <EmbeddedResource Update="MediaSelector.en.resx">
390- <DependentUpon>MediaSelector.cs</DependentUpon>
389+ <EmbeddedResource Update="MediaSelectorPanel.en.resx">
390+ <DependentUpon>MediaSelectorPanel.cs</DependentUpon>
391391 </EmbeddedResource>
392- <EmbeddedResource Update="MediaSelector.resx">
393- <DependentUpon>MediaSelector.cs</DependentUpon>
392+ <EmbeddedResource Update="MediaSelectorPanel.resx">
393+ <DependentUpon>MediaSelectorPanel.cs</DependentUpon>
394394 </EmbeddedResource>
395395 <EmbeddedResource Update="SendErrorReportForm.en.resx">
396396 <DependentUpon>SendErrorReportForm.cs</DependentUpon>
--- a/OpenTween/Properties/AssemblyInfo.cs
+++ b/OpenTween/Properties/AssemblyInfo.cs
@@ -22,7 +22,7 @@ using System.Runtime.InteropServices;
2222 // 次の GUID は、このプロジェクトが COM に公開される場合の、typelib の ID です
2323 [assembly: Guid("2d0ae0ba-adac-49a2-9b10-26fd69e695bf")]
2424
25-[assembly: AssemblyVersion("3.1.0.0")]
25+[assembly: AssemblyVersion("3.2.0.0")]
2626
2727 [assembly: InternalsVisibleTo("OpenTween.Tests")]
2828 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Moq
--- a/OpenTween/Properties/Resources.Designer.cs
+++ b/OpenTween/Properties/Resources.Designer.cs
@@ -571,6 +571,10 @@ namespace OpenTween.Properties {
571571 /// <summary>
572572 /// 更新履歴
573573 ///
574+ ///==== Ver 3.2.0(2023/01/20)
575+ /// * NEW: 複数枚の画像を添付する際にリスト上で画像を確認できるようになりました
576+ /// * CHG: アカウント追加時の認可関連のエラーメッセージがより詳細になるように変更
577+ ///
574578 ///==== Ver 3.1.0(2023/01/14)
575579 /// * NEW: 引用ツイートを Ctrl+Shift+L で実行するショートカットを追加 (thx @WizardOfPSG!)
576580 /// * CHG: 発言一覧のフォントサイズがアイコンより大きい場合は項目の高さをフォントサイズに合わせるように変更
@@ -579,8 +583,7 @@ namespace OpenTween.Properties {
579583 ///
580584 ///==== Ver 3.0.0(2023/01/11)
581585 /// * OpenTween v3.0.0 からは .NET Framework 4.8 以上が必須になります
582- /// - .NET Framework 4.8 ランタイムは https://dotnet.microsoft.com/ja-jp/download/dotnet-framework/net48 から入手できます
583- /// - Windows 10 21H1 以降には標準で .NET Framework 4.8 が含まれているため追加 [残りの文字列は切り詰められました]&quot;; に類似しているローカライズされた文字列を検索します。
586+ /// - .NET Framework 4.8 ランタイムは https://dotnet.m [残りの文字列は切り詰められました]&quot;; に類似しているローカライズされた文字列を検索します。
584587 /// </summary>
585588 internal static string ChangeLog {
586589 get {
--- a/OpenTween/Resources/ChangeLog.txt
+++ b/OpenTween/Resources/ChangeLog.txt
@@ -1,5 +1,9 @@
11 更新履歴
22
3+==== Ver 3.2.0(2023/01/20)
4+ * NEW: 複数枚の画像を添付する際にリスト上で画像を確認できるようになりました
5+ * CHG: アカウント追加時の認可関連のエラーメッセージがより詳細になるように変更
6+
37 ==== Ver 3.1.0(2023/01/14)
48 * NEW: 引用ツイートを Ctrl+Shift+L で実行するショートカットを追加 (thx @WizardOfPSG!)
59 * CHG: 発言一覧のフォントサイズがアイコンより大きい場合は項目の高さをフォントサイズに合わせるように変更
--- a/OpenTween/Tween.Designer.cs
+++ b/OpenTween/Tween.Designer.cs
@@ -53,7 +53,7 @@
5353 this.ToolStripSeparator11 = new System.Windows.Forms.ToolStripSeparator();
5454 this.DeleteTabMenuItem = new System.Windows.Forms.ToolStripMenuItem();
5555 this.TabImage = new System.Windows.Forms.ImageList(this.components);
56- this.ImageSelector = new OpenTween.MediaSelector();
56+ this.ImageSelector = new OpenTween.MediaSelectorPanel();
5757 this.ProfilePanel = new System.Windows.Forms.Panel();
5858 this.SplitContainer3 = new System.Windows.Forms.SplitContainer();
5959 this.SplitContainer2 = new System.Windows.Forms.SplitContainer();
@@ -2217,7 +2217,7 @@
22172217 internal System.Windows.Forms.ToolStripSeparator ToolStripSeparator11;
22182218 internal System.Windows.Forms.ToolStripMenuItem DeleteTabMenuItem;
22192219 internal System.Windows.Forms.ImageList TabImage;
2220- internal MediaSelector ImageSelector;
2220+ internal MediaSelectorPanel ImageSelector;
22212221 internal System.Windows.Forms.Panel ProfilePanel;
22222222 internal System.Windows.Forms.SplitContainer SplitContainer3;
22232223 internal System.Windows.Forms.SplitContainer SplitContainer2;
--- a/OpenTween/Tween.cs
+++ b/OpenTween/Tween.cs
@@ -555,7 +555,8 @@ namespace OpenTween
555555 Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection;
556556
557557 // 画像投稿サービス
558- this.ImageSelector.Initialize(this.tw, this.tw.Configuration, this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
558+ this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
559+ this.ImageSelector.Model.SelectMediaService(this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
559560
560561 this.tweetThumbnail1.Initialize(this.thumbGenerator);
561562
@@ -1282,7 +1283,8 @@ namespace OpenTween
12821283 if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems))
12831284 return;
12841285
1285- uploadService = this.ImageSelector.GetService(serviceName);
1286+ this.ImageSelector.EndSelection();
1287+ uploadService = this.ImageSelector.Model.GetService(serviceName);
12861288 }
12871289
12881290 this.inReplyTo = null;
@@ -1950,7 +1952,7 @@ namespace OpenTween
19501952
19511953 if (this.tw.Configuration.PhotoSizeLimit != 0)
19521954 {
1953- foreach (var service in this.ImageSelector.GetServices())
1955+ foreach (var (_, service) in this.ImageSelector.Model.MediaServices)
19541956 {
19551957 service.UpdateTwitterConfiguration(this.tw.Configuration);
19561958 }
@@ -2583,7 +2585,7 @@ namespace OpenTween
25832585 this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck;
25842586 this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost;
25852587
2586- this.ImageSelector.Reset(this.tw, this.tw.Configuration);
2588+ this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
25872589
25882590 try
25892591 {
@@ -3518,7 +3520,7 @@ namespace OpenTween
35183520 attachmentUrl = null;
35193521
35203522 // attachment_url は media_id と同時に使用できない
3521- if (this.ImageSelector.Visible && this.ImageSelector.SelectedService is TwitterPhoto)
3523+ if (this.ImageSelector.Visible && this.ImageSelector.Model.SelectedMediaService is TwitterPhoto)
35223524 return statusText;
35233525
35243526 var match = Twitter.AttachmentUrlRegex.Match(statusText);
@@ -3661,7 +3663,7 @@ namespace OpenTween
36613663 }
36623664
36633665 private IMediaUploadService? GetSelectedImageService()
3664- => this.ImageSelector.Visible ? this.ImageSelector.SelectedService : null;
3666+ => this.ImageSelector.Visible ? this.ImageSelector.Model.SelectedMediaService : null;
36653667
36663668 /// <summary>
36673669 /// 全てのタブの振り分けルールを反映し直します
@@ -5732,8 +5734,8 @@ namespace OpenTween
57325734 this.settings.Common.HashIsHead = this.HashMgr.IsHead;
57335735 this.settings.Common.HashIsPermanent = this.HashMgr.IsPermanent;
57345736 this.settings.Common.HashIsNotAddToAtReply = this.HashMgr.IsNotAddToAtReply;
5735- this.settings.Common.UseImageService = this.ImageSelector.ServiceIndex;
5736- this.settings.Common.UseImageServiceName = this.ImageSelector.ServiceName;
5737+ this.settings.Common.UseImageService = this.ImageSelector.Model.SelectedMediaServiceIndex;
5738+ this.settings.Common.UseImageServiceName = this.ImageSelector.Model.SelectedMediaServiceName;
57375739
57385740 this.settings.SaveCommon();
57395741 }
@@ -9168,7 +9170,7 @@ namespace OpenTween
91689170
91699171 private void SelectMedia_DragEnter(DragEventArgs e)
91709172 {
9171- if (this.ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
9173+ if (this.ImageSelector.Model.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
91729174 {
91739175 e.Effect = DragDropEffects.Copy;
91749176 return;
@@ -9180,7 +9182,10 @@ namespace OpenTween
91809182 {
91819183 this.Activate();
91829184 this.BringToFront();
9183- this.ImageSelector.BeginSelection((string[])e.Data.GetData(DataFormats.FileDrop, false));
9185+
9186+ var filePathArray = (string[])e.Data.GetData(DataFormats.FileDrop, false);
9187+ this.ImageSelector.BeginSelection();
9188+ this.ImageSelector.Model.AddMediaItemFromFilePath(filePathArray);
91849189 this.StatusText.Focus();
91859190 }
91869191
@@ -9231,12 +9236,14 @@ namespace OpenTween
92319236 {
92329237 // clipboardから画像を取得
92339238 using var image = Clipboard.GetImage();
9234- this.ImageSelector.BeginSelection(image);
9239+ this.ImageSelector.BeginSelection();
9240+ this.ImageSelector.Model.AddMediaItemFromImage(image);
92359241 }
92369242 else if (Clipboard.ContainsFileDropList())
92379243 {
92389244 var files = Clipboard.GetFileDropList().Cast<string>().ToArray();
9239- this.ImageSelector.BeginSelection(files);
9245+ this.ImageSelector.BeginSelection();
9246+ this.ImageSelector.Model.AddMediaItemFromFilePath(files);
92409247 }
92419248 }
92429249 catch (ExternalException ex)
--- a/OpenTween/Tween.resx
+++ b/OpenTween/Tween.resx
@@ -156,7 +156,7 @@
156156 <data name="&gt;&gt;ImageSelectMenuItem.Type"><value>System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
157157 <data name="&gt;&gt;ImageSelector.Name"><value>ImageSelector</value></data>
158158 <data name="&gt;&gt;ImageSelector.Parent"><value>SplitContainer1.Panel1</value></data>
159- <data name="&gt;&gt;ImageSelector.Type"><value>OpenTween.MediaSelector, OpenTween, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null</value></data>
159+ <data name="&gt;&gt;ImageSelector.Type"><value>OpenTween.MediaSelectorPanel, OpenTween, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null</value></data>
160160 <data name="&gt;&gt;ImageSelector.ZOrder"><value>1</value></data>
161161 <data name="&gt;&gt;ImageSelectPullDownMenuItem.Name"><value>ImageSelectPullDownMenuItem</value></data>
162162 <data name="&gt;&gt;ImageSelectPullDownMenuItem.Type"><value>System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data>
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,4 +1,4 @@
1-version: 3.0.0.{build}
1+version: 3.1.0.{build}
22
33 os: Visual Studio 2022
44
Show on old repository browser