修訂 | 9db5558929e2ced9c0139a97efe0744087faa744 (tree) |
---|---|
時間 | 2023-01-19 10:42:08 |
作者 | Kimura Youichi <kim.upsilon@bucy...> |
Commiter | Kimura Youichi |
Merge branch 'develop' into release
@@ -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; | |
2 | 24 | using System.Collections.Generic; |
3 | -using System.ComponentModel; | |
4 | 25 | using System.Drawing; |
5 | 26 | using System.IO; |
6 | 27 | using System.Linq; |
7 | 28 | using System.Reflection; |
8 | 29 | using System.Runtime.InteropServices; |
9 | 30 | using System.Text; |
10 | -using System.Text.RegularExpressions; | |
11 | 31 | using Moq; |
12 | 32 | using OpenTween.Api; |
13 | 33 | using OpenTween.Api.DataModel; |
@@ -29,438 +49,250 @@ namespace OpenTween | ||
29 | 49 | } |
30 | 50 | |
31 | 51 | [Fact] |
32 | - public void Initialize_TwitterTest() | |
52 | + public void SelectMediaService_TwitterTest() | |
33 | 53 | { |
34 | 54 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
35 | 55 | using var twitter = new Twitter(twitterApi); |
36 | 56 | using var mediaSelector = new MediaSelector(); |
37 | 57 | twitter.Initialize("", "", "", 0L); |
38 | - mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter"); | |
58 | + mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); | |
59 | + mediaSelector.SelectMediaService("Twitter"); | |
39 | 60 | |
40 | - Assert.NotEqual(-1, mediaSelector.ImageServiceCombo.Items.IndexOf("Twitter")); | |
61 | + Assert.Contains(mediaSelector.MediaServices, x => x.Key == "Twitter"); | |
41 | 62 | |
42 | 63 | // 投稿先に Twitter が選択されている |
43 | - Assert.Equal("Twitter", mediaSelector.ImageServiceCombo.Text); | |
64 | + Assert.Equal("Twitter", mediaSelector.SelectedMediaServiceName); | |
44 | 65 | |
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); | |
51 | 68 | } |
52 | 69 | |
53 | 70 | [Fact] |
54 | - public void Initialize_ImgurTest() | |
71 | + public void SelectMediaService_ImgurTest() | |
55 | 72 | { |
56 | 73 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
57 | 74 | using var twitter = new Twitter(twitterApi); |
58 | 75 | using var mediaSelector = new MediaSelector(); |
59 | 76 | twitter.Initialize("", "", "", 0L); |
60 | - mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Imgur"); | |
77 | + mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); | |
78 | + mediaSelector.SelectMediaService("Imgur"); | |
61 | 79 | |
62 | 80 | // 投稿先に 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); | |
90 | 82 | |
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); | |
99 | 85 | } |
100 | 86 | |
101 | 87 | [Fact] |
102 | - public void BeginSelection_FilePathTest() | |
88 | + public void AddMediaItem_FilePath_SingleTest() | |
103 | 89 | { |
104 | 90 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
105 | 91 | using var twitter = new Twitter(twitterApi); |
106 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
92 | + using var mediaSelector = new MediaSelector(); | |
107 | 93 | twitter.Initialize("", "", "", 0L); |
108 | - mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter"); | |
94 | + mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); | |
95 | + mediaSelector.SelectMediaService("Twitter"); | |
109 | 96 | |
110 | 97 | var images = new[] { "Resources/re.gif" }; |
98 | + mediaSelector.AddMediaItemFromFilePath(images); | |
111 | 99 | |
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); | |
120 | 102 | |
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); | |
128 | 106 | |
129 | 107 | 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); | |
132 | 111 | } |
133 | 112 | |
134 | 113 | [Fact] |
135 | - public void BeginSelection_MemoryImageTest() | |
114 | + public void AddMediaItem_MemoryImageTest() | |
136 | 115 | { |
137 | 116 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
138 | 117 | using var twitter = new Twitter(twitterApi); |
139 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
118 | + using var mediaSelector = new MediaSelector(); | |
140 | 119 | twitter.Initialize("", "", "", 0L); |
141 | - mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter"); | |
120 | + mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); | |
121 | + mediaSelector.SelectMediaService("Twitter"); | |
142 | 122 | |
143 | 123 | 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); | |
154 | 125 | |
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); | |
158 | 128 | |
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); | |
162 | 132 | |
163 | 133 | using (var bitmap = new Bitmap(width: 200, height: 200)) |
164 | 134 | { |
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); | |
167 | 138 | } |
168 | 139 | } |
169 | 140 | |
170 | 141 | [Fact] |
171 | - public void BeginSelection_MultiImageTest() | |
142 | + public void AddMediaItem_FilePath_MultipleTest() | |
172 | 143 | { |
173 | 144 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
174 | 145 | using var twitter = new Twitter(twitterApi); |
175 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
146 | + using var mediaSelector = new MediaSelector(); | |
176 | 147 | twitter.Initialize("", "", "", 0L); |
177 | - mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter"); | |
148 | + mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); | |
149 | + mediaSelector.SelectMediaService("Twitter"); | |
178 | 150 | |
179 | 151 | 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); | |
206 | 153 | |
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); | |
212 | 156 | |
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); | |
215 | 160 | |
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); | |
217 | 165 | } |
218 | 166 | |
219 | 167 | [Fact] |
220 | - public void PageChange_Test() | |
168 | + public void ClearMediaItems_Test() | |
221 | 169 | { |
222 | 170 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
223 | 171 | using var twitter = new Twitter(twitterApi); |
224 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
172 | + using var mediaSelector = new MediaSelector(); | |
225 | 173 | 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"); | |
230 | 176 | |
231 | - mediaSelector.ImagePageCombo.SelectedIndex = 0; | |
177 | + mediaSelector.AddMediaItemFromFilePath(new[] { "Resources/re.gif" }); | |
232 | 178 | |
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(); // 表示中の画像 | |
254 | 181 | |
255 | - mediaSelector.ImagePageCombo.SelectedIndex = 2; | |
182 | + mediaSelector.ClearMediaItems(); | |
256 | 183 | |
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)); | |
261 | 186 | } |
262 | 187 | |
263 | 188 | [Fact] |
264 | - public void PageChange_AlternativeTextTest() | |
189 | + public void DetachMediaItems_Test() | |
265 | 190 | { |
266 | 191 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
267 | 192 | using var twitter = new Twitter(twitterApi); |
268 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
193 | + using var mediaSelector = new MediaSelector(); | |
269 | 194 | 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"); | |
274 | 197 | |
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" }); | |
284 | 199 | |
285 | - // 3 ページ目 (新規ページ) | |
286 | - mediaSelector.ImagePageCombo.SelectedIndex = 2; | |
287 | - mediaSelector.AlternativeTextBox.Text = "Page 3"; | |
288 | - mediaSelector.ValidateChildren(); | |
200 | + var thumbnailImages = mediaSelector.ThumbnailList.ToArray(); | |
289 | 201 | |
290 | - mediaSelector.ImagePageCombo.SelectedIndex = 0; | |
291 | - Assert.Equal("Page 1", mediaSelector.AlternativeTextBox.Text); | |
202 | + var detachedMediaItems = mediaSelector.DetachMediaItems(); | |
292 | 203 | |
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)); | |
295 | 206 | |
296 | - // 画像が指定されていないページは入力した代替テキストも保持されない | |
297 | - mediaSelector.ImagePageCombo.SelectedIndex = 2; | |
298 | - Assert.Equal("", mediaSelector.AlternativeTextBox.Text); | |
207 | + // DetachMediaItems で切り離された MediaItem は破棄しない | |
208 | + Assert.True(detachedMediaItems.All(x => !x.IsDisposed)); | |
299 | 209 | } |
300 | 210 | |
301 | 211 | [Fact] |
302 | - public void PageChange_ImageDisposeTest() | |
212 | + public void SelectedMediaItemChange_Test() | |
303 | 213 | { |
304 | 214 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
305 | 215 | using var twitter = new Twitter(twitterApi); |
306 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
216 | + using var mediaSelector = new MediaSelector(); | |
307 | 217 | twitter.Initialize("", "", "", 0L); |
308 | - mediaSelector.Initialize(twitter, TwitterConfiguration.DefaultConfiguration(), "Twitter"); | |
218 | + mediaSelector.InitializeServices(twitter, TwitterConfiguration.DefaultConfiguration()); | |
219 | + mediaSelector.SelectMediaService("Twitter"); | |
309 | 220 | |
310 | 221 | var images = new[] { "Resources/re.gif", "Resources/re1.png" }; |
311 | - mediaSelector.BeginSelection(images); | |
222 | + mediaSelector.AddMediaItemFromFilePath(images); | |
312 | 223 | |
313 | - mediaSelector.ImagePageCombo.SelectedIndex = 0; | |
224 | + mediaSelector.SelectedMediaItemIndex = 0; | |
314 | 225 | |
315 | 226 | // 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); | |
325 | 228 | |
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 | + } | |
329 | 235 | |
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; | |
339 | 237 | |
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); | |
343 | 240 | |
344 | - // 入力したパスの画像が表示される | |
345 | 241 | using (var imageStream = File.OpenRead("Resources/re1.png")) |
346 | 242 | { |
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); | |
349 | 246 | } |
350 | - | |
351 | - // 2 ページ目まで選択可能な状態 | |
352 | - var pages = mediaSelector.ImagePageCombo.Items; | |
353 | - Assert.Equal(new[] { "1", "2" }, pages.Cast<object>().Select(x => x.ToString())); | |
354 | 247 | } |
355 | 248 | |
356 | 249 | [Fact] |
357 | - public void ImagePathInput_ReplaceFileMediaItemTest() | |
250 | + public void SelectedMediaItemChange_DisposeTest() | |
358 | 251 | { |
359 | 252 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
360 | 253 | using var twitter = new Twitter(twitterApi); |
361 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
254 | + using var mediaSelector = new MediaSelector(); | |
362 | 255 | 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"); | |
366 | 258 | |
367 | - // 既に入力されているファイルパスの画像 | |
368 | - var image1 = mediaSelector.ImageSelectedPicture.Image; | |
259 | + var images = new[] { "Resources/re.gif", "Resources/re1.png" }; | |
260 | + mediaSelector.AddMediaItemFromFilePath(images); | |
369 | 261 | |
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; | |
373 | 265 | |
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; | |
380 | 269 | |
381 | - // 最初に入力されていたファイルパスの表示用の MemoryImage は破棄される | |
382 | - Assert.True(image1!.IsDisposed); | |
270 | + Assert.True(firstImage!.IsDisposed); | |
383 | 271 | } |
384 | 272 | |
385 | 273 | [Fact] |
386 | - public void ImagePathInput_ReplaceMemoryImageMediaItemTest() | |
274 | + public void SetSelectedMediaAltText_Test() | |
387 | 275 | { |
388 | 276 | using var twitterApi = new TwitterApi(ApiKey.Create(""), ApiKey.Create("")); |
389 | 277 | using var twitter = new Twitter(twitterApi); |
390 | - using var mediaSelector = new MediaSelector { Visible = false, Enabled = false }; | |
278 | + using var mediaSelector = new MediaSelector(); | |
391 | 279 | 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"); | |
410 | 282 | |
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); | |
417 | 285 | |
418 | - // 最初に入力されていたファイルパスの表示用の MemoryImage は破棄される | |
419 | - Assert.True(image1!.IsDisposed); | |
286 | + // 1 ページ目 | |
287 | + mediaSelector.SelectedMediaItemIndex = 0; | |
288 | + mediaSelector.SetSelectedMediaAltText("Page 1"); | |
420 | 289 | |
421 | - // 参照されなくなった MemoryImageMediaItem も破棄される | |
422 | - Assert.True(mediaItem.IsDisposed); | |
423 | - } | |
290 | + // 2 ページ目 | |
291 | + mediaSelector.SelectedMediaItemIndex = 1; | |
292 | + mediaSelector.SetSelectedMediaAltText("Page 2"); | |
424 | 293 | |
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); | |
464 | 296 | } |
465 | 297 | } |
466 | 298 | } |
@@ -43,6 +43,9 @@ namespace OpenTween.Api | ||
43 | 43 | public TwitterErrorItem[] Errors |
44 | 44 | => this.ErrorResponse != null ? this.ErrorResponse.Errors : Array.Empty<TwitterErrorItem>(); |
45 | 45 | |
46 | + public string[] LongMessages | |
47 | + => this.Errors.Select(x => x.Message).ToArray(); | |
48 | + | |
46 | 49 | public TwitterApiException() |
47 | 50 | { |
48 | 51 | } |
@@ -39,6 +39,7 @@ using System.Text; | ||
39 | 39 | using System.Threading; |
40 | 40 | using System.Threading.Tasks; |
41 | 41 | using System.Windows.Forms; |
42 | +using OpenTween.Api; | |
42 | 43 | using OpenTween.Connection; |
43 | 44 | using OpenTween.Setting.Panel; |
44 | 45 | using OpenTween.Thumbnail; |
@@ -208,9 +209,11 @@ namespace OpenTween | ||
208 | 209 | "Authenticate", |
209 | 210 | MessageBoxButtons.OK); |
210 | 211 | } |
211 | - catch (WebApiException ex) | |
212 | + catch (TwitterApiException ex) | |
212 | 213 | { |
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 | + | |
214 | 217 | MessageBox.Show(this, message, "Authenticate", MessageBoxButtons.OK); |
215 | 218 | } |
216 | 219 | } |
@@ -107,7 +107,7 @@ namespace OpenTween.Connection | ||
107 | 107 | if (endpointName != null) |
108 | 108 | MyCommon.TwitterApiInfo.UpdateFromHeader(response.Headers, endpointName); |
109 | 109 | |
110 | - await this.CheckStatusCode(response) | |
110 | + await TwitterApiConnection.CheckStatusCode(response) | |
111 | 111 | .ConfigureAwait(false); |
112 | 112 | |
113 | 113 | using var content = response.Content; |
@@ -190,7 +190,7 @@ namespace OpenTween.Connection | ||
190 | 190 | var response = await this.HttpStreaming.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) |
191 | 191 | .ConfigureAwait(false); |
192 | 192 | |
193 | - await this.CheckStatusCode(response) | |
193 | + await TwitterApiConnection.CheckStatusCode(response) | |
194 | 194 | .ConfigureAwait(false); |
195 | 195 | |
196 | 196 | return await response.Content.ReadAsStreamAsync() |
@@ -220,7 +220,7 @@ namespace OpenTween.Connection | ||
220 | 220 | response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) |
221 | 221 | .ConfigureAwait(false); |
222 | 222 | |
223 | - await this.CheckStatusCode(response) | |
223 | + await TwitterApiConnection.CheckStatusCode(response) | |
224 | 224 | .ConfigureAwait(false); |
225 | 225 | |
226 | 226 | var result = new LazyJson<T>(response); |
@@ -267,7 +267,7 @@ namespace OpenTween.Connection | ||
267 | 267 | response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) |
268 | 268 | .ConfigureAwait(false); |
269 | 269 | |
270 | - await this.CheckStatusCode(response) | |
270 | + await TwitterApiConnection.CheckStatusCode(response) | |
271 | 271 | .ConfigureAwait(false); |
272 | 272 | |
273 | 273 | var result = new LazyJson<T>(response); |
@@ -313,7 +313,7 @@ namespace OpenTween.Connection | ||
313 | 313 | using var response = await this.HttpUpload.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) |
314 | 314 | .ConfigureAwait(false); |
315 | 315 | |
316 | - await this.CheckStatusCode(response) | |
316 | + await TwitterApiConnection.CheckStatusCode(response) | |
317 | 317 | .ConfigureAwait(false); |
318 | 318 | } |
319 | 319 | catch (HttpRequestException ex) |
@@ -345,7 +345,7 @@ namespace OpenTween.Connection | ||
345 | 345 | response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) |
346 | 346 | .ConfigureAwait(false); |
347 | 347 | |
348 | - await this.CheckStatusCode(response) | |
348 | + await TwitterApiConnection.CheckStatusCode(response) | |
349 | 349 | .ConfigureAwait(false); |
350 | 350 | |
351 | 351 | var result = new LazyJson<T>(response); |
@@ -377,7 +377,7 @@ namespace OpenTween.Connection | ||
377 | 377 | using var response = await this.Http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead) |
378 | 378 | .ConfigureAwait(false); |
379 | 379 | |
380 | - await this.CheckStatusCode(response) | |
380 | + await TwitterApiConnection.CheckStatusCode(response) | |
381 | 381 | .ConfigureAwait(false); |
382 | 382 | } |
383 | 383 | catch (HttpRequestException ex) |
@@ -390,7 +390,7 @@ namespace OpenTween.Connection | ||
390 | 390 | } |
391 | 391 | } |
392 | 392 | |
393 | - protected async Task CheckStatusCode(HttpResponseMessage response) | |
393 | + protected static async Task CheckStatusCode(HttpResponseMessage response) | |
394 | 394 | { |
395 | 395 | var statusCode = response.StatusCode; |
396 | 396 |
@@ -546,8 +546,8 @@ namespace OpenTween.Connection | ||
546 | 546 | var responseText = await content.ReadAsStringAsync() |
547 | 547 | .ConfigureAwait(false); |
548 | 548 | |
549 | - if (!response.IsSuccessStatusCode) | |
550 | - throw new TwitterApiException(response.StatusCode, responseText); | |
549 | + await TwitterApiConnection.CheckStatusCode(response) | |
550 | + .ConfigureAwait(false); | |
551 | 551 | |
552 | 552 | var responseParams = HttpUtility.ParseQueryString(responseText); |
553 | 553 |
@@ -158,6 +158,19 @@ namespace OpenTween | ||
158 | 158 | return count; |
159 | 159 | } |
160 | 160 | |
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 | + | |
161 | 174 | public static Task ForEachAsync<T>(this IObservable<T> observable, Action<T> subscriber) |
162 | 175 | { |
163 | 176 | return ForEachAsync(observable, value => |
@@ -30,9 +30,19 @@ using System.Threading; | ||
30 | 30 | |
31 | 31 | namespace OpenTween |
32 | 32 | { |
33 | - public interface IMediaItem | |
33 | + public interface IMediaItem : IDisposable | |
34 | 34 | { |
35 | 35 | /// <summary> |
36 | + /// メディアのID | |
37 | + /// </summary> | |
38 | + Guid Id { get; } | |
39 | + | |
40 | + /// <summary> | |
41 | + /// メディアが既に破棄されているかを示す真偽値 | |
42 | + /// </summary> | |
43 | + bool IsDisposed { get; } | |
44 | + | |
45 | + /// <summary> | |
36 | 46 | /// メディアへの絶対パス |
37 | 47 | /// </summary> |
38 | 48 | string Path { get; } |
@@ -58,11 +68,6 @@ namespace OpenTween | ||
58 | 68 | long Size { get; } |
59 | 69 | |
60 | 70 | /// <summary> |
61 | - /// メディアが画像であるかどうかを示す真偽値 | |
62 | - /// </summary> | |
63 | - bool IsImage { get; } | |
64 | - | |
65 | - /// <summary> | |
66 | 71 | /// 代替テキスト (アップロード先が対応している必要がある) |
67 | 72 | /// </summary> |
68 | 73 | string? AltText { get; set; } |
@@ -106,6 +111,10 @@ namespace OpenTween | ||
106 | 111 | { |
107 | 112 | } |
108 | 113 | |
114 | + public Guid Id { get; } = Guid.NewGuid(); | |
115 | + | |
116 | + public bool IsDisposed { get; private set; } = false; | |
117 | + | |
109 | 118 | public string Path |
110 | 119 | => this.FileInfo.FullName; |
111 | 120 |
@@ -121,34 +130,6 @@ namespace OpenTween | ||
121 | 130 | public long Size |
122 | 131 | => this.FileInfo.Length; |
123 | 132 | |
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 | - | |
152 | 133 | public MemoryImage CreateImage() |
153 | 134 | { |
154 | 135 | using var fs = this.FileInfo.OpenRead(); |
@@ -163,6 +144,14 @@ namespace OpenTween | ||
163 | 144 | using var fs = this.FileInfo.OpenRead(); |
164 | 145 | fs.CopyTo(stream); |
165 | 146 | } |
147 | + | |
148 | + public void Dispose() | |
149 | + { | |
150 | + if (this.IsDisposed) | |
151 | + return; | |
152 | + | |
153 | + this.IsDisposed = true; | |
154 | + } | |
166 | 155 | } |
167 | 156 | |
168 | 157 | /// <summary> |
@@ -171,7 +160,7 @@ namespace OpenTween | ||
171 | 160 | /// <remarks> |
172 | 161 | /// 用途の関係上、メモリ使用量が大きくなるため、不要になればできるだけ破棄すること |
173 | 162 | /// </remarks> |
174 | - public class MemoryImageMediaItem : IMediaItem, IDisposable | |
163 | + public class MemoryImageMediaItem : IMediaItem | |
175 | 164 | { |
176 | 165 | public const string PathPrefix = "<>MemoryImage://"; |
177 | 166 | private static int fileNumber = 0; |
@@ -187,6 +176,8 @@ namespace OpenTween | ||
187 | 176 | this.Path = PathPrefix + num + this.image.ImageFormatExt; |
188 | 177 | } |
189 | 178 | |
179 | + public Guid Id { get; } = Guid.NewGuid(); | |
180 | + | |
190 | 181 | public string Path { get; } |
191 | 182 | |
192 | 183 | public string? AltText { get; set; } |
@@ -203,9 +194,6 @@ namespace OpenTween | ||
203 | 194 | public long Size |
204 | 195 | => this.image.Stream.Length; |
205 | 196 | |
206 | - public bool IsImage | |
207 | - => true; | |
208 | - | |
209 | 197 | public MemoryImage CreateImage() |
210 | 198 | => this.image.Clone(); |
211 | 199 |
@@ -1,5 +1,6 @@ | ||
1 | 1 | // OpenTween - Client of Twitter |
2 | 2 | // Copyright (c) 2014 spx (@5px) |
3 | +// Copyright (c) 2023 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/> | |
3 | 4 | // All rights reserved. |
4 | 5 | // |
5 | 6 | // This file is part of OpenTween. |
@@ -24,158 +25,127 @@ | ||
24 | 25 | using System; |
25 | 26 | using System.Collections.Generic; |
26 | 27 | using System.ComponentModel; |
27 | -using System.Data; | |
28 | -using System.Diagnostics.CodeAnalysis; | |
29 | 28 | using System.Drawing; |
30 | 29 | using System.IO; |
31 | 30 | using System.Linq; |
32 | 31 | using System.Text; |
33 | 32 | using System.Threading.Tasks; |
34 | -using System.Windows.Forms; | |
35 | 33 | using OpenTween.Api.DataModel; |
36 | 34 | using OpenTween.MediaUploadServices; |
37 | 35 | |
38 | 36 | namespace OpenTween |
39 | 37 | { |
40 | - public partial class MediaSelector : UserControl | |
38 | + public sealed class MediaSelector : NotifyPropertyChangedBase, IDisposable | |
41 | 39 | { |
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; | |
43 | 45 | |
44 | - public event EventHandler<EventArgs>? EndSelecting; | |
46 | + public bool IsDisposed { get; private set; } = false; | |
45 | 47 | |
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 | + } | |
49 | 53 | |
50 | - public event EventHandler<EventArgs>? SelectedServiceChanged; | |
54 | + public BindingList<IMediaItem> MediaItems | |
55 | + => this.mediaItems; | |
51 | 56 | |
52 | - [Browsable(false)] | |
53 | - [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] | |
54 | - public OpenFileDialog? FilePickDialog { get; set; } | |
57 | + public MemoryImageList ThumbnailList { get; } = new(); | |
55 | 58 | |
56 | 59 | /// <summary> |
57 | 60 | /// 選択されている投稿先名を取得する。 |
58 | 61 | /// </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 | + } | |
63 | 67 | |
64 | 68 | /// <summary> |
65 | 69 | /// 選択されている投稿先を示すインデックスを取得する。 |
66 | 70 | /// </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); | |
71 | 73 | |
72 | 74 | /// <summary> |
73 | 75 | /// 選択されている投稿先の IMediaUploadService を取得する。 |
74 | 76 | /// </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); | |
98 | 79 | |
99 | - /// <summary> | |
100 | - /// 利用可能な全ての IMediaUploadService インスタンスを取得する。 | |
101 | - /// </summary> | |
102 | - public ICollection<IMediaUploadService> GetServices() | |
103 | - => this.pictureService.Values; | |
80 | + public bool CanUseAltText | |
81 | + => this.SelectedMediaService?.CanUseAltText ?? false; | |
104 | 82 | |
105 | - private class SelectedMedia | |
83 | + public Guid? SelectedMediaItemId | |
106 | 84 | { |
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 | |
114 | 87 | { |
115 | - this.Item = item; | |
116 | - this.Type = type; | |
117 | - this.Text = text; | |
118 | - } | |
88 | + if (this.selectedMediaItemId == value) | |
89 | + return; | |
119 | 90 | |
120 | - public SelectedMedia(string text) | |
121 | - : this(null, MyCommon.UploadFileType.Invalid, text) | |
122 | - { | |
91 | + this.SetProperty(ref this.selectedMediaItemId, value); | |
92 | + this.LoadSelectedMediaItemImage(); | |
123 | 93 | } |
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; | |
135 | 94 | } |
136 | 95 | |
137 | - private Dictionary<string, IMediaUploadService> pictureService = new(); | |
96 | + public IMediaItem? SelectedMediaItem | |
97 | + => this.SelectedMediaItemId != null ? this.MediaItems.First(x => x.Id == this.SelectedMediaItemId) : null; | |
138 | 98 | |
139 | - private void CreateServices(Twitter tw, TwitterConfiguration twitterConfig) | |
99 | + public int SelectedMediaItemIndex | |
140 | 100 | { |
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; | |
149 | 103 | } |
150 | 104 | |
151 | - public MediaSelector() | |
105 | + public MemoryImage? SelectedMediaItemImage | |
152 | 106 | { |
153 | - this.InitializeComponent(); | |
154 | - | |
155 | - this.ImageSelectedPicture.InitialImage = Properties.Resources.InitialImage; | |
107 | + get => this.selectedMediaItemImage; | |
108 | + set => this.SetProperty(ref this.selectedMediaItemImage, value); | |
156 | 109 | } |
157 | 110 | |
158 | 111 | /// <summary> |
159 | - /// 投稿先サービスなどを初期化する。 | |
112 | + /// 指定された投稿先名から、作成済みの IMediaUploadService インスタンスを取得する。 | |
160 | 113 | /// </summary> |
161 | - public void Initialize(Twitter tw, TwitterConfiguration twitterConfig, string svc, int? index = null) | |
114 | + public IMediaUploadService? GetService(string serviceName) | |
162 | 115 | { |
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 | + } | |
167 | 119 | |
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 | + }; | |
169 | 128 | } |
170 | 129 | |
171 | - /// <summary> | |
172 | - /// 投稿先サービスを再作成する。 | |
173 | - /// </summary> | |
174 | - public void Reset(Twitter tw, TwitterConfiguration twitterConfig) | |
130 | + public void SelectMediaService(string serviceName, int? index = null) | |
175 | 131 | { |
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); | |
177 | 141 | |
178 | - this.SetImageServiceCombo(); | |
142 | + // svc が空白以外かつ存在しないサービス名の場合は Twitter を選択させる | |
143 | + // (廃止されたサービスを選択していた場合の対応) | |
144 | + if (idx == -1) | |
145 | + idx = 0; | |
146 | + } | |
147 | + | |
148 | + this.SelectedMediaServiceName = this.MediaServices[idx].Key; | |
179 | 149 | } |
180 | 150 | |
181 | 151 | /// <summary> |
@@ -187,198 +157,116 @@ namespace OpenTween | ||
187 | 157 | var ext = fl.Extension; |
188 | 158 | var size = ignoreSize ? (long?)null : fl.Length; |
189 | 159 | |
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(); | |
200 | 161 | } |
201 | 162 | |
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) | |
207 | 170 | { |
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; | |
215 | 174 | |
216 | - if (imageService.CheckFileSize(ext, size.Value)) | |
217 | - return true; | |
218 | - } | |
219 | - } | |
220 | - return false; | |
175 | + this.AddMediaItem(mediaItem); | |
176 | + this.SelectedMediaItemId = mediaItem.Id; | |
221 | 177 | } |
222 | 178 | |
223 | - /// <summary> | |
224 | - /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。 | |
225 | - /// </summary> | |
226 | - private void BeginSelection(IMediaItem[] items) | |
179 | + public void AddMediaItemFromFilePath(string[] filePathArray) | |
227 | 180 | { |
228 | - if (items == null || items.Length == 0) | |
229 | - { | |
230 | - this.BeginSelection(); | |
181 | + if (filePathArray.Length == 0) | |
231 | 182 | return; |
232 | - } | |
233 | - | |
234 | - var service = this.SelectedService; | |
235 | - if (service == null) return; | |
236 | 183 | |
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]; | |
242 | 185 | |
243 | - this.BeginSelecting?.Invoke(this, EventArgs.Empty); | |
186 | + // 連番のファイル名を一括でアップロードする場合の利便性のためソートする | |
187 | + var sortedFilePath = filePathArray.OrderBy(x => x); | |
244 | 188 | |
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()) | |
256 | 190 | { |
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; | |
269 | 194 | |
270 | - /// <summary> | |
271 | - /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する(主にD&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; | |
279 | 196 | } |
280 | 197 | |
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; | |
283 | 203 | } |
284 | 204 | |
285 | - /// <summary> | |
286 | - /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。 | |
287 | - /// </summary> | |
288 | - public void BeginSelection(Image image) | |
205 | + public void AddMediaItem(IMediaItem item) | |
289 | 206 | { |
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); | |
298 | 211 | } |
299 | 212 | |
300 | - /// <summary> | |
301 | - /// 投稿するファイルとその投稿先を選択するためのコントロールを表示する。 | |
302 | - /// </summary> | |
303 | - public void BeginSelection() | |
213 | + private MemoryImage GenerateThumbnailImage(IMediaItem item) | |
304 | 214 | { |
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; | |
308 | 218 | |
309 | - this.Visible = true; | |
310 | - this.Enabled = true; | |
219 | + using var bitmap = new Bitmap(thumbSize.Width, thumbSize.Height); | |
311 | 220 | |
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)); | |
315 | 234 | } |
235 | + | |
236 | + return MemoryImage.CopyFromImage(bitmap); | |
316 | 237 | } |
317 | 238 | |
318 | - /// <summary> | |
319 | - /// 選択処理を終了してコントロールを隠す。 | |
320 | - /// </summary> | |
321 | - public void EndSelection() | |
239 | + public void ClearMediaItems() | |
322 | 240 | { |
323 | - if (this.Visible) | |
324 | - { | |
325 | - this.ImagefilePathText.CausesValidation = false; | |
241 | + this.SelectedMediaItemId = null; | |
326 | 242 | |
327 | - this.EndSelecting?.Invoke(this, EventArgs.Empty); | |
243 | + var mediaItems = this.MediaItems.ToList(); | |
244 | + this.MediaItems.Clear(); | |
328 | 245 | |
329 | - this.Visible = false; | |
330 | - this.Enabled = false; | |
331 | - this.ClearImageSelectedPicture(); | |
246 | + foreach (var mediaItem in mediaItems) | |
247 | + mediaItem.Dispose(); | |
332 | 248 | |
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(); | |
335 | 254 | } |
336 | 255 | |
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() | |
341 | 257 | { |
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(); | |
344 | 262 | |
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; | |
377 | 264 | } |
378 | 265 | |
379 | - private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image, bool noMsgBox) | |
266 | + private MemoryImageMediaItem? CreateMemoryImageMediaItem(Image image) | |
380 | 267 | { |
381 | - if (image == null) return null; | |
268 | + if (image == null) | |
269 | + return null; | |
382 | 270 | |
383 | 271 | MemoryImage? memoryImage = null; |
384 | 272 | try |
@@ -391,15 +279,14 @@ namespace OpenTween | ||
391 | 279 | catch |
392 | 280 | { |
393 | 281 | memoryImage?.Dispose(); |
394 | - | |
395 | - if (!noMsgBox) MessageBox.Show("Unable to create MemoryImage."); | |
396 | 282 | return null; |
397 | 283 | } |
398 | 284 | } |
399 | 285 | |
400 | - private IMediaItem? CreateFileMediaItem(string path, bool noMsgBox) | |
286 | + private FileMediaItem? CreateFileMediaItem(string path) | |
401 | 287 | { |
402 | - if (MyCommon.IsNullOrEmpty(path)) return null; | |
288 | + if (MyCommon.IsNullOrEmpty(path)) | |
289 | + return null; | |
403 | 290 | |
404 | 291 | try |
405 | 292 | { |
@@ -407,428 +294,100 @@ namespace OpenTween | ||
407 | 294 | } |
408 | 295 | catch |
409 | 296 | { |
410 | - if (!noMsgBox) MessageBox.Show("Invalid file path: " + path); | |
411 | 297 | return null; |
412 | 298 | } |
413 | 299 | } |
414 | 300 | |
415 | - private void ValidateNewFileMediaItem(string path, string altText, bool noMsgBox) | |
301 | + private void LoadSelectedMediaItemImage() | |
416 | 302 | { |
417 | - var media = (SelectedMedia)this.ImagePageCombo.SelectedItem; | |
418 | - var item = media.Item; | |
303 | + var previousImage = this.selectedMediaItemImage; | |
419 | 304 | |
420 | - if (path != media.Path) | |
305 | + if (this.SelectedMediaItem == null) | |
421 | 306 | { |
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(); | |
470 | 309 | return; |
471 | 310 | } |
472 | 311 | |
473 | - this.ValidateNewFileMediaItem(this.ImagefilePathText.Text.Trim(), this.AlternativeTextBox.Text.Trim(), false); | |
312 | + this.SelectedMediaItemImage = this.CreateMediaItemImage(this.SelectedMediaItem); | |
313 | + previousImage?.Dispose(); | |
474 | 314 | } |
475 | 315 | |
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) | |
480 | 317 | { |
481 | - var valid = false; | |
482 | - | |
483 | 318 | try |
484 | 319 | { |
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(); | |
555 | 321 | } |
556 | - finally | |
322 | + catch (InvalidImageException) | |
557 | 323 | { |
558 | - if (!valid) | |
559 | - { | |
560 | - this.ClearImagePage(index); | |
561 | - this.DisposeMediaItem(item); | |
562 | - } | |
324 | + return MemoryImage.CopyFromImage(Properties.Resources.MultiMediaImage); | |
563 | 325 | } |
564 | 326 | } |
565 | 327 | |
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) | |
582 | 329 | { |
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; | |
631 | 333 | |
632 | - this.SelectImageServiceComboItem(svc); | |
633 | - } | |
334 | + selectedMedia.AltText = altText.Trim(); | |
634 | 335 | } |
635 | 336 | |
636 | - private void SelectImageServiceComboItem(string svc, int? index = null) | |
337 | + public MediaSelectorErrorType Validate(out IMediaItem? rejectedMedia) | |
637 | 338 | { |
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; | |
651 | 340 | |
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; | |
660 | 343 | |
661 | - this.UpdateAltTextPanelVisible(); | |
662 | - } | |
344 | + var uploadService = this.SelectedMediaService; | |
345 | + if (uploadService == null) | |
346 | + return MediaSelectorErrorType.ServiceNotSelected; | |
663 | 347 | |
664 | - private void UpdateAltTextPanelVisible() | |
665 | - => this.AlternativeTextPanel.Visible = this.SelectedService switch | |
348 | + foreach (var mediaItem in this.MediaItems) | |
666 | 349 | { |
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) | |
677 | 352 | { |
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; | |
727 | 355 | } |
728 | 356 | } |
729 | 357 | |
730 | - this.SelectedServiceChanged?.Invoke(this, EventArgs.Empty); | |
358 | + return MediaSelectorErrorType.None; | |
731 | 359 | } |
732 | 360 | |
733 | - private void SetImagePageCombo(SelectedMedia? media = null) | |
361 | + private MediaSelectorErrorType ValidateMediaItem(IMediaUploadService imageService, IMediaItem item) | |
734 | 362 | { |
735 | - using (ControlTransaction.Update(this.ImagePageCombo)) | |
736 | - { | |
737 | - this.ImagePageCombo.Enabled = false; | |
363 | + var ext = item.Extension; | |
364 | + var size = item.Size; | |
738 | 365 | |
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; | |
748 | 368 | |
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; | |
752 | 371 | |
753 | - this.ImagePageCombo.SelectedIndex = 0; | |
754 | - } | |
372 | + return MediaSelectorErrorType.None; | |
755 | 373 | } |
756 | 374 | |
757 | - private void AddNewImagePage(int selectedIndex) | |
375 | + public void Dispose() | |
758 | 376 | { |
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; | |
813 | 379 | |
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(); | |
822 | 382 | } |
383 | + } | |
823 | 384 | |
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, | |
833 | 392 | } |
834 | 393 | } |
@@ -1,25 +1,12 @@ | ||
1 | 1 | namespace OpenTween |
2 | 2 | { |
3 | - partial class MediaSelector | |
3 | + partial class MediaSelectorPanel | |
4 | 4 | { |
5 | 5 | /// <summary> |
6 | 6 | /// 必要なデザイナー変数です。 |
7 | 7 | /// </summary> |
8 | 8 | private System.ComponentModel.IContainer components = null; |
9 | 9 | |
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 | - | |
23 | 10 | #region コンポーネント デザイナーで生成されたコード |
24 | 11 | |
25 | 12 | /// <summary> |
@@ -28,64 +15,26 @@ | ||
28 | 15 | /// </summary> |
29 | 16 | private void InitializeComponent() |
30 | 17 | { |
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)); | |
36 | 19 | this.Label2 = new System.Windows.Forms.Label(); |
37 | 20 | this.ImageServiceCombo = new System.Windows.Forms.ComboBox(); |
38 | 21 | this.ImageCancelButton = new System.Windows.Forms.Button(); |
39 | 22 | this.AlternativeTextPanel = new System.Windows.Forms.Panel(); |
40 | 23 | this.AlternativeTextBox = new System.Windows.Forms.TextBox(); |
41 | 24 | 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(); | |
42 | 30 | this.ImageSelectedPicture = new OpenTween.OTPictureBox(); |
43 | - this.ImagePathPanel.SuspendLayout(); | |
44 | 31 | this.AlternativeTextPanel.SuspendLayout(); |
32 | + this.tableLayoutPanel1.SuspendLayout(); | |
33 | + this.panel1.SuspendLayout(); | |
34 | + this.ServiceSelectPanel.SuspendLayout(); | |
45 | 35 | ((System.ComponentModel.ISupportInitialize)(this.ImageSelectedPicture)).BeginInit(); |
46 | 36 | this.SuspendLayout(); |
47 | 37 | // |
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 | - // | |
89 | 38 | // Label2 |
90 | 39 | // |
91 | 40 | resources.ApplyResources(this.Label2, "Label2"); |
@@ -100,9 +49,6 @@ | ||
100 | 49 | resources.GetString("ImageServiceCombo.Items")}); |
101 | 50 | this.ImageServiceCombo.Name = "ImageServiceCombo"; |
102 | 51 | 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); | |
106 | 52 | // |
107 | 53 | // ImageCancelButton |
108 | 54 | // |
@@ -122,31 +68,73 @@ | ||
122 | 68 | // |
123 | 69 | resources.ApplyResources(this.AlternativeTextBox, "AlternativeTextBox"); |
124 | 70 | 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); | |
126 | 72 | // |
127 | 73 | // AlternativeTextLabel |
128 | 74 | // |
129 | 75 | resources.ApplyResources(this.AlternativeTextLabel, "AlternativeTextLabel"); |
130 | 76 | this.AlternativeTextLabel.Name = "AlternativeTextLabel"; |
131 | 77 | // |
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 | + // | |
132 | 118 | // ImageSelectedPicture |
133 | 119 | // |
134 | 120 | resources.ApplyResources(this.ImageSelectedPicture, "ImageSelectedPicture"); |
135 | 121 | this.ImageSelectedPicture.Name = "ImageSelectedPicture"; |
136 | 122 | this.ImageSelectedPicture.TabStop = false; |
137 | 123 | // |
138 | - // MediaSelector | |
124 | + // MediaSelectorPanel | |
139 | 125 | // |
140 | 126 | resources.ApplyResources(this, "$this"); |
141 | 127 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; |
142 | 128 | this.Controls.Add(this.ImageSelectedPicture); |
143 | 129 | 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"; | |
148 | 132 | this.AlternativeTextPanel.ResumeLayout(false); |
149 | 133 | this.AlternativeTextPanel.PerformLayout(); |
134 | + this.tableLayoutPanel1.ResumeLayout(false); | |
135 | + this.panel1.ResumeLayout(false); | |
136 | + this.panel1.PerformLayout(); | |
137 | + this.ServiceSelectPanel.ResumeLayout(false); | |
150 | 138 | ((System.ComponentModel.ISupportInitialize)(this.ImageSelectedPicture)).EndInit(); |
151 | 139 | this.ResumeLayout(false); |
152 | 140 | this.PerformLayout(); |
@@ -156,15 +144,16 @@ | ||
156 | 144 | #endregion |
157 | 145 | |
158 | 146 | 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; | |
163 | 147 | internal System.Windows.Forms.Label Label2; |
164 | 148 | internal System.Windows.Forms.ComboBox ImageServiceCombo; |
165 | 149 | internal System.Windows.Forms.Button ImageCancelButton; |
166 | 150 | internal System.Windows.Forms.Panel AlternativeTextPanel; |
167 | 151 | internal System.Windows.Forms.TextBox AlternativeTextBox; |
168 | 152 | 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; | |
169 | 158 | } |
170 | 159 | } |
@@ -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 | +} |
@@ -10,8 +10,12 @@ | ||
10 | 10 | <data name="$this.AutoScaleDimensions" type="System.Drawing.SizeF, System.Drawing"><value>96, 96</value></data> |
11 | 11 | <metadata name="$this.Localizable" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"><value>True</value></metadata> |
12 | 12 | <data name="$this.Size" type="System.Drawing.Size, System.Drawing"><value>608, 280</value></data> |
13 | - <data name=">>$this.Name"><value>MediaSelector</value></data> | |
13 | + <data name=">>$this.Name"><value>MediaSelectorPanel</value></data> | |
14 | 14 | <data name=">>$this.Type"><value>System.Windows.Forms.UserControl, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> |
15 | + <data name=">>AddMediaButton.Name"><value>AddMediaButton</value></data> | |
16 | + <data name=">>AddMediaButton.Parent"><value>panel1</value></data> | |
17 | + <data name=">>AddMediaButton.Type"><value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
18 | + <data name=">>AddMediaButton.ZOrder"><value>1</value></data> | |
15 | 19 | <data name=">>AlternativeTextBox.Name"><value>AlternativeTextBox</value></data> |
16 | 20 | <data name=">>AlternativeTextBox.Parent"><value>AlternativeTextPanel</value></data> |
17 | 21 | <data name=">>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 @@ | ||
24 | 28 | <data name=">>AlternativeTextPanel.Parent"><value>$this</value></data> |
25 | 29 | <data name=">>AlternativeTextPanel.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> |
26 | 30 | <data name=">>AlternativeTextPanel.ZOrder"><value>1</value></data> |
27 | - <data name=">>FilePickButton.Name"><value>FilePickButton</value></data> | |
28 | - <data name=">>FilePickButton.Parent"><value>ImagePathPanel</value></data> | |
29 | - <data name=">>FilePickButton.Type"><value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
30 | - <data name=">>FilePickButton.ZOrder"><value>2</value></data> | |
31 | 31 | <data name=">>ImageCancelButton.Name"><value>ImageCancelButton</value></data> |
32 | - <data name=">>ImageCancelButton.Parent"><value>ImagePathPanel</value></data> | |
32 | + <data name=">>ImageCancelButton.Parent"><value>panel1</value></data> | |
33 | 33 | <data name=">>ImageCancelButton.Type"><value>System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> |
34 | - <data name=">>ImageCancelButton.ZOrder"><value>5</value></data> | |
35 | - <data name=">>ImagefilePathText.Name"><value>ImagefilePathText</value></data> | |
36 | - <data name=">>ImagefilePathText.Parent"><value>ImagePathPanel</value></data> | |
37 | - <data name=">>ImagefilePathText.Type"><value>System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
38 | - <data name=">>ImagefilePathText.ZOrder"><value>0</value></data> | |
39 | - <data name=">>ImagePageCombo.Name"><value>ImagePageCombo</value></data> | |
40 | - <data name=">>ImagePageCombo.Parent"><value>ImagePathPanel</value></data> | |
41 | - <data name=">>ImagePageCombo.Type"><value>System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
42 | - <data name=">>ImagePageCombo.ZOrder"><value>1</value></data> | |
43 | - <data name=">>ImagePathPanel.Name"><value>ImagePathPanel</value></data> | |
44 | - <data name=">>ImagePathPanel.Parent"><value>$this</value></data> | |
45 | - <data name=">>ImagePathPanel.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
46 | - <data name=">>ImagePathPanel.ZOrder"><value>2</value></data> | |
34 | + <data name=">>ImageCancelButton.ZOrder"><value>0</value></data> | |
47 | 35 | <data name=">>ImageSelectedPicture.Name"><value>ImageSelectedPicture</value></data> |
48 | 36 | <data name=">>ImageSelectedPicture.Parent"><value>$this</value></data> |
49 | - <data name=">>ImageSelectedPicture.Type"><value>OpenTween.OTPictureBox, OpenTween, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null</value></data> | |
37 | + <data name=">>ImageSelectedPicture.Type"><value>OpenTween.OTPictureBox, OpenTween, Version=3.1.0.1, Culture=neutral, PublicKeyToken=null</value></data> | |
50 | 38 | <data name=">>ImageSelectedPicture.ZOrder"><value>0</value></data> |
51 | 39 | <data name=">>ImageServiceCombo.Name"><value>ImageServiceCombo</value></data> |
52 | - <data name=">>ImageServiceCombo.Parent"><value>ImagePathPanel</value></data> | |
40 | + <data name=">>ImageServiceCombo.Parent"><value>ServiceSelectPanel</value></data> | |
53 | 41 | <data name=">>ImageServiceCombo.Type"><value>System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> |
54 | - <data name=">>ImageServiceCombo.ZOrder"><value>4</value></data> | |
42 | + <data name=">>ImageServiceCombo.ZOrder"><value>0</value></data> | |
55 | 43 | <data name=">>Label2.Name"><value>Label2</value></data> |
56 | - <data name=">>Label2.Parent"><value>ImagePathPanel</value></data> | |
44 | + <data name=">>Label2.Parent"><value>ServiceSelectPanel</value></data> | |
57 | 45 | <data name=">>Label2.Type"><value>System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> |
58 | - <data name=">>Label2.ZOrder"><value>3</value></data> | |
46 | + <data name=">>Label2.ZOrder"><value>1</value></data> | |
47 | + <data name=">>MediaListView.Name"><value>MediaListView</value></data> | |
48 | + <data name=">>MediaListView.Parent"><value>tableLayoutPanel1</value></data> | |
49 | + <data name=">>MediaListView.Type"><value>System.Windows.Forms.ListView, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
50 | + <data name=">>MediaListView.ZOrder"><value>0</value></data> | |
51 | + <data name=">>panel1.Name"><value>panel1</value></data> | |
52 | + <data name=">>panel1.Parent"><value>tableLayoutPanel1</value></data> | |
53 | + <data name=">>panel1.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
54 | + <data name=">>panel1.ZOrder"><value>1</value></data> | |
55 | + <data name=">>ServiceSelectPanel.Name"><value>ServiceSelectPanel</value></data> | |
56 | + <data name=">>ServiceSelectPanel.Parent"><value>panel1</value></data> | |
57 | + <data name=">>ServiceSelectPanel.Type"><value>System.Windows.Forms.Panel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
58 | + <data name=">>ServiceSelectPanel.ZOrder"><value>2</value></data> | |
59 | + <data name=">>tableLayoutPanel1.Name"><value>tableLayoutPanel1</value></data> | |
60 | + <data name=">>tableLayoutPanel1.Parent"><value>$this</value></data> | |
61 | + <data name=">>tableLayoutPanel1.Type"><value>System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> | |
62 | + <data name=">>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> | |
59 | 70 | <data name="AlternativeTextBox.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms"><value>Top, Left, Right</value></data> |
60 | 71 | <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> | |
62 | 75 | <data name="AlternativeTextBox.TabIndex" type="System.Int32, mscorlib"><value>1</value></data> |
63 | 76 | <data name="AlternativeTextLabel.Anchor" type="System.Windows.Forms.AnchorStyles, System.Windows.Forms"><value>Top, Bottom, Left</value></data> |
64 | 77 | <data name="AlternativeTextLabel.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data> |
65 | 78 | <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> | |
67 | 80 | <data name="AlternativeTextLabel.TabIndex" type="System.Int32, mscorlib"><value>0</value></data> |
68 | 81 | <data name="AlternativeTextLabel.Text"><value>代替テキスト(&A):</value></data> |
69 | 82 | <data name="AlternativeTextLabel.TextAlign" type="System.Drawing.ContentAlignment, System.Drawing"><value>MiddleRight</value></data> |
70 | 83 | <data name="AlternativeTextPanel.AutoSize" type="System.Boolean, mscorlib"><value>True</value></data> |
71 | 84 | <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> | |
83 | 89 | <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> | |
101 | 95 | <data name="ImageSelectedPicture.Dock" type="System.Windows.Forms.DockStyle, System.Windows.Forms"><value>Fill</value></data> |
102 | 96 | <data name="ImageSelectedPicture.ImeMode" type="System.Windows.Forms.ImeMode, System.Windows.Forms"><value>NoControl</value></data> |
103 | 97 | <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> | |
105 | 99 | <data name="ImageSelectedPicture.SizeMode" type="System.Windows.Forms.PictureBoxSizeMode, System.Windows.Forms"><value>Zoom</value></data> |
106 | 100 | <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> | |
108 | 102 | <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> | |
113 | 107 | <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> | |
117 | 111 | <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><?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="MediaListView" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="panel1" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /></Controls><Columns Styles="Percent,100,Absolute,173" /><Rows Styles="Percent,100" /></TableLayoutSettings></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> | |
119 | 134 | </root> |
@@ -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 | +} |
@@ -118,11 +118,11 @@ | ||
118 | 118 | <Compile Update="InputDialog.Designer.cs"> |
119 | 119 | <DependentUpon>InputDialog.cs</DependentUpon> |
120 | 120 | </Compile> |
121 | - <Compile Update="MediaSelector.cs"> | |
121 | + <Compile Update="MediaSelectorPanel.cs"> | |
122 | 122 | <SubType>UserControl</SubType> |
123 | 123 | </Compile> |
124 | - <Compile Update="MediaSelector.Designer.cs"> | |
125 | - <DependentUpon>MediaSelector.cs</DependentUpon> | |
124 | + <Compile Update="MediaSelectorPanel.Designer.cs"> | |
125 | + <DependentUpon>MediaSelectorPanel.cs</DependentUpon> | |
126 | 126 | </Compile> |
127 | 127 | <Compile Update="NativeMethods.cs"> |
128 | 128 | <SubType>Code</SubType> |
@@ -386,11 +386,11 @@ | ||
386 | 386 | <EmbeddedResource Update="LoginDialog.resx"> |
387 | 387 | <DependentUpon>LoginDialog.cs</DependentUpon> |
388 | 388 | </EmbeddedResource> |
389 | - <EmbeddedResource Update="MediaSelector.en.resx"> | |
390 | - <DependentUpon>MediaSelector.cs</DependentUpon> | |
389 | + <EmbeddedResource Update="MediaSelectorPanel.en.resx"> | |
390 | + <DependentUpon>MediaSelectorPanel.cs</DependentUpon> | |
391 | 391 | </EmbeddedResource> |
392 | - <EmbeddedResource Update="MediaSelector.resx"> | |
393 | - <DependentUpon>MediaSelector.cs</DependentUpon> | |
392 | + <EmbeddedResource Update="MediaSelectorPanel.resx"> | |
393 | + <DependentUpon>MediaSelectorPanel.cs</DependentUpon> | |
394 | 394 | </EmbeddedResource> |
395 | 395 | <EmbeddedResource Update="SendErrorReportForm.en.resx"> |
396 | 396 | <DependentUpon>SendErrorReportForm.cs</DependentUpon> |
@@ -22,7 +22,7 @@ using System.Runtime.InteropServices; | ||
22 | 22 | // 次の GUID は、このプロジェクトが COM に公開される場合の、typelib の ID です |
23 | 23 | [assembly: Guid("2d0ae0ba-adac-49a2-9b10-26fd69e695bf")] |
24 | 24 | |
25 | -[assembly: AssemblyVersion("3.1.0.0")] | |
25 | +[assembly: AssemblyVersion("3.2.0.0")] | |
26 | 26 | |
27 | 27 | [assembly: InternalsVisibleTo("OpenTween.Tests")] |
28 | 28 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // for Moq |
@@ -571,6 +571,10 @@ namespace OpenTween.Properties { | ||
571 | 571 | /// <summary> |
572 | 572 | /// 更新履歴 |
573 | 573 | /// |
574 | + ///==== Ver 3.2.0(2023/01/20) | |
575 | + /// * NEW: 複数枚の画像を添付する際にリスト上で画像を確認できるようになりました | |
576 | + /// * CHG: アカウント追加時の認可関連のエラーメッセージがより詳細になるように変更 | |
577 | + /// | |
574 | 578 | ///==== Ver 3.1.0(2023/01/14) |
575 | 579 | /// * NEW: 引用ツイートを Ctrl+Shift+L で実行するショートカットを追加 (thx @WizardOfPSG!) |
576 | 580 | /// * CHG: 発言一覧のフォントサイズがアイコンより大きい場合は項目の高さをフォントサイズに合わせるように変更 |
@@ -579,8 +583,7 @@ namespace OpenTween.Properties { | ||
579 | 583 | /// |
580 | 584 | ///==== Ver 3.0.0(2023/01/11) |
581 | 585 | /// * 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 が含まれているため追加 [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 | |
586 | + /// - .NET Framework 4.8 ランタイムは https://dotnet.m [残りの文字列は切り詰められました]"; に類似しているローカライズされた文字列を検索します。 | |
584 | 587 | /// </summary> |
585 | 588 | internal static string ChangeLog { |
586 | 589 | get { |
@@ -1,5 +1,9 @@ | ||
1 | 1 | 更新履歴 |
2 | 2 | |
3 | +==== Ver 3.2.0(2023/01/20) | |
4 | + * NEW: 複数枚の画像を添付する際にリスト上で画像を確認できるようになりました | |
5 | + * CHG: アカウント追加時の認可関連のエラーメッセージがより詳細になるように変更 | |
6 | + | |
3 | 7 | ==== Ver 3.1.0(2023/01/14) |
4 | 8 | * NEW: 引用ツイートを Ctrl+Shift+L で実行するショートカットを追加 (thx @WizardOfPSG!) |
5 | 9 | * CHG: 発言一覧のフォントサイズがアイコンより大きい場合は項目の高さをフォントサイズに合わせるように変更 |
@@ -53,7 +53,7 @@ | ||
53 | 53 | this.ToolStripSeparator11 = new System.Windows.Forms.ToolStripSeparator(); |
54 | 54 | this.DeleteTabMenuItem = new System.Windows.Forms.ToolStripMenuItem(); |
55 | 55 | this.TabImage = new System.Windows.Forms.ImageList(this.components); |
56 | - this.ImageSelector = new OpenTween.MediaSelector(); | |
56 | + this.ImageSelector = new OpenTween.MediaSelectorPanel(); | |
57 | 57 | this.ProfilePanel = new System.Windows.Forms.Panel(); |
58 | 58 | this.SplitContainer3 = new System.Windows.Forms.SplitContainer(); |
59 | 59 | this.SplitContainer2 = new System.Windows.Forms.SplitContainer(); |
@@ -2217,7 +2217,7 @@ | ||
2217 | 2217 | internal System.Windows.Forms.ToolStripSeparator ToolStripSeparator11; |
2218 | 2218 | internal System.Windows.Forms.ToolStripMenuItem DeleteTabMenuItem; |
2219 | 2219 | internal System.Windows.Forms.ImageList TabImage; |
2220 | - internal MediaSelector ImageSelector; | |
2220 | + internal MediaSelectorPanel ImageSelector; | |
2221 | 2221 | internal System.Windows.Forms.Panel ProfilePanel; |
2222 | 2222 | internal System.Windows.Forms.SplitContainer SplitContainer3; |
2223 | 2223 | internal System.Windows.Forms.SplitContainer SplitContainer2; |
@@ -555,7 +555,8 @@ namespace OpenTween | ||
555 | 555 | Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection; |
556 | 556 | |
557 | 557 | // 画像投稿サービス |
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); | |
559 | 560 | |
560 | 561 | this.tweetThumbnail1.Initialize(this.thumbGenerator); |
561 | 562 |
@@ -1282,7 +1283,8 @@ namespace OpenTween | ||
1282 | 1283 | if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems)) |
1283 | 1284 | return; |
1284 | 1285 | |
1285 | - uploadService = this.ImageSelector.GetService(serviceName); | |
1286 | + this.ImageSelector.EndSelection(); | |
1287 | + uploadService = this.ImageSelector.Model.GetService(serviceName); | |
1286 | 1288 | } |
1287 | 1289 | |
1288 | 1290 | this.inReplyTo = null; |
@@ -1950,7 +1952,7 @@ namespace OpenTween | ||
1950 | 1952 | |
1951 | 1953 | if (this.tw.Configuration.PhotoSizeLimit != 0) |
1952 | 1954 | { |
1953 | - foreach (var service in this.ImageSelector.GetServices()) | |
1955 | + foreach (var (_, service) in this.ImageSelector.Model.MediaServices) | |
1954 | 1956 | { |
1955 | 1957 | service.UpdateTwitterConfiguration(this.tw.Configuration); |
1956 | 1958 | } |
@@ -2583,7 +2585,7 @@ namespace OpenTween | ||
2583 | 2585 | this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck; |
2584 | 2586 | this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost; |
2585 | 2587 | |
2586 | - this.ImageSelector.Reset(this.tw, this.tw.Configuration); | |
2588 | + this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration); | |
2587 | 2589 | |
2588 | 2590 | try |
2589 | 2591 | { |
@@ -3518,7 +3520,7 @@ namespace OpenTween | ||
3518 | 3520 | attachmentUrl = null; |
3519 | 3521 | |
3520 | 3522 | // 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) | |
3522 | 3524 | return statusText; |
3523 | 3525 | |
3524 | 3526 | var match = Twitter.AttachmentUrlRegex.Match(statusText); |
@@ -3661,7 +3663,7 @@ namespace OpenTween | ||
3661 | 3663 | } |
3662 | 3664 | |
3663 | 3665 | private IMediaUploadService? GetSelectedImageService() |
3664 | - => this.ImageSelector.Visible ? this.ImageSelector.SelectedService : null; | |
3666 | + => this.ImageSelector.Visible ? this.ImageSelector.Model.SelectedMediaService : null; | |
3665 | 3667 | |
3666 | 3668 | /// <summary> |
3667 | 3669 | /// 全てのタブの振り分けルールを反映し直します |
@@ -5732,8 +5734,8 @@ namespace OpenTween | ||
5732 | 5734 | this.settings.Common.HashIsHead = this.HashMgr.IsHead; |
5733 | 5735 | this.settings.Common.HashIsPermanent = this.HashMgr.IsPermanent; |
5734 | 5736 | 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; | |
5737 | 5739 | |
5738 | 5740 | this.settings.SaveCommon(); |
5739 | 5741 | } |
@@ -9168,7 +9170,7 @@ namespace OpenTween | ||
9168 | 9170 | |
9169 | 9171 | private void SelectMedia_DragEnter(DragEventArgs e) |
9170 | 9172 | { |
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)) | |
9172 | 9174 | { |
9173 | 9175 | e.Effect = DragDropEffects.Copy; |
9174 | 9176 | return; |
@@ -9180,7 +9182,10 @@ namespace OpenTween | ||
9180 | 9182 | { |
9181 | 9183 | this.Activate(); |
9182 | 9184 | 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); | |
9184 | 9189 | this.StatusText.Focus(); |
9185 | 9190 | } |
9186 | 9191 |
@@ -9231,12 +9236,14 @@ namespace OpenTween | ||
9231 | 9236 | { |
9232 | 9237 | // clipboardから画像を取得 |
9233 | 9238 | using var image = Clipboard.GetImage(); |
9234 | - this.ImageSelector.BeginSelection(image); | |
9239 | + this.ImageSelector.BeginSelection(); | |
9240 | + this.ImageSelector.Model.AddMediaItemFromImage(image); | |
9235 | 9241 | } |
9236 | 9242 | else if (Clipboard.ContainsFileDropList()) |
9237 | 9243 | { |
9238 | 9244 | var files = Clipboard.GetFileDropList().Cast<string>().ToArray(); |
9239 | - this.ImageSelector.BeginSelection(files); | |
9245 | + this.ImageSelector.BeginSelection(); | |
9246 | + this.ImageSelector.Model.AddMediaItemFromFilePath(files); | |
9240 | 9247 | } |
9241 | 9248 | } |
9242 | 9249 | catch (ExternalException ex) |
@@ -156,7 +156,7 @@ | ||
156 | 156 | <data name=">>ImageSelectMenuItem.Type"><value>System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> |
157 | 157 | <data name=">>ImageSelector.Name"><value>ImageSelector</value></data> |
158 | 158 | <data name=">>ImageSelector.Parent"><value>SplitContainer1.Panel1</value></data> |
159 | - <data name=">>ImageSelector.Type"><value>OpenTween.MediaSelector, OpenTween, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null</value></data> | |
159 | + <data name=">>ImageSelector.Type"><value>OpenTween.MediaSelectorPanel, OpenTween, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null</value></data> | |
160 | 160 | <data name=">>ImageSelector.ZOrder"><value>1</value></data> |
161 | 161 | <data name=">>ImageSelectPullDownMenuItem.Name"><value>ImageSelectPullDownMenuItem</value></data> |
162 | 162 | <data name=">>ImageSelectPullDownMenuItem.Type"><value>System.Windows.Forms.ToolStripMenuItem, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value></data> |
@@ -1,4 +1,4 @@ | ||
1 | -version: 3.0.0.{build} | |
1 | +version: 3.1.0.{build} | |
2 | 2 | |
3 | 3 | os: Visual Studio 2022 |
4 | 4 |